packages feed

cmt (empty) → 0.2.0.0

raw patch · 14 files changed

+604/−0 lines, 14 filesdep +attoparsecdep +basedep +classy-preludesetup-changed

Dependencies added: attoparsec, base, classy-prelude, cmt, directory, file-embed, filepath, process, tasty, tasty-discover, tasty-expected-failure, tasty-hunit, text

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Small Hadron Collider / Mark Wales (c) 2019++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 Small Hadron Collider / Mark Wales 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,111 @@+# cmt++Write consistent git commit messages++## Install++[Binaries for Mac and Linux are available](https://github.com/smallhadroncollider/cmt/releases). Add the binary to a directory in your path (such as `/usr/local/bin`).++### Cabal++**Requirements**: [Cabal](https://www.haskell.org/cabal/)++```bash+cabal install cmt+```++Make sure you run `cabal update` if you haven't run it recently.++### Building++**Requirements**: [Stack](https://docs.haskellstack.org/en/stable/README/)++The following command will build cmt and then install it in `~/.local/bin`:++```bash+stack build && stack install+```+++## Usage++Add a `.cmt` file to your project directory.++```bash+cmt # will show the options and then commit+```++If you're using the `${*}` format option then:++```bash+cmt "blah blah blah" # this will go in ${*} place+```++### Format++A `.cmt` file consist of two parts: the input parts and the output format.++For example, the [AngularJS Commit Message Guidelines](https://gist.github.com/stephenparish/9941e89d80e2bc58a153):++```txt+# The input parts+{+    # Shows a list of options+    "Type" = [+        "feat",+        "fix",+        "docs",+        "style",+        "refactor",+        "test",+        "chore"+    ]+    "Scope" = @ # Allows a single line of input+    "Subject" = @+    "Body" = !@ # Allows multi-line input+    "Footer" = !@+}++# The output format+# Takes the values provided from the input stage+# and interpolates them in+${Type} (${Scope}): ${Subject}++${Body}++${Footer}+```+++#### Input Parts++These are at the top of the `.cmt` file and surrounded by opening and closing curly braces. A consist of a name and a type:++- `@`: single line input+- `!@`: multi line input+- `["option 1", "option 2"]`: list of options++#### Output Format++The output format consists of named input parts plus anything else you want.++You can accept a output called `${*}`, which will add in whatever is passed to `cmt` as command line arguments.++For example:++```txt+# Input parts+# * input not needed, as comes from command-line+{+    "Scope" = @+}++# Scope from input and * from command-line+(${Scope}): ${*}+```++Then use with:++```bash+cmt "Blah blah blah"+```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,10 @@+{-# LANGUAGE NoImplicitPrelude #-}++module Main where++import ClassyPrelude++import Cmt (go)++main :: IO ()+main = go
+ cmt.cabal view
@@ -0,0 +1,77 @@+cabal-version: 1.12+name: cmt+version: 0.2.0.0+license: BSD3+license-file: LICENSE+copyright: Small Hadron Collider / Mark Wales+maintainer: mark@smallhadroncollider.com+author: Small Hadron Collider / Mark Wales+homepage: https://github.com/smallhadroncollider/cmt#readme+bug-reports: https://github.com/smallhadroncollider/cmt/issues+synopsis: Write consistent git commit messages+description:+    Please see the README on GitHub at <https://github.com/smallhadroncollider/cmt#readme>+category: Command Line Tools+build-type: Simple+extra-source-files:+    README.md++source-repository head+    type: git+    location: https://github.com/smallhadroncollider/cmt++library+    exposed-modules:+        Cmt+        Cmt.IO.Config+        Cmt.IO.Git+        Cmt.IO.Input+        Cmt.Output.Format+        Cmt.Parser.Config+        Cmt.Types.Config+    hs-source-dirs: src+    other-modules:+        Paths_cmt+    default-language: Haskell2010+    default-extensions: OverloadedStrings NoImplicitPrelude+    build-depends:+        attoparsec >=0.13.2.2 && <0.14,+        base >=4.7 && <5,+        classy-prelude >=1.5.0 && <1.6,+        directory >=1.3.3.0 && <1.4,+        filepath >=1.4.2.1 && <1.5,+        process >=1.6.3.0 && <1.7,+        text >=1.2.3.1 && <1.3++executable cmt+    main-is: Main.hs+    hs-source-dirs: app+    other-modules:+        Paths_cmt+    default-language: Haskell2010+    default-extensions: OverloadedStrings NoImplicitPrelude+    ghc-options: -threaded -rtsopts -with-rtsopts=-N+    build-depends:+        base >=4.7 && <5,+        classy-prelude >=1.5.0 && <1.6,+        cmt -any++test-suite cmt-test+    type: exitcode-stdio-1.0+    main-is: Spec.hs+    hs-source-dirs: test+    other-modules:+        Cmt.Parser.ConfigTest+        Paths_cmt+    default-language: Haskell2010+    default-extensions: OverloadedStrings NoImplicitPrelude+    ghc-options: -threaded -rtsopts -with-rtsopts=-N+    build-depends:+        base >=4.7 && <5,+        classy-prelude >=1.5.0 && <1.6,+        cmt -any,+        file-embed >=0.0.11 && <0.1,+        tasty ==1.2.*,+        tasty-discover >=4.2.1 && <4.3,+        tasty-expected-failure >=0.11.1.1 && <0.12,+        tasty-hunit >=0.10.0.1 && <0.11
+ src/Cmt.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}++module Cmt+    ( go+    ) where++import ClassyPrelude++import Cmt.IO.Config     (load)+import Cmt.IO.Git        (commit)+import Cmt.IO.Input      (loop)+import Cmt.Output.Format (format)+import Cmt.Types.Config  (Config, Output)++display :: Either Text (Config, [Output]) -> IO ()+display (Left err) = putStrLn err+display (Right (cfg, output)) = do+    parts <- loop cfg+    let txt = format cfg (output ++ parts)+    commit txt >>= putStrLn++go :: IO ()+go = load >>= display
+ src/Cmt/IO/Config.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}++module Cmt.IO.Config where++import ClassyPrelude++import Data.List        (nub)+import System.Directory (doesFileExist, getCurrentDirectory, getHomeDirectory)+import System.FilePath  (takeDirectory, (</>))++import Cmt.Parser.Config (config)+import Cmt.Types.Config++configFile :: FilePath+configFile = ".cmt"++checkFormat :: [Output] -> Config -> Either Text (Config, [Output])+checkFormat output (Config parts format) = do+    let partNames = nub $ partName <$> parts+    let formatNames = nub . catMaybes $ formatName <$> format+    let result+            | isJust (find (== "*") formatNames) && null output = Left "Command line entry missing"+            | (length partNames + length output) /= length formatNames =+                Left "Parts and format do not match"+            | otherwise = Right (Config parts format, output)+    result++parse :: [Output] -> Text -> Either Text (Config, [Output])+parse output cfg = config cfg >>= checkFormat output++read :: FilePath -> IO Text+read path = decodeUtf8 <$> readFile path++parseArgs :: [Text] -> [Output]+parseArgs []    = []+parseArgs [msg] = [("*", msg)]+parseArgs parts = [("*", unwords parts)]++checkParent :: FilePath -> IO (Maybe FilePath)+checkParent path = do+    let parent = takeDirectory path+    if parent /= path+        then checkDir parent+        else pure Nothing++checkDir :: FilePath -> IO (Maybe FilePath)+checkDir path = do+    let fp = path </> configFile+    exists <- doesFileExist fp+    if exists+        then pure $ Just fp+        else checkParent path++homeDir :: IO (Maybe FilePath)+homeDir = do+    home <- getHomeDirectory+    let fp = home </> configFile+    exists <- doesFileExist fp+    pure $+        if exists+            then Just fp+            else Nothing++findFile :: IO (Maybe FilePath)+findFile = do+    inDir <- checkDir =<< getCurrentDirectory+    case inDir of+        Just fp -> pure $ Just fp+        Nothing -> homeDir++load :: IO (Either Text (Config, [Output]))+load = do+    exists <- findFile+    case exists of+        Just path -> do+            output <- parseArgs <$> getArgs+            parse output <$> read path+        Nothing -> pure $ Left ".cmt file not found"
+ src/Cmt/IO/Git.hs view
@@ -0,0 +1,16 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}++module Cmt.IO.Git+    ( commit+    ) where++import ClassyPrelude++import System.Process (readCreateProcessWithExitCode, shell)++commit :: Text -> IO Text+commit message = do+    let msg = "git commit -m '" <> unpack message <> "'"+    (_, out, _) <- readCreateProcessWithExitCode (shell msg) ""+    pure $ pack out
+ src/Cmt/IO/Input.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}++module Cmt.IO.Input+    ( loop+    ) where++import ClassyPrelude++import Cmt.Types.Config++listItem :: (Int, Text) -> IO ()+listItem (n, o) = putStrLn $ tshow n <> ") " <> o++multiLine :: [Text] -> IO [Text]+multiLine input = do+    value <- getLine+    if null value+        then pure input+        else multiLine $ input ++ [value]++output :: Part -> IO (Name, Text)+output (Part name Line) = do+    putStrLn $ name <> ":"+    val <- getLine+    if null val+        then output (Part name Line)+        else pure (name, val)+output (Part name (Options opts)) = do+    let opts' = zip [1 ..] opts+    putStrLn $ name <> ":"+    sequence_ $ listItem <$> opts'+    chosen <- getLine+    case find ((== chosen) . tshow . fst) opts' of+        Nothing     -> output (Part name (Options opts))+        Just (_, o) -> pure (name, o)+output (Part name Lines) = do+    putStrLn $ name <> ":"+    val <- unlines <$> multiLine []+    pure (name, val)++loop :: Config -> IO [(Name, Text)]+loop (Config parts _) = sequence $ output <$> parts
+ src/Cmt/Output/Format.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}++module Cmt.Output.Format+    ( format+    ) where++import ClassyPrelude++import Cmt.Types.Config++tidy :: [Output] -> FormatPart -> Text+tidy _ (Literal c) = c+tidy parts (Named name) =+    case find ((== name) . fst) parts of+        Just (_, t) -> t+        Nothing     -> ""++format :: Config -> [Output] -> Text+format (Config _ fmt) parts = concat $ tidy parts <$> fmt
+ src/Cmt/Parser/Config.hs view
@@ -0,0 +1,86 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}++module Cmt.Parser.Config+    ( config+    ) where++import ClassyPrelude++import Data.Attoparsec.Text++import Cmt.Types.Config++-- useful bits+lexeme :: Parser a -> Parser a+lexeme p = skipSpace *> p <* skipSpace++tchar :: Char -> Parser Text+tchar ch = singleton <$> char ch++chopt :: Char -> Parser Text+chopt ch = option "" (tchar ch)++tnotChar :: Char -> Parser Text+tnotChar c = pack <$> many1 (notChar c)++commentP :: Parser ()+commentP = lexeme $ many' (char '#' *> many' (notChar '\n') *> char '\n') $> ()++stripComments :: Parser a -> Parser a+stripComments p = lexeme $ commentP *> p <* commentP++word :: Parser Text+word = pack <$> many1 (letter <|> space)++valid :: [Name] -> Parser Text+valid names = choice $ "*" : (string <$> names)++-- format parts+formatNamedP :: [Name] -> Parser FormatPart+formatNamedP names = Named <$> (string "${" *> valid names <* char '}')++formatLiteralP :: Parser FormatPart+formatLiteralP = Literal <$> (singleton <$> anyChar)++formatP :: [Name] -> Parser Format+formatP names = stripComments $ many1 (formatNamedP names <|> formatLiteralP)++-- input parts+lineP :: Parser PartType+lineP = char '@' $> Line++linesP :: Parser PartType+linesP = string "!@" $> Lines++listItemP :: Parser Text+listItemP = stripComments $ char '"' *> tnotChar '"' <* char '"' <* chopt ','++listP :: Parser PartType+listP = Options <$> (char '[' *> many' listItemP <* char ']')++-- name+nameP :: Parser Text+nameP = char '"' *> word <* char '"' <* lexeme (char '=')++-- part+partP :: Parser Part+partP = stripComments $ Part <$> nameP <*> (listP <|> lineP <|> linesP)++partsP :: Parser [Part]+partsP = stripComments $ stripComments (char '{') *> many' partP <* stripComments (char '}')++configP :: Parser Config+configP = do+    parts <- partsP+    format <- formatP $ partName <$> parts+    _ <- endOfInput+    pure $ Config parts format++-- run parser+config :: Text -> Either Text Config+config cfg =+    case parseOnly configP cfg of+        Right c -> Right c+        Left _ ->+            Left "Could not parse config. Check that your format doesn't contain any invalid parts."
+ src/Cmt/Types/Config.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE NoImplicitPrelude #-}++module Cmt.Types.Config where++import ClassyPrelude (Eq, Maybe (..), Show, Text)++type Output = (Name, Text)++type Name = Text++data FormatPart+    = Named Name+    | Literal Text+    deriving (Show, Eq)++type Format = [FormatPart]++data PartType+    = Options [Text]+    | Line+    | Lines+    deriving (Show, Eq)++data Part =+    Part Name+         PartType+    deriving (Show, Eq)++data Config =+    Config [Part]+           Format+    deriving (Show, Eq)++partName :: Part -> Name+partName (Part name _) = name++formatName :: FormatPart -> Maybe Name+formatName (Named name) = Just name+formatName _            = Nothing
+ test/Cmt/Parser/ConfigTest.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE NoImplicitPrelude #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module Cmt.Parser.ConfigTest where++import ClassyPrelude++import Test.Tasty+import Test.Tasty.HUnit++import Data.FileEmbed (embedFile)++import Cmt.Parser.Config (config)+import Cmt.Types.Config++basic :: Text+basic = decodeUtf8 $(embedFile "test/data/.cmt")++basicConfig :: Config+basicConfig =+    Config [Part "Week" Line] [Named "Week", Literal ":", Literal " ", Named "*", Literal "\n"]++angular :: Text+angular = decodeUtf8 $(embedFile "test/data/.cmt-angular")++comments :: Text+comments = decodeUtf8 $(embedFile "test/data/.cmt-comments")++angularConfig :: Config+angularConfig =+    Config+        [ Part "Type" (Options ["feat", "fix", "docs", "style", "refactor", "test", "chore"])+        , Part "Scope" Line+        , Part "Short Message" Line+        , Part "Body" Lines+        ]+        [ Named "Type"+        , Literal " "+        , Literal "("+        , Named "Scope"+        , Literal ")"+        , Literal ":"+        , Literal " "+        , Named "Short Message"+        , Literal "\n"+        , Literal "\n"+        , Named "Body"+        , Literal "\n"+        ]++-- import Test.Tasty.HUnit+test_config :: TestTree+test_config =+    testGroup+        "Cmt.Parser.Config"+        [ testCase+              "basic"+              (assertEqual "Gives back correct format" (Right basicConfig) (config basic))+        , testCase+              "angular"+              (assertEqual "Gives back correct format" (Right angularConfig) (config angular))+        , testCase+              "comments"+              (assertEqual "Gives back correct format" (Right angularConfig) (config comments))+        ]
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF tasty-discover #-}