packages feed

cmt 0.4.0.0 → 0.5.0.0

raw patch · 10 files changed

+246/−109 lines, 10 filesdep +containersdep ~processPVP ok

version bump matches the API change (PVP)

Dependencies added: containers

Dependency ranges changed: process

API changes (from Hackage documentation)

+ Cmt.IO.Config: checkFormat :: Outputs -> Config -> Either Text (Config, Outputs)
+ Cmt.Types.Config: type Outputs = Map Name Text
+ Cmt.Types.Config: type PreDefinedParts = Map Text Config
- Cmt.IO.Config: readCfg :: [Output] -> IO (Either Text (Config, [Output]))
+ Cmt.IO.Config: readCfg :: Outputs -> IO (Either Text (Config, Outputs))
- Cmt.IO.Input: loop :: Config -> IO [(Name, Text)]
+ Cmt.IO.Input: loop :: Config -> IO Outputs
- Cmt.Output.Format: format :: Config -> [Output] -> Text
+ Cmt.Output.Format: format :: Config -> Outputs -> Text
- Cmt.Parser.Config: predefined :: Text -> Either Text [PreDefinedPart]
+ Cmt.Parser.Config: predefined :: Text -> Either Text PreDefinedParts
- Cmt.Parser.Config.PreDefined: predefinedPartsP :: Parser [PreDefinedPart]
+ Cmt.Parser.Config.PreDefined: predefinedPartsP :: [Part] -> Parser PreDefinedParts
- Cmt.Types.Config: type PreDefinedPart = (Text, Text)
+ Cmt.Types.Config: type PreDefinedPart = (Text, Config)

Files

