Skip to content

Commit 5177377

Browse files
Lint: Setup gitlint.
Note: .gitlint, gitlint-rules.py and lint is taken directly from zulip/zulip.
1 parent 744fea8 commit 5177377

File tree

5 files changed

+205
-0
lines changed

5 files changed

+205
-0
lines changed

.gitlint

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[general]
2+
ignore=title-trailing-punctuation, body-min-length, body-is-missing, title-imperative-mood
3+
4+
extra-path=tools/gitlint-rules.py
5+
6+
[title-match-regex-allow-exception]
7+
regex=^(.+:\ )?[A-Z].+\.$
8+
9+
[title-max-length]
10+
line-length=76
11+
12+
[body-max-line-length]
13+
line-length=76

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ pytest
99
-e ./zulip_botserver
1010
-e git+https://github.com/zulip/zulint@639c0d34c23ac559ef0f7b9510cf95f73f6d0eb9#egg=zulint==1.0.0
1111
mypy==0.770
12+
gitlint>=0.10

tools/gitlint-rules.py

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import re
2+
from typing import List
3+
4+
from gitlint.git import GitCommit
5+
from gitlint.options import StrOption
6+
from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation
7+
8+
# Word list from https://github.com/m1foley/fit-commit
9+
# Copyright (c) 2015 Mike Foley
10+
# License: MIT
11+
# Ref: fit_commit/validators/tense.rb
12+
WORD_SET = {
13+
'adds', 'adding', 'added',
14+
'allows', 'allowing', 'allowed',
15+
'amends', 'amending', 'amended',
16+
'bumps', 'bumping', 'bumped',
17+
'calculates', 'calculating', 'calculated',
18+
'changes', 'changing', 'changed',
19+
'cleans', 'cleaning', 'cleaned',
20+
'commits', 'committing', 'committed',
21+
'corrects', 'correcting', 'corrected',
22+
'creates', 'creating', 'created',
23+
'darkens', 'darkening', 'darkened',
24+
'disables', 'disabling', 'disabled',
25+
'displays', 'displaying', 'displayed',
26+
'documents', 'documenting', 'documented',
27+
'drys', 'drying', 'dryed',
28+
'ends', 'ending', 'ended',
29+
'enforces', 'enforcing', 'enforced',
30+
'enqueues', 'enqueuing', 'enqueued',
31+
'extracts', 'extracting', 'extracted',
32+
'finishes', 'finishing', 'finished',
33+
'fixes', 'fixing', 'fixed',
34+
'formats', 'formatting', 'formatted',
35+
'guards', 'guarding', 'guarded',
36+
'handles', 'handling', 'handled',
37+
'hides', 'hiding', 'hid',
38+
'increases', 'increasing', 'increased',
39+
'ignores', 'ignoring', 'ignored',
40+
'implements', 'implementing', 'implemented',
41+
'improves', 'improving', 'improved',
42+
'keeps', 'keeping', 'kept',
43+
'kills', 'killing', 'killed',
44+
'makes', 'making', 'made',
45+
'merges', 'merging', 'merged',
46+
'moves', 'moving', 'moved',
47+
'permits', 'permitting', 'permitted',
48+
'prevents', 'preventing', 'prevented',
49+
'pushes', 'pushing', 'pushed',
50+
'rebases', 'rebasing', 'rebased',
51+
'refactors', 'refactoring', 'refactored',
52+
'removes', 'removing', 'removed',
53+
'renames', 'renaming', 'renamed',
54+
'reorders', 'reordering', 'reordered',
55+
'replaces', 'replacing', 'replaced',
56+
'requires', 'requiring', 'required',
57+
'restores', 'restoring', 'restored',
58+
'sends', 'sending', 'sent',
59+
'sets', 'setting',
60+
'separates', 'separating', 'separated',
61+
'shows', 'showing', 'showed',
62+
'simplifies', 'simplifying', 'simplified',
63+
'skips', 'skipping', 'skipped',
64+
'sorts', 'sorting',
65+
'speeds', 'speeding', 'sped',
66+
'starts', 'starting', 'started',
67+
'supports', 'supporting', 'supported',
68+
'takes', 'taking', 'took',
69+
'testing', 'tested', # 'tests' excluded to reduce false negative
70+
'truncates', 'truncating', 'truncated',
71+
'updates', 'updating', 'updated',
72+
'uses', 'using', 'used',
73+
}
74+
75+
imperative_forms = [
76+
'add', 'allow', 'amend', 'bump', 'calculate', 'change', 'clean', 'commit',
77+
'correct', 'create', 'darken', 'disable', 'display', 'document', 'dry',
78+
'end', 'enforce', 'enqueue', 'extract', 'finish', 'fix', 'format', 'guard',
79+
'handle', 'hide', 'ignore', 'implement', 'improve', 'increase', 'keep',
80+
'kill', 'make', 'merge', 'move', 'permit', 'prevent', 'push', 'rebase',
81+
'refactor', 'remove', 'rename', 'reorder', 'replace', 'require', 'restore',
82+
'send', 'separate', 'set', 'show', 'simplify', 'skip', 'sort', 'speed',
83+
'start', 'support', 'take', 'test', 'truncate', 'update', 'use',
84+
]
85+
imperative_forms.sort()
86+
87+
88+
def head_binary_search(key: str, words: List[str]) -> str:
89+
""" Find the imperative mood version of `word` by looking at the first
90+
3 characters. """
91+
92+
# Edge case: 'disable' and 'display' have the same 3 starting letters.
93+
if key in ['displays', 'displaying', 'displayed']:
94+
return 'display'
95+
96+
lower = 0
97+
upper = len(words) - 1
98+
99+
while True:
100+
if lower > upper:
101+
# Should not happen
102+
raise Exception(f"Cannot find imperative mood of {key}")
103+
104+
mid = (lower + upper) // 2
105+
imperative_form = words[mid]
106+
107+
if key[:3] == imperative_form[:3]:
108+
return imperative_form
109+
elif key < imperative_form:
110+
upper = mid - 1
111+
elif key > imperative_form:
112+
lower = mid + 1
113+
114+
115+
class ImperativeMood(LineRule):
116+
""" This rule will enforce that the commit message title uses imperative
117+
mood. This is done by checking if the first word is in `WORD_SET`, if so
118+
show the word in the correct mood. """
119+
120+
name = "title-imperative-mood"
121+
id = "Z1"
122+
target = CommitMessageTitle
123+
124+
error_msg = ('The first word in commit title should be in imperative mood '
125+
'("{word}" -> "{imperative}"): "{title}"')
126+
127+
def validate(self, line: str, commit: GitCommit) -> List[RuleViolation]:
128+
violations = []
129+
130+
# Ignore the section tag (ie `<section tag>: <message body>.`)
131+
words = line.split(': ', 1)[-1].split()
132+
first_word = words[0].lower()
133+
134+
if first_word in WORD_SET:
135+
imperative = head_binary_search(first_word, imperative_forms)
136+
violation = RuleViolation(self.id, self.error_msg.format(
137+
word=first_word,
138+
imperative=imperative,
139+
title=commit.message.title,
140+
))
141+
142+
violations.append(violation)
143+
144+
return violations
145+
146+
147+
class TitleMatchRegexAllowException(LineRule):
148+
"""Allows revert commits contrary to the built-in title-match-regex rule"""
149+
150+
name = 'title-match-regex-allow-exception'
151+
id = 'Z2'
152+
target = CommitMessageTitle
153+
options_spec = [StrOption('regex', ".*", "Regex the title should match")]
154+
155+
def validate(self, title: str, commit: GitCommit) -> List[RuleViolation]:
156+
157+
regex = self.options['regex'].value
158+
pattern = re.compile(regex, re.UNICODE)
159+
if not pattern.search(title) and not title.startswith("Revert \""):
160+
violation_msg = f"Title does not match regex ({regex})"
161+
return [RuleViolation(self.id, violation_msg, title)]
162+
163+
return []

