summaryrefslogtreecommitdiff
path: root/linters/lint-commit-msg/Main.hs
blob: cf12b19ba5b5bf1a38ba25d2c7f28c95a1f98c66 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}

{-# OPTIONS_GHC -Wno-missing-local-signatures #-}

module Main where

-- base
import           Control.Monad
  ( forM, forM_, unless, when )
import           Data.Maybe
  ( isJust )
import           System.Environment
  ( getArgs )
import           System.Exit
  ( ExitCode(..), exitWith )

-- mtl
import           Control.Monad.Writer
  ( liftIO, execWriter, tell )

-- text
import           Data.Text
  ( Text )
import qualified Data.Text    as T
import qualified Data.Text.IO as T
  ( putStrLn )

-- linters-common
import           Linters.Common
  ( LintMsg(..), LintLvl(..)
  , gitCatCommit, gitNormCid, tshow
  )

--------------------------------------------------------------------------------

main :: IO ()
main = do
    dir:refs <- getArgs >>= \case
        [] -> fail "usage: lint-commit-msg <git-repo> [<commit-id>+]"
        x  -> return x

    stats <- forM (map T.pack refs) $ \ref -> do
        cid <- gitNormCid dir ref
        (_, msg) <- gitCatCommit dir cid

        let cmsgs = lintMsg msg

        liftIO $ do
            -- putStrLn (T.unpack cid)
            -- forM_ (zip [1::Int ..] (T.lines msg)) $ \(lno,l) -> do
            --     putStrLn (show lno <> "\t" <> show l)
            -- putStrLn "--"

            let status = maximum (Nothing : [ Just lvl | LintMsg lvl _ _ _ <- cmsgs ])
                ok     = status < Just LintLvlErr

            unless (null cmsgs) $ do
                putStrLn "====================================================================================="
                putStrLn ("commit " <> T.unpack cid <> " has linter issues:")
                putStrLn ""
                forM_ cmsgs $ \(LintMsg lvl lno l m) -> do
                    let lvls = case lvl of
                            LintLvlErr  -> "*ERROR*"
                            LintLvlWarn -> "Warning"
                    putStrLn (" " <> lvls <> " on line " <> show lno <> ": " <> T.unpack m)
                    putStrLn (" > " <> show l)
                    putStrLn ""
                    return ()

            unless ok $
                putStrLn ("Validation FAILED for " <> T.unpack cid)

            return status

    unless (null $ filter isJust stats) $
        T.putStrLn "====================================================================================="

    let stats1 = maximum (Nothing : stats)

    unless (stats1 == Nothing) $ do
        T.putStrLn "There were commit message linter issues! For more information see"
        T.putStrLn " http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html"
        T.putStrLn ""

    unless (stats1 < Just LintLvlErr) $ do
        T.putStrLn "Validation FAILED because at least one commit had linter errors!"
        exitWith (ExitFailure 1)

    T.putStrLn "Commit message validation passed!"

-- | Commit message linter
lintMsg :: Text -> [LintMsg]
lintMsg msg0 = execWriter $ do
    -- subject-line validations
    if | T.null (T.strip subj) -> errSubj "empty subject line"
       | otherwise -> do
           when (T.stripStart subj /= subj) $
               errSubj "subject line with leading whitespace"

           when (T.stripEnd subj /= subj) $
               warnSubj "subject line with trailing whitespace"

           when (T.any (== '\t') subj) $
               errSubj "subject line contains TAB"

           if | slen > 80 -> errSubj  ("subject line longer than 80 characters (was " <> tshow slen <> " characters)"
                                       <> " -- , ideally subject line is at most 50 characters long")
              | slen > 50 -> warnSubj ("subject line longer than 50 characters (was " <> tshow slen <> " characters)")
              | slen < 8  -> errSubj  ("subject line shorter than 8 characters (was " <> tshow slen <> " characters)")
              | otherwise -> return ()

    -- 2nd-line & body validations
    case lns of
        []  -> return () -- empty commit msg -- will have caused already an LintLvlErr
        [_] -> return () -- single-line commit msg
        (_:line2:body) -> do
            -- 2nd line validations
            if | not (T.null line2)
                   -> tell [LintMsg LintLvlErr  2 line2 "2nd line must be empty"]
               | null body
                   -> tell [LintMsg LintLvlWarn 2 line2 "2nd line exists, but no commit msg body found"]
               | otherwise -> return ()

            -- body validations
            forM_ (zip [3..] body) $ \(lineno,l) -> do
                let llen = T.length l
                    warnBody m = tell [LintMsg LintLvlWarn lineno l m]
                    errBody  m = tell [LintMsg LintLvlErr  lineno l m]

                when (T.stripEnd l /= l) $ warnBody "trailing whitespace"

                when (T.any (== '\t') l) $ warnBody "contains TAB character"

                when (T.isPrefixOf "Summary:" l) $
                    warnBody "redundant Phabricator 'Summary:' tag detected -- please trim your commit message"

                when (T.isPrefixOf "Summary: Signed-off-by:" l) $
                    errBody "'Signed-Off-by:'-marker not starting on first column"

                if | llen > 100 -> errBody  ("body line longer than 100 characters (was "
                                             <> tshow llen <> " characters) -- "
                                             <> "ideally body lines are at most 72 characters long")
                   | llen > 72  -> warnBody ("body line longer than 72 characters (was "
                                             <> tshow llen <> " characters)")
                   | otherwise  -> return ()

    return ()
  where
    warnSubj m = tell [LintMsg LintLvlWarn 1 subj m]
    errSubj  m = tell [LintMsg LintLvlErr  1 subj m]

    lns = T.lines msg0

    subj | (l0:_) <- lns = l0
         | otherwise     = ""

    slen = T.length subj