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
|