tools/lint

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ EXCLUDED_FILES = [
1414
def run() -> None:
1515
parser = argparse.ArgumentParser()
1616
add_default_linter_arguments(parser)
17+
parser.add_argument('--no-gitlint', action='store_true', help='Disable gitlint')
1718
args = parser.parse_args()
1819

1920
linter_config = LinterConfig(args)
@@ -26,6 +27,10 @@ def run() -> None:
2627
linter_config.external_linter('flake8', ['flake8'], ['py'],
2728
description="Standard Python linter (config: .flake8)")
2829

30+
if not args.no_gitlint:
31+
linter_config.external_linter('gitlint', ['tools/lint-commits'],
32+
description="Git Lint for commit messages")
33+
2934
@linter_config.lint
3035
def custom_py() -> int:
3136
"""Runs custom checks for python files (config: tools/linter_lib/custom_check.py)"""

tools/lint-commits

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/bash
2+
3+
# Lint all commit messages that are newer than upstream/master if running
4+
# locally or the commits in the push or PR Gh-Actions.
5+
6+
# The rules can be found in /.gitlint
7+
8+
if [[ "
9+
$(git remote -v)
10+
" =~ '
11+
'([^[:space:]]*)[[:space:]]*(https://github\.com/|ssh://git@github\.com/|git@github\.com:)zulip/zulip(\.git|/)?\ \(fetch\)'
12+
' ]]; then
13+
range="${BASH_REMATCH[1]}/master..HEAD"
14+
else
15+
range="upstream/master..HEAD"
16+
fi
17+
18+
commits=$(git log "$range" | wc -l)
19+
if [ "$commits" -gt 0 ]; then
20+
# Only run gitlint with non-empty commit lists, to avoid a printed
21+
# warning.
22+
gitlint --commits "$range"
23+
fi

0 commit comments

Comments
 (0)