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 +134/−48
- cmt.cabal +3/−2
- src/Cmt.hs +21/−17
- src/Cmt/IO/Config.hs +5/−3
- src/Cmt/IO/Input.hs +19/−11
- src/Cmt/Output/Format.hs +6/−9
- src/Cmt/Parser/Config.hs +5/−4
- src/Cmt/Parser/Config/PreDefined.hs +22/−8
- src/Cmt/Types/Config.hs +7/−2
- test/Cmt/Parser/ConfigTest.hs +24/−5
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+ -**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)) ] ]