README.md view
@@ -1,63 +1,52 @@ # cmt -Write consistent git commit messages+Write consistent git commit messages based on a custom template. -## Install+Similar idea to [commitizen](https://github.com/commitizen/cz-cli), but with an emphasis on making it easy to define a custom commit style. -[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`).+- [Concept](#Concept)+- [Format](#Format)+- [Usage](#Usage)+- [Install](#Install) -### Cabal+![Demo](https://files.smallhadroncollider.com/cmt-0.4.gif) -**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+## Concept -**Requirements**: [Stack](https://docs.haskellstack.org/en/stable/README/)+It's important to write consistent commit messages, but depending on the project you may well want to use different commit styles. -The following command will build cmt and then install it in `~/.local/bin`:+With `cmt` you create a `.cmt` file in your project directory. The `.cmt` file enforces a particular style of commit message for that project. You can also add predefined commit messages for things like version bumps and updating the readme. -```bash-stack build && stack install-```+For example, for my programming projects I try to use a commit style similar to the [AngularJS Commit Message Guidelines](https://gist.github.com/stephenparish/9941e89d80e2bc58a153). However, this isn't appropriate for my teaching notes repos or for my capistrano build repos.  -## Usage--Add a `.cmt` file to your project directory.--```bash-cmt # will show the options and then commit-```+## Format -If you're using the `${*}` format option then:+A `.cmt` file consist of two parts: the input parts and the output format. -```bash-cmt "blah blah blah" # this will go in ${*} place-```+A basic `.cmt` file to include a subject and body would look like: -If there are commit message you use frequently (such as "version bump"), you can setup predefined messages with aliases:+```txt+# The input parts+{+    "Subject" = @ # Single line input+    "Body" = !@ # Multi-line input+} -```bash-cmt -p vb # use the version bump message-```+# predefined commit messages+# this section is optional+{+    vb = "version bump"+} -If the commit returns with a non-zero status code, your previous commit message is stored in a `.cmt.bkp` file. You can re-run the commit when you're ready with:+# The output format+${Subject} -```bash-cmt --prev+${Body} ``` -### 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):+A more complex example, the [AngularJS Commit Message Guidelines](https://gist.github.com/stephenparish/9941e89d80e2bc58a153):  ```txt # The input parts@@ -72,16 +61,17 @@         "test",         "chore"     ]-    "Scope" = % # Select from a list of staged files-    "Subject" = @ # Single line input+    "Scope" = @ # Single line input+    "Subject" = @     "Body" = !@ # Multi-line input     "Footer" = !@ }  # predefined messages+# this section is optional {-    vb = "chore (package.yaml): version bump"-    readme = "docs (README.md): updated readme"+    vb = "chore: version bump"+    readme = "docs: updated readme" }  # The output format@@ -94,9 +84,17 @@ ${Footer} ``` +For my capistrano build repos the `.cmt` file is simply: -#### Input Parts+```txt+{} +"latest build"+```+++### 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@@ -104,12 +102,40 @@ - `%`: select from a list of staged files - `["option 1", "option 2"]`: list of options -#### Output Format+### Predefined Messages -The output format consists of named input parts plus anything else you want.+The predefined messages section is optional. You can provide a list of names and messages and then use the `-p <name>` command-line argument to use one of them. -You can accept a output called `${*}`, which will add in whatever is passed to `cmt` as command line arguments.+For example, with the following config, `cmt -p vb` would use the message "version bump". +```txt+vb = "version bump"+```++Predefined messages can also use any input parts defined in the prior section. An example of this would be:++```txt+{+    "Project" = [+        "ghc",+        "cabal"+    ]+}+{+    vb = "${Project}: version bump"+}+```++Running `cmt -p vb` will now prompt you to select which project is getting version bumped.++### Output Format++The output format consists of named input parts (`${<name>}`) plus anything else you want.++#### Wildcard Output++You can accept an output called `${*}`, which will add in whatever is passed to `cmt` as command-line arguments.+ For example:  ```txt@@ -127,4 +153,64 @@  ```bash cmt "Blah blah blah"+```+++## Usage++Add a `.cmt` file to your project directory.++```bash+cmt # will show the options and then commit+```++`cmt` will also look in your home directory if a `.cmt` file isn't found in the project directory hierarchy. This can be used to define a global commit style, which you can then override on a per-project basis.++### Predefined Messages++If there are commit message you use frequently (such as "version bump"), you can setup predefined messages with aliases:++```bash+cmt -p vb # use the version bump message+```++### Wildcard Output++If you're using the `${*}` format option then:++```bash+cmt "blah blah blah" # this will go in ${*} place+```++### Re-run Failed Commits++If the commit returns with a non-zero status code, your previous commit message is stored in a `.cmt.bkp` file. You can re-run the commit when you're ready with:++```bash+cmt --prev+```+++## 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 ```
cmt.cabal view
@@ -1,6 +1,6 @@ cabal-version: 1.12 name: cmt-version: 0.4.0.0+version: 0.5.0.0 license: BSD3 license-file: LICENSE copyright: Small Hadron Collider / Mark Wales@@ -43,9 +43,10 @@         attoparsec >=0.13.2.2 && <0.14,         base >=4.7 && <5,         classy-prelude >=1.5.0 && <1.6,+        containers >=0.6.0.1 && <0.7,         directory >=1.3.3.0 && <1.4,         filepath >=1.4.2.1 && <1.5,-        process >=1.6.3.0 && <1.7,+        process >=1.6.5.0 && <1.7,         terminal-size >=0.3.2.1 && <0.4,         text >=1.2.3.1 && <1.3 
src/Cmt.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedLists #-}  module Cmt     ( go@@ -11,17 +12,18 @@ import System.Directory (removeFile) import System.Exit      (exitFailure, exitSuccess) -import Cmt.IO.Config     (load, readCfg)+import Cmt.IO.Config     (checkFormat, load, readCfg) import Cmt.IO.Git        (commit) import Cmt.IO.Input      (loop) import Cmt.Output.Format (format) import Cmt.Parser.Config (predefined)-import Cmt.Types.Config  (Config, Output)+import Cmt.Types.Config  (Config, Outputs)  data Next     = Previous     | PreDefined Text-    | Continue [Output]+                 Outputs+    | Continue Outputs  backup :: FilePath backup = ".cmt.bkp"@@ -38,7 +40,7 @@             writeFile backup (encodeUtf8 txt)             failure msg -display :: Either Text (Config, [Output]) -> IO ()+display :: Either Text (Config, Outputs) -> IO () display (Left err) = putStrLn err display (Right (cfg, output)) = do     parts <- loop cfg@@ -50,27 +52,29 @@     removeFile backup     send txt -predef :: Text -> IO ()-predef name = do+predef :: Text -> Outputs -> IO ()+predef name output = do     cfg <- load     case predefined =<< cfg of         Left msg -> failure msg         Right pre ->-            case find ((==) name . fst) pre of-                Nothing       -> failure "No matching predefined message"-                Just (_, txt) -> send txt+            case lookup name pre of+                Nothing -> failure "No matching predefined message"+                Just cf -> display $ checkFormat output cf  parseArgs :: [Text] -> Next-parseArgs ["--prev"]   = Previous-parseArgs ["-p", name] = PreDefined name-parseArgs []           = Continue []-parseArgs [msg]        = Continue [("*", msg)]-parseArgs parts        = Continue [("*", unwords parts)]+parseArgs ["--prev"]        = Previous+parseArgs ["-p", name]      = PreDefined name []+parseArgs ["-p", name, msg] = PreDefined name [("*", msg)]+parseArgs ("-p":name:parts) = PreDefined name [("*", unwords parts)]+parseArgs []                = Continue []+parseArgs [msg]             = Continue [("*", msg)]+parseArgs parts             = Continue [("*", unwords parts)]  go :: IO () go = do     next <- parseArgs <$> getArgs     case next of-        Continue output -> readCfg output >>= display-        Previous        -> previous-        PreDefined name -> predef name+        Continue output        -> readCfg output >>= display+        Previous               -> previous+        PreDefined name output -> predef name output
src/Cmt/IO/Config.hs view
@@ -1,9 +1,11 @@ {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedLists #-}  module Cmt.IO.Config     ( load     , readCfg+    , checkFormat     ) where  import ClassyPrelude@@ -18,7 +20,7 @@ configFile :: FilePath configFile = ".cmt" -checkFormat :: [Output] -> Config -> Either Text (Config, [Output])+checkFormat :: Outputs -> Config -> Either Text (Config, Outputs) checkFormat output (Config parts format) = do     let partNames = nub $ partName <$> parts     let formatNames = nub . catMaybes $ formatName <$> format@@ -29,7 +31,7 @@             | otherwise = Right (Config parts format, output)     result -parse :: [Output] -> Text -> Either Text (Config, [Output])+parse :: Outputs -> Text -> Either Text (Config, Outputs) parse output cfg = config cfg >>= checkFormat output  read :: FilePath -> IO Text@@ -74,7 +76,7 @@         Just path -> Right <$> read path         Nothing   -> pure $ Left ".cmt file not found" -readCfg :: [Output] -> IO (Either Text (Config, [Output]))+readCfg :: Outputs -> IO (Either Text (Config, Outputs)) readCfg output = do     cfg <- load     pure (parse output =<< cfg)
src/Cmt/IO/Input.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedLists #-}  module Cmt.IO.Input     ( loop@@ -7,12 +8,15 @@  import ClassyPrelude +import Data.Map.Strict              (Map) import System.Console.Terminal.Size (size, width)  import Cmt.IO.Git         (changed) import Cmt.Parser.Options (parse) import Cmt.Types.Config +type Choice = Map Int Text+ prompt :: Text -> IO Text prompt s = do     putStr $ s <> " "@@ -25,23 +29,27 @@ putName :: Text -> IO () putName name = putStrLn $ "\n" <> name <> ":" -listItem :: (Int, Text) -> Text-listItem (n, o) = tshow n <> ") " <> o+listItem :: Int -> Text -> Text+listItem n o = tshow n <> ") " <> o -displayOptions :: [(Int, Text)] -> IO ()+displayOptions :: Choice -> IO () displayOptions opts = do-    let long = intercalate "   " $ listItem <$> opts+    let parts = mapWithKey listItem opts+    let long = intercalate "   " parts     maxLength <- getWidth     if length long < maxLength         then putStrLn long-        else sequence_ $ putStrLn . listItem <$> opts+        else sequence_ $ putStrLn <$> parts -choice :: [(Int, Text)] -> Int -> Maybe Text-choice opts chosen = snd <$> find ((== chosen) . fst) opts+choice :: Choice -> Int -> Maybe Text+choice opts chosen = lookup chosen opts +choiceGen :: [Text] -> Choice+choiceGen ts = mapFromList $ zip [1 ..] ts+ options :: [Text] -> IO Text options opts = do-    let opts' = zip [1 ..] opts+    let opts' = choiceGen opts     displayOptions opts'     chosen <- parse <$> prompt ">"     case chosen of@@ -66,11 +74,11 @@         then line         else pure value -output :: Part -> IO (Name, Text)+output :: Part -> IO Output output (Part name Line)           = putName name >> (,) name <$> line output (Part name (Options opts)) = putName name >> (,) name <$> options opts output (Part name Lines)          = putName name >> (,) name <$> (unlines <$> multiLine []) output (Part name Changed)        = putName name >> (,) name <$> (options =<< changed) -loop :: Config -> IO [(Name, Text)]-loop (Config parts _) = sequence $ output <$> parts+loop :: Config -> IO Outputs+loop (Config parts _) = mapFromList <$> traverse output parts
src/Cmt/Output/Format.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedLists #-}  module Cmt.Output.Format     ( format@@ -9,12 +9,9 @@  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     -> ""+tidy :: Outputs -> FormatPart -> Maybe Text+tidy _ (Literal c)      = pure c+tidy parts (Named name) = lookup name parts -format :: Config -> [Output] -> Text-format (Config _ fmt) parts = concat $ tidy parts <$> fmt+format :: Config -> Outputs -> Text+format (Config _ fmt) parts = concat . catMaybes $ tidy parts <$> fmt
src/Cmt/Parser/Config.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedLists #-}  module Cmt.Parser.Config     ( config@@ -18,15 +19,15 @@ configP :: Parser Config configP = do     parts <- partsP-    _ <- predefinedPartsP+    _ <- predefinedPartsP parts     format <- formatP $ partName <$> parts     _ <- endOfInput     pure $ Config parts format -predefinedP :: Parser [PreDefinedPart]+predefinedP :: Parser PreDefinedParts predefinedP = do     parts <- partsP-    pre <- predefinedPartsP+    pre <- predefinedPartsP parts     _ <- formatP $ partName <$> parts     _ <- endOfInput     pure pre@@ -39,7 +40,7 @@         Left _ ->             Left "Could not parse config. Check that your format doesn't contain any invalid parts." -predefined :: Text -> Either Text [PreDefinedPart]+predefined :: Text -> Either Text PreDefinedParts predefined cfg =     case parseOnly predefinedP cfg of         Right c -> Right c
src/Cmt/Parser/Config/PreDefined.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedLists #-}  module Cmt.Parser.Config.PreDefined     ( predefinedPartsP@@ -10,15 +10,29 @@ import Data.Attoparsec.Text  import Cmt.Parser.Attoparsec+import Cmt.Parser.Config.Format (formatP) import Cmt.Types.Config  -- predefined-value :: Parser Text-value = lexeme $ char '"' *> tnotChar '"' <* char '"'+value :: Name -> [Part] -> Parser Config+value name parts = lexeme (char '"' *> takeTill (== '"') <* char '"') >>= getConfig+  where+    getConfig :: Text -> Parser Config+    getConfig template =+        case parseOnly (formatP $ partName <$> parts) template of+            Left _    -> fail $ "Invalid predefined template: " ++ show name+            Right fmt -> pure $ Config (filterParts fmt parts) fmt+    filterParts :: [FormatPart] -> [Part] -> [Part]+    filterParts fps = filter ((`elem` catMaybes (formatName <$> fps)) . partName) -partP :: Parser PreDefinedPart-partP = stripComments $ (,) <$> (pack <$> many' letter <* lexeme (char '=')) <*> value+partP :: [Part] -> Parser PreDefinedPart+partP ps =+    stripComments $ do+        name <- pack <$> many' letter <* lexeme (char '=')+        conf <- stripComments (value name ps)+        pure (name, conf) -predefinedPartsP :: Parser [PreDefinedPart]-predefinedPartsP =-    stripComments $ option [] (stripComments (char '{') *> many' partP <* stripComments (char '}'))+predefinedPartsP :: [Part] -> Parser PreDefinedParts+predefinedPartsP ps =+    mapFromList <$>+    option [] (stripComments (char '{') *> many' (partP ps) <* stripComments (char '}'))
src/Cmt/Types/Config.hs view
@@ -2,11 +2,16 @@  module Cmt.Types.Config where -import ClassyPrelude (Eq, Maybe (..), Show, Text)+import ClassyPrelude   (Eq, Maybe (..), Show, Text)+import Data.Map.Strict (Map)  type Output = (Name, Text) -type PreDefinedPart = (Text, Text)+type Outputs = Map Name Text++type PreDefinedPart = (Text, Config)++type PreDefinedParts = Map Text Config  type Name = Text 
test/Cmt/Parser/ConfigTest.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE TemplateHaskell #-}  module Cmt.Parser.ConfigTest where@@ -47,6 +48,24 @@ pre :: Text pre = decodeUtf8 $(embedFile "test/data/.cmt-predefined") +preWithVars :: Text+preWithVars = decodeUtf8 $(embedFile "test/data/.cmt-predefined-with-vars")++preConfig :: PreDefinedParts+preConfig =+    [ ("vb", Config [] [Literal "chore (package.yaml): version bump"])+    , ("readme", Config [] [Literal "docs (README.md): updated readme"])+    ]++preWithVarsConfig :: PreDefinedParts+preWithVarsConfig =+    [ ( "vb"+      , Config [Part "Scope" Changed] [Literal "chore (", Named "Scope", Literal "): version bump"])+    , ( "readme"+      , Config [Part "Short Message" Line] [Literal "docs (README.md): ", Named "Short Message"])+    , ("multiline", Config [] [Literal "This\nNow works # but comments are included\nI believe\n"])+    ]+ -- import Test.Tasty.HUnit test_config :: TestTree test_config =@@ -71,12 +90,12 @@                     (assertEqual "Gives back correct format" (Right []) (predefined comments))               , testCase                     "predefined"+                    (assertEqual "Gives back correct format" (Right preConfig) (predefined pre))+              , testCase+                    "predefined with variables"                     (assertEqual                          "Gives back correct format"-                         (Right-                              [ ("vb", "chore (package.yaml): version bump")-                              , ("readme", "docs (README.md): updated readme")-                              ])-                         (predefined pre))+                         (Right preWithVarsConfig)+                         (predefined preWithVars))               ]         ]