git-vogue (empty) → 0.1.0.0
raw patch · 15 files changed
+1488/−0 lines, 15 filesdep +Cabaldep +Diffdep +MissingHbuild-type:Customsetup-changed
Dependencies added: Cabal, Diff, MissingH, base, bifunctors, cpphs, directory, filepath, formatting, ghc-mod, git-vogue, haskell-src-exts, hlint, hscolour, hspec, mtl, optparse-applicative, process, split, strict, stylish-haskell, text, transformers, transformers-base, unix
Files
- LICENSE +30/−0
- README.md +141/−0
- Setup.hs +105/−0
- git-vogue.cabal +139/−0
- lib/Git/Vogue.hs +192/−0
- lib/Git/Vogue/Plugins.hs +111/−0
- lib/Git/Vogue/Types.hs +50/−0
- src/git-vogue-cabal.hs +72/−0
- src/git-vogue-ghc-mod.hs +93/−0
- src/git-vogue-hlint.hs +117/−0
- src/git-vogue-stylish.hs +140/−0
- src/git-vogue.hs +101/−0
- templates/pre-commit +3/−0
- tests/git-setup.hs +106/−0
- tests/plugins.hs +88/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2014, Anchor Engineering <engineering@anchor.com.au>++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Anchor Engineering <engineering@anchor.com.au> nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,141 @@+git-vogue - A framework for pre-commit checks +=========================================================++[](https://travis-ci.org/anchor/git-vogue)++Intended to be used as a git pre-commit hook, *git-vogue* encourages developers+to keep their Haskell code ["en vogue"][1] by providing a framework for+checking code quality and some supporting plugins.++Currently, *git-vogue* ships with the following plugins:++* [cabal][6]+* [hlint][2]+* [stylish-haskell][4] with automatic fixing+* [ghc-mod][5]++[1]: https://www.youtube.com/watch?v=GuJQSAiODqI+[2]: http://hackage.haskell.org/package/hlint+[4]: https://hackage.haskell.org/package/stylish-haskell+[5]: https://hackage.haskell.org/package/ghc-mod+[6]: https://hackage.haskell.org/package/Cabal++Quickstart+----------++```bash+cabal install git-vogue+```++If you wish to set up pre-commit hooks (recommended):++```bash+git vogue init+```++With pre-commit hooks set up, `git vogue check` will be run before every+commit. If you wish to check the whole repository, run `git vogue check --all`.++You can attempt to automatically rectify any problems discovered via `git vogue+fix` and `git vogue fix --all`. The only plugin that currently supports this+auto-fixing is stylish-haskell.++# Plugin discovery/disabling++Running `git-vogue plugins` will show you the libexec directory in which+git-vogue will discover plugins.++Should one or more plugins annoy you, you may disable it by setting it+non-executable:++```bash+chmod -x .cabal/libexec/git-vogue/git-vogue-stylish+```++A more sophisticated interface to plugin manipulation is planned.++# Plugins++## cabal++Checks your .cabal file for packaging problems. Can not fix problems+automatically.++## hlint++Checks .hs files for linting issues, respects `HLint.hs` in the top level of+the repository. Can not fix problems automatically.++## ghc-mod++Checks .hs files (excluding `HLint.hs` and `Setup.hs`) as per ghc-mod check.+ghc-mod can be temperamental, so if this fails to run the plugin will allow the+commit to pass. Can not fix problems automatically.++## stylish-haskell++Checks if .hs files would have been modified by stylish-haskell. Respects+`.stylish-haskell.yaml`. Can fix problems automatically.++Uninstalling +------------++To remove a `git-vogue init` configured pre-commit hook, run:++```bash+rm .git/hooks/pre-commit+```++[Here are instructions](https://www.youtube.com/watch?v=4qXD5l-ZlfA) for+uninstalling a cabal package.++Philosophy+---------++At Anchor Engineering, we're pretty un-dogmatic about using one editor or even+one OS for development. As such, we've found ourselves in need of a common+benchmark for linting, formatting and code quality checks.++We wanted a tool that would:++* Install in one command+* Require nothing to be learned for a new developer+* Tell that developer what they need to fix in the code that they modify and+ that code only.++git-vogue aims to satisfy these needs, whilst saving developers from the+drudgery of installing, configuring and running each of these tools+independently.++Plugin specification+-------------------++**The interface** for an executable (to be called by git-vogue) is a single+command line argument, one of:++* check+* fix+* name++The plugin can assume that the CWD will be set to the top-level directory of+the package.++The plugin will receive a list of all files in the current repository that may+be looked at via STDIN when running in "check" or "fix" mode. These file paths+will be absolute and newline separated. The plugin is expected to filter them+appropriate to its needs.++## Invariants for well-behaved plugin commands++* `name` will return a human-readable name one line+* `check` will not modify any files+* `check` will exit with a return code of:+ * No errors - 0+ * Errors need fixing - 1+ * Catastrophic failure to check - 2+* `fix` is idempotent+* `fix` will exit with a return code of:+ * The code is now good (changes may or may not have been made) - 0+ * Some errors remain - 1+ * Catastrophic failure to check - 2+* If `fix` returns "success" (return code 0), `check` must no longer fail
+ Setup.hs view
@@ -0,0 +1,105 @@+module Main where++import Control.Monad+import Data.List+import Distribution.PackageDescription (PackageDescription (..))+import Distribution.Simple+import Distribution.Simple.Install+import Distribution.Simple.LocalBuildInfo+import Distribution.Simple.Setup+import Distribution.Simple.Utils+import System.FilePath++-- | Run cabal with custom copyHook which puts our extra executables in the+-- libexec directory.+main :: IO ()+main = defaultMainWithHooks $ simpleUserHooks+ { copyHook = copyThings+ }++-- | Copy "extra" executables to $PREFIX/libexec/git-vogue/ instead of+-- $PREFIX/bin.+copyThings+ :: PackageDescription+ -> LocalBuildInfo+ -> UserHooks+ -> CopyFlags+ -> IO ()+copyThings pkg lbi _ flags = do+ -- First install only the "main" components with the default settings.+ let (main_pkg, main_lbi) = tweakMainInstall pkg lbi+ install main_pkg main_lbi flags++ -- Then install the "secondary" executables and plugin scripts with the+ -- special settings.+ let (sub_pkg, sub_lbi) = tweakSubcommandInstall pkg lbi+ install sub_pkg sub_lbi flags++ -- Install shell scripts from plugins/.+ --+ -- This was stolen from "Distribution.Simple.Install" and hacked. Why is+ -- cabal so terrible compared to make and friends?+ let scripts = fmap stripPluginPrefix . filter isPlugin $ dataFiles pkg+ verbosity = fromFlag (copyVerbosity flags)+ src_data_dir = dataDir sub_pkg+ dest_data_dir = datadir . absoluteInstallDirs sub_pkg sub_lbi $+ fromFlag (copyDest flags)+ forM_ scripts $ \ file -> do+ let dir = takeDirectory file+ files <- matchDirFileGlob src_data_dir file+ createDirectoryIfMissingVerbose verbosity True (dest_data_dir </> dir)+ forM_ files $ \file' ->+ installExecutableFile verbosity (src_data_dir </> file')+ (dest_data_dir </> file')++-- | Directory our plugin scripts are kept in.+pluginPrefix :: FilePath+pluginPrefix = "plugins/"++-- | Strip the 'pluginPrefix' off the front of a 'FilePath'.+stripPluginPrefix :: FilePath -> FilePath+stripPluginPrefix = dropWhile (== '/') . dropWhile (/= '/')++-- | A data file is in the plugins directory.+isPlugin :: FilePath -> Bool+isPlugin = (pluginPrefix `isPrefixOf`)++-- | Tweak parameters for install of main components (i.e. library and first+-- executable) only.+tweakMainInstall+ :: PackageDescription+ -> LocalBuildInfo+ -> (PackageDescription, LocalBuildInfo)+tweakMainInstall pkg lbi =+ let pkg' = pkg { executables = take 1 $ executables pkg+ , dataFiles = filter (not . isPlugin) $ dataFiles pkg+ }+ in (pkg', lbi)++-- | Tweak parameters for install of secondary components (i.e. executables+-- other than the first.+tweakSubcommandInstall+ :: PackageDescription+ -> LocalBuildInfo+ -> (PackageDescription, LocalBuildInfo)+tweakSubcommandInstall pkg lbi =+ let dest = suffixIt . libexecdir $ installDirTemplates lbi+ pkg' = pkg { executables = tail $ executables pkg+ , dataFiles = []+ , dataDir = dataDir pkg </> pluginPrefix+ , library = Nothing+ , testSuites = []+ , benchmarks = []+ , extraSrcFiles = []+ , extraTmpFiles = []+ , extraDocFiles = []+ }+ lbi' = lbi { installDirTemplates = (installDirTemplates lbi)+ { bindir = dest+ , datadir = dest+ , datasubdir = toPathTemplate ""+ }+ }+ in (pkg', lbi')+ where+ suffixIt = toPathTemplate . (</> "git-vogue") . fromPathTemplate
+ git-vogue.cabal view
@@ -0,0 +1,139 @@+name: git-vogue+version: 0.1.0.0+synopsis: A framework for pre-commit checks.+description: Make your Haskell git repositories fashionable.+homepage: https://github.com/anchor/git-vogue+license: BSD3+license-file: LICENSE+author: Anchor Engineering <engineering@anchor.com.au>+maintainer: Anchor Engineering <engineering@anchor.com.au>+copyright: (c) 2015 Anchor Systems, Pty Ltd and Others+category: Development+build-type: Custom+extra-source-files: README.md+cabal-version: >=1.10+data-files: templates/pre-commit++source-repository HEAD+ type: git+ location: https://github.com/anchor/git-vogue++flag gpl+ default: True+ description: Use GPL libraries, specifically hscolour++library+ default-language: Haskell2010+ hs-source-dirs: lib+ exposed-modules: Git.Vogue+ Git.Vogue.Types+ Git.Vogue.Plugins+ other-modules: Paths_git_vogue+ build-depends: base >=4.7 && <4.8+ , MissingH+ , directory+ , filepath+ , process+ , formatting+ , split+ , text+ , transformers+ , mtl+ , unix+ -- To fix depedencies+ , hlint >= 1.9 && < 1.10+ , stylish-haskell >= 0.5.11 && < 0.5.12+ , ghc-mod >= 5.2 && < 5.3++executable git-vogue+ default-language: Haskell2010+ hs-source-dirs: src+ main-is: git-vogue.hs+ build-depends: base >=4.7 && <4.8+ , filepath+ , directory+ , git-vogue+ , optparse-applicative >= 0.11+ , split++executable git-vogue-cabal+ default-language: Haskell2010+ hs-source-dirs: src+ main-is: git-vogue-cabal.hs+ build-depends: base >=4.7 && <4.8+ , Cabal+ , optparse-applicative >= 0.11+ , process++executable git-vogue-hlint+ default-language: Haskell2010+ hs-source-dirs: src+ main-is: git-vogue-hlint.hs+ build-depends: base >=4.7 && <4.8+ , directory+ , filepath+ , hlint+ , cpphs+ , bifunctors+ , haskell-src-exts+ , optparse-applicative >= 0.11+ , hscolour+ , process+ if !flag(gpl)+ cpp-options: -DGPL_SCARES_ME++executable git-vogue-stylish+ default-language: Haskell2010+ hs-source-dirs: src+ main-is: git-vogue-stylish.hs+ build-depends: base >=4.7 && <4.8+ , Diff+ , directory+ , filepath+ , optparse-applicative >= 0.11+ , process+ , strict+ , stylish-haskell++executable git-vogue-ghc-mod+ default-language: Haskell2010+ hs-source-dirs: src+ main-is: git-vogue-ghc-mod.hs+ build-depends: base >=4.7 && <4.8+ , Diff+ , directory+ , filepath+ , optparse-applicative >= 0.11+ , process+ , strict+ , ghc-mod++test-suite test-git-setup+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ hs-source-dirs: tests+ main-is: git-setup.hs+ build-depends: base >=4.7 && <4.8+ , directory+ , filepath+ , git-vogue+ , hspec+ , process+ , transformers+ , unix++test-suite test-plugins+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ hs-source-dirs: tests+ main-is: plugins.hs+ build-depends: base >=4.7 && <4.8+ , filepath+ , git-vogue+ , hspec+ , process+ , transformers+ , transformers-base+ , unix++-- vim: set tabstop=21 expandtab:
+ lib/Git/Vogue.hs view
@@ -0,0 +1,192 @@+--+-- 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.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)
+ lib/Git/Vogue/Plugins.hs view
@@ -0,0 +1,111 @@+--+-- 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 OverloadedStrings #-}++module Git.Vogue.Plugins where++import Git.Vogue.Types++import Control.Applicative+import Control.Monad+import Control.Monad.IO.Class+import Data.Foldable+import Data.Monoid+import Data.String+import Data.String.Utils+import Data.Text.Lazy (Text)+import qualified Data.Text.Lazy as T+import qualified Data.Text.Lazy.IO as T+import Formatting+import Prelude hiding (maximum)+import System.Directory+import System.Exit+import System.Process++-- | Execute a plugin in IO+ioPluginExecutorImpl :: MonadIO m => PluginExecutorImpl m+ioPluginExecutorImpl =+ PluginExecutorImpl (f "fix") (f "check")+ where+ -- | Given the command sub-type, and the path to the plugin, execute it+ -- appropriately.+ --+ -- This involves the interface described in README under "Plugin design".+ f :: MonadIO m => String -> SearchMode -> Plugin -> m (Status a)+ f arg sm (Plugin path) = liftIO $ do+ name <- getName path+ fs <- unlines <$> (lines <$> paths sm >>= filterM doesFileExist)+ (status, out, err) <- readProcessWithExitCode path [arg] fs+ let glommed = fromString $ out <> err++ return $ case status of+ ExitSuccess -> Success name glommed+ ExitFailure 1 -> Failure name glommed+ ExitFailure n -> Catastrophe n name glommed++ paths FindChanged = git ["diff", "--cached", "--name-only"]+ paths FindAll = git ["ls-files"]++ git args = readProcess "git" args ""++ getName path = do+ (status, name, _) <- readProcessWithExitCode path ["name"] mempty+ return . PluginName . fromString . strip $ case status of+ ExitSuccess -> if null name then path else name+ ExitFailure _ -> path++colorize :: Status a -> Text+colorize (Success (PluginName x) y) =+ format ("\x1b[32m" % text % " succeeded\x1b[0m with:\n" % text) x y+colorize (Failure (PluginName x) y) =+ format ("\x1b[33m" % text % " failed\x1b[0m with:\n" % text) x y+colorize (Catastrophe n (PluginName x) y) =+ format ("\x1b[31m"+ % text+ % " exploded \x1b[0m with exit code "+ % int+ %":\n"+ % text) x n y++-- | Output the result of a Plugin and exit with an appropriate return code+outputStatusAndExit+ :: MonadIO m+ => Status a+ -> m ()+outputStatusAndExit status = liftIO $+ case status of+ Success _ output -> do+ T.putStrLn output+ exitSuccess+ Failure _ output -> do+ T.putStrLn output+ exitWith $ ExitFailure 1+ Catastrophe _ _ output -> do+ T.putStrLn output+ exitWith $ ExitFailure 2++-- | Run a bunch of plugin actions, mush the statuses together and stick them+-- all under the header of the worst.+getWorst+ :: Monad m+ => (Plugin -> m (Status a))+ -> [Plugin]+ -> m (Status a)+getWorst f ps = do+ rs <- mapM f ps+ return $ insertMax rs (T.unlines $ fmap colorize rs)++insertMax :: [Status a] -> Text -> Status a+insertMax [] _ = Success mempty "No plugins to run, vacuous success."+insertMax rs txt =+ case maximum rs of+ Success{} -> Success mempty txt+ Failure{} -> Failure mempty txt+ Catastrophe{} -> Catastrophe 0 mempty txt
+ lib/Git/Vogue/Types.hs view
@@ -0,0 +1,50 @@+--+-- 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 EmptyDataDecls #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++module Git.Vogue.Types where++import Data.Monoid+import Data.String+import Data.Text.Lazy (Text)++-- | Phantom type for Statuses related to checking+data Check+-- | Phantom type for Statuses related to fixing+data Fix++-- | Result of running a Plugin+data Status a+ = Success PluginName Text+ | Failure PluginName Text+ | Catastrophe Int PluginName Text+ deriving (Show, Ord, Eq)++-- | Absolute path to an executable+newtype Plugin = Plugin {+ unPlugin :: FilePath+} deriving (Show, Ord, Eq, IsString)++-- | Nice, human readable name of a plugin+newtype PluginName = PluginName {+ unPluginName :: Text+} deriving (Show, Ord, Eq, IsString, Monoid)++-- | We want the flexibility of just checking changed files, or maybe checking+-- all of them.+data SearchMode = FindAll | FindChanged+ deriving (Eq, Show)++-- | An implementation of a "runner" of plugins. Mostly for easy testing.+data PluginExecutorImpl m = PluginExecutorImpl{+ executeFix :: SearchMode -> Plugin -> m (Status Fix),+ executeCheck :: SearchMode -> Plugin -> m (Status Check)+}
+ src/git-vogue-cabal.hs view
@@ -0,0 +1,72 @@+--+-- 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.+--++-- | Description: Check with "cabal check".+module Main where++import Common+import Control.Monad (unless, when)+import Data.Monoid+import Distribution.PackageDescription.Check+import Distribution.PackageDescription.Configuration (flattenPackageDescription)+import Distribution.PackageDescription.Parse (readPackageDescription)+import Distribution.Simple.Utils (defaultPackageDesc,+ toUTF8,+ wrapText)+import Distribution.Verbosity (Verbosity,+ silent)+import System.Exit++main :: IO ()+main = f =<< getPluginCommand+ "Check your Haskell project for cabal-related problems."+ "git-vogue-cabal - check for cabal problems"+ where+ f CmdName = putStrLn "cabal"+ f CmdCheck = do+ ok <- check silent+ unless ok exitFailure+ f CmdFix = exitFailure++-- | Runs the same thing as cabal check.+-- See also "Distribution.Client.Check" in cabal-install.+check :: Verbosity -> IO Bool+check verbosity = do+ pdfile <- defaultPackageDesc verbosity+ ppd <- readPackageDescription verbosity pdfile+ let pkg_desc = flattenPackageDescription ppd+ ioChecks <- checkPackageFiles pkg_desc "."+ let packageChecks = ioChecks <> checkPackage ppd (Just pkg_desc)+ buildImpossible = [ x | x@PackageBuildImpossible {} <- packageChecks ]+ buildWarning = [ x | x@PackageBuildWarning {} <- packageChecks ]+ distSuspicious = [ x | x@PackageDistSuspicious {} <- packageChecks ]+ distInexusable = [ x | x@PackageDistInexcusable {} <- packageChecks ]+ unless (null buildImpossible) $ do+ putStrLn "The package will not build sanely due to these errors:"+ printCheckMessages buildImpossible+ unless (null buildWarning) $ do+ putStrLn "The following warnings are likely affect your build negatively:"+ printCheckMessages buildWarning+ unless (null distSuspicious) $ do+ putStrLn "These warnings may cause trouble when distributing the package:"+ printCheckMessages distSuspicious+ unless (null distInexusable) $ do+ putStrLn "The following errors will cause portability problems on other environments:"+ printCheckMessages distInexusable+ let isDistError (PackageDistSuspicious {}) = False+ isDistError _ = True+ errors = filter isDistError packageChecks+ unless (null errors) $+ putStrLn "Hackage would reject this package."+ when (null packageChecks) $+ putStrLn "Checked cabal file"+ return (null packageChecks)+ where+ printCheckMessages = mapM_ (putStrLn . format . explanation)+ format = toUTF8 . wrapText . ("* "++)
+ src/git-vogue-ghc-mod.hs view
@@ -0,0 +1,93 @@+--+-- 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.+--++-- | Description: Check with "cabal check".+module Main where++import Common+import Control.Applicative+import Data.Char+import Data.Foldable+import Data.List hiding (elem)+import Data.Maybe+import Data.Monoid+import Data.Traversable+import Language.Haskell.GhcMod+import Prelude hiding (elem)+import System.Exit++main :: IO ()+main =+ f =<< getPluginCommand+ "Check your Haskell project for ghc-mod problems."+ "git-vogue-ghc-mod - check for ghc-mod problems"+ where+ f CmdName = putStrLn "ghc-mod check"+ f CmdCheck = ghcModCheck+ f CmdFix = do+ putStrLn "you need to fix ghc-mod check failures"+ exitFailure++-- | Try to help the user out with some munging of error messages+explain :: String -> String+explain s+ -- A terrible heuristic, but it may help some people+ | "test" `isInfixOf` fmap toLower s && "hidden package" `isInfixOf` s =+ s <> "\n\tSuggestion: cabal configure --enable-tests\n"+ | "bench" `isInfixOf` fmap toLower s && "hidden package" `isInfixOf` s =+ s <> "\n\tSuggestion: cabal configure --enable-benchmarks\n"+ | otherwise = s++-- | ghc-mod check all of the .hs files from stdin+ghcModCheck :: IO ()+ghcModCheck = do+ -- HLint.hs is weird and more of a config file than a source file, so+ -- ghc-mod doesn't like it.+ --+ -- Setup.hs can import cabal things magically, without requiring it to be+ -- mentioned in cabal (which ghc-mod hates)+ --+ -- This is ugly, and should be replaced in favor of a git-vogue ignoring+ -- mechanism.+ files <- filter (not . (`elem` ["HLint.hs", "Setup.hs"])) <$> hsFiles++ -- We can't actually check all at once, or ghc-mod gets confused, so we+ -- traverse+ (r,_) <- runGhcModT defaultOptions (traverse (check . return) files)++ -- Seriously guys? Eithers within eithers?+ warn_errs <- case r of+ -- This is some kind of outer-error, we don't fail on it.+ Left e -> do+ print e+ return []+ -- And these are the warnings and errors.+ Right rs ->+ return rs++ -- Traverse the errors, picking the warnings out. We don't fail on errors+ -- but do warn about them.+ maybe_ws <- for warn_errs $ \warn_err ->+ case warn_err of+ -- Errors in files+ Left e -> do+ putStrLn (explain e)+ return Nothing+ -- Warnings, sometimes empty strings+ Right warn ->+ return $ if null warn then Nothing else Just warn++ let warns = catMaybes maybe_ws+ if null warns+ then do+ putStrLn $ "Checked " <> show (length files) <> " file(s)"+ exitSuccess+ else do+ traverse_ putStrLn warns+ exitFailure
+ src/git-vogue-hlint.hs view
@@ -0,0 +1,117 @@+--+-- 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 CPP #-}+{-# LANGUAGE RecordWildCards #-}++-- | Description: Check with "cabal check".+module Main where++import Common+import Control.Applicative+import Data.Bifunctor+import Data.List+import Data.Monoid+import Data.Traversable+import Language.Haskell.Exts.SrcLoc+import Language.Haskell.HLint3+import Language.Preprocessor.Cpphs+import System.Directory+import System.Exit++#ifndef GPL_SCARES_ME+import Language.Haskell.HsColour.Colourise+import Language.Haskell.HsColour.TTY+#endif++main :: IO ()+main =+ f =<< getPluginCommand+ "Check your Haskell project for hlint-related problems."+ "git-vogue-hlint - check for hlint problems"+ where+ f CmdName = putStrLn "hlint"+ f CmdCheck = lint+ f CmdFix = putStrLn "you need to fix hlint failures" >> exitFailure++-- | Lint all of the .hs files from stdin+lint :: IO ()+lint = do+ files <- hsFiles+ (flags, classify, hint) <- autoSettings'++ -- Cpphs is off by default+ let flags' = flags { cppFlags = Cpphs defaultCpphsOptions }++ -- Traverse the files, parsing and processing as we go for efficiency+ parsed <- for files $ \f ->+ process classify hint <$> parseModuleEx flags' f Nothing+++ let ideas = concat [ x | Right x <- parsed]+ let errors = [ x | Left x <- parsed ]+ let out = unlines errors <> "\n" <> ideas++ if null ideas && null errors+ then do+ putStrLn ("Checked " <> show (length files) <> " file(s)")+ exitSuccess+ else putStrLn out >> exitFailure+ where+ process classify hint =+ bimap f g+ where+ f x = parseErrorMessage x <> show (parseErrorLocation x)+ g x = showANSI $ applyHints classify hint [x]++-- | The default autoSettings form HLint3 does not handle custom HLint.hs files+-- in the current directory. So we define our own.+autoSettings' :: IO (ParseFlags, [Classify], Hint)+autoSettings' = do+ local_hlint <- doesFileExist "HLint.hs"+ let start_at = if local_hlint then Just "HLint" else Nothing+ (fixities, classify, hints) <- findSettings (readSettingsFile Nothing)+ start_at+ return (parseFlagsAddFixities fixities defaultParseFlags, classify, resolveHints hints)++#ifdef GPL_SCARES_ME+format :: String -> String+format s = "\x1b[36m" <> s <> "\x1b[0m"+#else+format :: String -> String+format = hscolour defaultColourPrefs+#endif++-- All of the code below is more or less salvaged from hlint internals.++-- | Pretty print and Idea with colouring+showANSI :: [Idea] -> String+showANSI =+ (>>= \i -> showEx format i <> "\n")++-- | Show an idea with a function that highlights.+showEx :: (String -> String) -> Idea -> String+showEx tt Idea{..} = unlines $+ ["\x1b[33m" <> showSrcLoc (getPointLoc ideaSpan) <> "\x1b[0m " <> (if ideaHint == "" then "" else show ideaSeverity <> ": " <> ideaHint)] <>+ f "Found" (Just ideaFrom) <> f "Why not" ideaTo <>+ ["Note: " <> n | let n = showNotes ideaNote, n /= ""]+ where+ f _ Nothing = []+ f msg (Just x) | null xs = [msg <> " remove it."]+ | otherwise = (msg <> ":") : fmap (" "<>) xs+ where xs = lines $ tt x++showSrcLoc :: SrcLoc -> String+showSrcLoc (SrcLoc file line col) = file <> ":" <> show line <> ":" <> show col <> ":"++showNotes :: [Note] -> String+showNotes = intercalate ", " . fmap show . filter use+ where use ValidInstance{} = False -- Not important enough to tell an end user+ use _ = True+
+ src/git-vogue-stylish.hs view
@@ -0,0 +1,140 @@+--+-- 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.+--++-- | Description: Check and fix style differences with stylish-haskell+module Main where++import Common+import Control.Monad+import Data.Algorithm.Diff+import Data.Algorithm.DiffOutput+import Data.Foldable+import Data.List hiding (and)+import Data.Monoid hiding (First)+import Data.Traversable+import Language.Haskell.Stylish+import Prelude hiding (and)+import System.Exit+import System.IO hiding (hGetContents)+import System.IO.Strict (hGetContents)++main :: IO ()+main = do+ cmd <- getPluginCommand+ "Check your Haskell project for stylish-haskell-related problems."+ "git-vogue-stylish - check for stylish-haskell problems"+ cfg <- getConfig+ files <- hsFiles+ f files cfg cmd+ where+ f _ _ CmdName = putStrLn "stylish"+ f files cfg CmdCheck = do+ rs <- traverse (stylishCheckFile cfg) files+ if and rs+ then do+ putStrLn $ "Checked " <> show (length rs) <> " file(s)"+ exitSuccess+ else+ exitFailure++ f files cfg CmdFix = do+ -- Fix all of the things first+ traverse_ (stylishRunFile cfg) files+ -- Now double check they are fixed+ rs <- traverse (stylishCheckFile cfg) files+ if and rs+ then+ putStrLn "Style converged"+ else do+ putStrLn "Style did not converge, bailing"+ exitFailure++-- | Try various configuration locations as per stylish-haskell binary+getConfig+ :: IO Config+getConfig =+ -- Don't spew every file checked to stdout+ let v = makeVerbose False+ in configFilePath v Nothing >>= loadConfig v++-- | Checks whether running Stylish over a given file produces any differences.+-- Returns TRUE if there's nothing left to change.+-- Prints results and returns FALSE if there are things left to change.+stylishCheckFile+ :: Config -- ^ Stylish Haskell config+ -> FilePath -- ^ File to run through Stylish+ -> IO Bool+stylishCheckFile cfg fp = stylishFile fp cfg (\original stylish ->+ case getStyleDiffs original stylish of+ [] -> return True+ x -> do+ putStrLn $ "\x1b[33m" <> fp <> "\x1b[0m"+ <> " has differing style:\n" <> ppDiff x+ return False+ )++-- | Runs Stylish over a given file. If there are changes, write them.+stylishRunFile+ :: Config -- ^ Stylish Haskell config+ -> FilePath -- ^ File to run through Stylish+ -> IO ()+stylishRunFile cfg fp = stylishFile fp cfg $ \original stylish ->+ unless (null $ getStyleDiffs original stylish) (writeFile fp stylish)++-- | Get diffs and filter out equivalent ones+getStyleDiffs+ :: String+ -> String+ -> [Diff [String]]+getStyleDiffs original stylish = filter filterDiff $+ getGroupedDiff (lines original) (lines stylish)++-- | Filters out equal diffs+filterDiff+ :: (Eq a)+ => Diff a+ -> Bool+filterDiff (Both a b) = a /= b+filterDiff _ = True++-- | Takes an original file, the Stylish version of the file, and does something+-- depending on the outcome of the Stylish transformation.+stylishFile+ :: FilePath -- ^ File to run through Stylish+ -> Config -- ^ Stylish Haskell config+ -> (String -> String -> IO a)+ -> IO a+stylishFile fp cfg fn = do+ original <- readUTF8File fp+ stylish <- stylishFile' (Just fp) cfg+ fn original stylish++-- | Processes a single file, or stdin if no filepath is given+stylishFile'+ :: Maybe FilePath -- ^ File to run through Stylish+ -> Config -- ^ Stylish Haskell config+ -> IO String+stylishFile' mfp conf = do+ contents <- maybe getContents readUTF8File mfp+ let result = runSteps (configLanguageExtensions conf)+ mfp (configSteps conf) $ lines contents+ case result of+ Left err -> do+ hPutStrLn stderr err+ return contents+ Right ok -> return $ unlines ok++-- | Loads a UTF8 file.+readUTF8File+ :: FilePath -- ^ Filepath to read+ -> IO String -- ^ File data+readUTF8File fp =+ withFile fp ReadMode $ \h -> do+ hSetEncoding h utf8+ hGetContents h
+ src/git-vogue.hs view
@@ -0,0 +1,101 @@+--+-- 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 OverloadedStrings #-}++module Main where++import Control.Monad+import Data.List.Split+import Data.Maybe+import Data.String+import Data.Traversable+import Options.Applicative+import Options.Applicative.Types+import System.Directory+import System.Environment+import System.FilePath++import Git.Vogue+import Git.Vogue.Types++import Common+import qualified Paths_git_vogue as Paths++-- | Parse command-line options.+optionsParser :: Parser VogueOptions+optionsParser = Options+ <$> flag FindChanged FindAll+ ( long "all"+ <> short 'A'+ <> help "Apply to all files, not just changed files."+ )+ <*> commandParser++commandParser :: Parser VogueCommand+commandParser = subparser+ ( command "init" (info pInit+ (progDesc "Initialise git-vogue support in a git repo"))+ <> pCommand "verify"+ CmdVerify+ "Check git-vogue support is all legit"+ <> pCommand "plugins"+ CmdPlugins+ "List installed plugins."+ <> pCommand "check"+ CmdRunCheck+ "Run check plugins on files in a git repo"+ <> pCommand "fix"+ CmdRunFix+ "Run fix plugins on files a git repo"+ )+ where+ pInit = CmdInit <$> option (Just <$> readerAsk)+ ( long "template"+ <> value Nothing+ )++-- | Discover all available plugins.+--+-- This function inspects the $PREFIX/libexec/git-vogue directory and the+-- directories listed in the $GIT_VOGUE_PATH environmental variable (if+-- defined) and builds a 'Plugin' for the executables found.+discoverPlugins :: IO [Plugin]+discoverPlugins = do+ -- Use the environmental variable and $libexec/git-vogue/ directories as+ -- the search path.+ path <- fromMaybe "" <$> lookupEnv "GIT_VOGUE_PATH"+ libexec <- (</> "git-vogue") <$> Paths.getLibexecDir+ let directories = splitOn ":" path <> [libexec]++ -- Find all executables in the directories in path.+ (fmap . fmap) fromString+ (traverse ls directories >>= filterM isExecutable . concat)+ where+ ls :: FilePath -> IO [FilePath]+ ls p = do+ exists <- doesDirectoryExist p+ if exists+ then fmap (p </>) <$> getDirectoryContents p+ else return []++ isExecutable :: FilePath -> IO Bool+ isExecutable = fmap executable . getPermissions++-- | Parse the command line and run the command.+main :: IO ()+main = do+ opt <- execParser opts+ plugins <- discoverPlugins+ runVogue plugins (runCommand (optCommand opt) (optSearch opt))+ where+ opts = info (helper <*> optionsParser)+ ( fullDesc+ <> progDesc "Make your Haskell git repository fashionable"+ <> header "git-vogue - git integration for fashionable Haskell" )
+ templates/pre-commit view
@@ -0,0 +1,3 @@+#!/bin/sh++git-vogue check
+ tests/git-setup.hs view
@@ -0,0 +1,106 @@+--+-- 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 MultiParamTypeClasses #-}+-- | Description: Test git repository setup.+module Main where++import Control.Exception+import Control.Monad+import Data.List+import Data.Monoid+import System.Directory+import System.Exit+import System.FilePath+import System.Posix.Files+import System.Posix.Temp+import System.Process+import Test.Hspec++import Git.Vogue++main :: IO ()+main = hspec . describe "Git repository setup" $ do+ it "should install a new pre-commit hook" . withGitRepo $ \path -> do+ let hook = path </> ".git" </> "hooks" </> "pre-commit"++ shouldInstallWorking path hook++ it "should skip a correct pre-commit hook" . withGitRepo $ \path -> do+ let hook = path </> ".git" </> "hooks" </> "pre-commit"++ -- Create an existing hook to update.+ copyHookTemplateTo (Just "templates/pre-commit") hook++ shouldInstallWorking path hook++ it "should report a conflict pre-commit hook" . withGitRepo $ \path -> do+ let hook = path </> ".git" </> "hooks" </> "pre-commit"++ -- Create an existing hook to update.+ writeFile hook "echo YAY\n"+ setPermissions hook $ emptyPermissions+ { readable = True+ , executable = True+ }++ -- Run the setup program.+ code <- runInRepo path+ code `shouldBe` ExitFailure 1++-- | Run "git-vogue init" and check we wind up with a working pre-commit hook.+shouldInstallWorking :: FilePath -> FilePath -> IO ()+shouldInstallWorking path hook = do+ code <- runInRepo path+ code `shouldBe` ExitSuccess+ checkPreCommitHook hook++-- | Execute the setup command in a git repository.+runInRepo+ :: FilePath+ -> IO ExitCode+runInRepo path = do+ pwd <- getCurrentDirectory+ let exe = pwd </> "dist/build/git-vogue/git-vogue"+ let tpl = pwd </> "templates/pre-commit"+ ps <- spawnCommand $+ "cd " <> path <> " && " <> exe <> " init --template=" <> tpl+ waitForProcess ps++-- | Check that a pre-commit hook script is "correct".+checkPreCommitHook+ :: FilePath+ -> IO ()+checkPreCommitHook hook = do+ -- Check the hook exists.+ exists <- fileExist hook+ unless exists $ error "Commit hook missing"++ -- Check the hook is executable.+ perm <- getPermissions hook+ unless (executable perm) $ error "Commit hook is not executable"++ -- Check it has our command in it.+ content <- readFile hook+ unless ("git-vogue check" `isInfixOf` content) $+ error "Commit hook does not contain command"++-- | Create a git repository and run an action with it.+withGitRepo+ :: (FilePath -> IO ())+ -> IO ()+withGitRepo = bracket createRepo deleteRepo+ where+ createRepo = do+ path <- mkdtemp "/tmp/git-setup-test."+ callProcess "git" ["init", path]+ return path+ deleteRepo path =+ callProcess "rm" ["-rf", path]
+ tests/plugins.hs view
@@ -0,0 +1,88 @@+--+-- 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 OverloadedStrings #-}++-- | Description: Test git repository setup.+module Main where++import Control.Monad.IO.Class ()+import Data.Monoid+import System.FilePath+import Test.Hspec++import Git.Vogue.Plugins+import Git.Vogue.Types++failingExecutor, succeedingExecutor :: b -> IO (Status a)+failingExecutor = const . return $ Failure mempty mempty+succeedingExecutor = const . return $ Success mempty mempty++main :: IO ()+main = hspec $ do+ describe "module collation" $ do+ it "maximum of statuses is failure" $+ maximum [Success mempty mempty, Failure mempty mempty]+ `shouldBe` Failure mempty mempty++ it "fails if one module fails" $ do+ getWorst failingExecutor ["a", "b"]+ >>= (`shouldBe` Failure mempty "\ESC[33m failed\ESC[0m with:\n\n\ESC[33m failed\ESC[0m with:\n\n")+ getWorst failingExecutor ["monkey"]+ >>= (`shouldBe` Failure mempty "\ESC[33m failed\ESC[0m with:\n\n")++ it "succeeds if all modules succeed" $ do+ getWorst succeedingExecutor ["a", "b"]+ >>= (`shouldBe` Success mempty "\ESC[32m succeeded\ESC[0m with:\n\n\ESC[32m succeeded\ESC[0m with:\n\n")+ getWorst succeedingExecutor ["monkey"]+ >>= (`shouldBe` Success mempty "\ESC[32m succeeded\ESC[0m with:\n\n")++ describe "IO module executor" $ do+ describe "check" $ do+ it "fails on failing module" $+ runCheckExecutor FindAll "failing"+ (Failure "failing" "ohnoes\n")++ it "fails on succeeding module" $+ runCheckExecutor FindAll "succeeding"+ (Success "succeeding" "yay\n")++ it "fails on exploding module" $+ runCheckExecutor FindAll "exploding"+ (Catastrophe 3 "exploding" "something broke\n")++ describe "fix" $ do+ it "fails on failing module" $+ runFixExecutor FindAll "failing"+ (Failure "failing" "ohnoes\n")++ it "fails on succeeding module" $+ runFixExecutor FindAll "succeeding"+ (Success "succeeding" "yay\n")++ it "fails on exploding module" $+ runFixExecutor FindAll "exploding"+ (Catastrophe 3 "exploding" "something broke\n")++runCheckExecutor :: SearchMode -> FilePath -> Status Check -> Expectation+runCheckExecutor = runTestExecutor executeCheck++runFixExecutor :: SearchMode -> FilePath -> Status Fix -> Expectation+runFixExecutor = runTestExecutor executeFix++runTestExecutor+ :: (PluginExecutorImpl IO -> SearchMode -> Plugin -> IO (Status a))+ -> SearchMode+ -> FilePath+ -> Status a+ -> Expectation+runTestExecutor act search_mode file expected =+ act ioPluginExecutorImpl search_mode (Plugin ("fixtures" </> file))+ >>= (`shouldBe` expected)