-- -- Copyright © 2013-2015 Anchor Systems, Pty Ltd and Others -- -- The code in this file, and the program it is a part of, is -- made available to you by its authors as open source software: -- you can redistribute it and/or modify it under the terms of -- the 3-clause BSD licence. -- {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeFamilies #-} module Git.Vogue where import Control.Applicative import Control.Exception import Control.Monad import Control.Monad.IO.Class import Control.Monad.Reader import Data.List import Data.Monoid import Data.String.Utils import System.Directory import System.Exit import System.FilePath import System.IO import System.Posix.Files import System.Process import Git.Vogue.Plugins import Git.Vogue.Types import Paths_git_vogue -- | Options parsed from the command-line. data VogueOptions = Options { optSearch :: SearchMode , optCommand :: VogueCommand } deriving (Eq, Show) -- | Commands, with parameters, to be executed. data VogueCommand -- | Add git-vogue support to a git repository. = CmdInit { templatePath :: Maybe FilePath } -- | Verify that support is installed and plugins happen. | CmdVerify -- | List the plugins that git-vogue knows about. | CmdPlugins -- | Run check plugins on files in a git repository. | CmdRunCheck -- | Run fix plugins on files in a git repository. | CmdRunFix deriving (Eq, Show) -- | Plugins that git-vogue knows about. -- FIXME: this will become the fix/check modules newtype Vogue m x = Vogue { vogue :: ReaderT [Plugin] m x } deriving ( Functor, Applicative, Monad , MonadTrans, MonadIO, MonadReader [Plugin] ) -- | Execute a Vogue program runVogue :: [Plugin] -> Vogue m a -> m a runVogue ps (Vogue act) = runReaderT act ps -- | Execute a git-vogue command. runCommand :: (MonadIO m, Functor m) => VogueCommand -> SearchMode -> Vogue m () runCommand CmdInit{..} _ = runWithRepoPath (gitAddHook templatePath) runCommand CmdVerify _ = runWithRepoPath (gitCheckHook runsVogue) runCommand CmdPlugins _ = listPlugins runCommand CmdRunCheck search = runCheck search runCommand CmdRunFix search = runFix search -- | Try to fix the broken things. We first do one pass to check what's broken, -- then only run fix on those. runFix :: (MonadIO m, Functor m) => SearchMode -> Vogue m () runFix sm = do -- See which plugins failed first rs <- ask >>= mapM (\x -> (x,) <$> executeCheck ioPluginExecutorImpl sm x) -- Now fix the failed ones only getWorst (executeFix ioPluginExecutorImpl sm) [ x | (x, Failure{}) <- rs ] >>= outputStatusAndExit -- | Check for broken things. runCheck :: MonadIO m => SearchMode -> Vogue m () runCheck sm = ask >>= getWorst (executeCheck ioPluginExecutorImpl sm) >>= outputStatusAndExit -- | Find the git repository path and pass it to an action. -- -- Throws an error if the PWD is not in a git repo. runWithRepoPath :: MonadIO m => (FilePath -> m a) -> m a runWithRepoPath action = -- Get the path to the git repo top-level directory. liftIO (readProcess "git" ["rev-parse", "--show-toplevel"] "") >>= action . strip --- | Command string to insert into pre-commit hooks. preCommitCommand :: String preCommitCommand = "git-vogue check" -- | Add the git pre-commit hook. gitAddHook :: MonadIO m => Maybe FilePath -- ^ Template path -> FilePath -- ^ Hook path -> Vogue m () gitAddHook template path = liftIO $ do let hook = path ".git" "hooks" "pre-commit" exists <- fileExist hook if exists then updateHook hook else createHook hook where createHook = copyHookTemplateTo template updateHook hook = do content <- readFile hook unless (preCommitCommand `isInfixOf` content) $ do putStrLn $ "A pre-commit hook already exists at \n\t" <> hook <> "\nbut it does not contain the command\n\t" <> preCommitCommand <> "\nPlease edit the hook and add this command yourself!" exitFailure putStrLn "Your commit hook is already in place." -- | Copy the template pre-commit hook to a git repo. copyHookTemplateTo :: Maybe FilePath -> FilePath -> IO () copyHookTemplateTo maybe_t hook = do template <- maybe (getDataFileName "templates/pre-commit") return maybe_t copyFile template hook perm <- getPermissions hook setPermissions hook $ perm { executable = True } -- | Use a predicate to check a git commit hook. gitCheckHook :: MonadIO m => (FilePath -> IO Bool) -> FilePath -> Vogue m () gitCheckHook p path = do let hook = path ".git" "hooks" "pre-commit" -- Check it exists (so openFile doesn't explode). exists <- liftIO . fileExist $ hook if exists then checkPredicate hook else failWith $ "Missing file " <> hook liftIO exitSuccess where checkPredicate hook = liftIO $ do pass <- p hook unless pass $ failWith "Invalid configuration." failWith msg = liftIO $ do hPutStrLn stderr msg exitFailure -- | Check that a script seems to run git vogue. runsVogue :: FilePath -> IO Bool runsVogue path = do c <- readFile path return $ preCommitCommand `isInfixOf` c -- | Print a list of all plugins. listPlugins :: MonadIO m => Vogue m () listPlugins = do dir <- liftIO (( "git-vogue") <$> getLibexecDir) liftIO . putStrLn $ "git-vogue looks for plugins in:\n\n\t" <> dir <> "\n" plugins <- ask liftIO . putStr $ "git-vogue knows about the following plugins:\n\n" <> unlines (fmap (('\t':) . unPlugin) plugins) -- | Get list of disabled plugins from git configuration. disabledPlugins :: (Monad m, Functor m, MonadIO m) => m [String] disabledPlugins = lines <$> liftIO (readConfig `catch` none) where readConfig = readProcess "git" ["config", "--get-all", "vogue.disable"] "" none (SomeException _) = return []