mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 22:13:26 +00:00
Fixes #2665. Regenerated by tabbott with `lint --fix` after a rebase and change in parameters. Note from tabbott: In a few cases, this converts technical debt in the form of unsorted imports into different technical debt in the form of our largest files having very long, ugly import sequences at the start. I expect this change will increase pressure for us to split those files, which isn't a bad thing. Signed-off-by: Anders Kaseorg <anders@zulip.com>
163 lines
5.7 KiB
Python
163 lines
5.7 KiB
Python
import re
|
|
from typing import List, Text
|
|
|
|
from gitlint.git import GitCommit
|
|
from gitlint.options import StrOption
|
|
from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation
|
|
|
|
# Word list from https://github.com/m1foley/fit-commit
|
|
# Copyright (c) 2015 Mike Foley
|
|
# License: MIT
|
|
# Ref: fit_commit/validators/tense.rb
|
|
WORD_SET = {
|
|
'adds', 'adding', 'added',
|
|
'allows', 'allowing', 'allowed',
|
|
'amends', 'amending', 'amended',
|
|
'bumps', 'bumping', 'bumped',
|
|
'calculates', 'calculating', 'calculated',
|
|
'changes', 'changing', 'changed',
|
|
'cleans', 'cleaning', 'cleaned',
|
|
'commits', 'committing', 'committed',
|
|
'corrects', 'correcting', 'corrected',
|
|
'creates', 'creating', 'created',
|
|
'darkens', 'darkening', 'darkened',
|
|
'disables', 'disabling', 'disabled',
|
|
'displays', 'displaying', 'displayed',
|
|
'documents', 'documenting', 'documented',
|
|
'drys', 'drying', 'dryed',
|
|
'ends', 'ending', 'ended',
|
|
'enforces', 'enforcing', 'enforced',
|
|
'enqueues', 'enqueuing', 'enqueued',
|
|
'extracts', 'extracting', 'extracted',
|
|
'finishes', 'finishing', 'finished',
|
|
'fixes', 'fixing', 'fixed',
|
|
'formats', 'formatting', 'formatted',
|
|
'guards', 'guarding', 'guarded',
|
|
'handles', 'handling', 'handled',
|
|
'hides', 'hiding', 'hid',
|
|
'increases', 'increasing', 'increased',
|
|
'ignores', 'ignoring', 'ignored',
|
|
'implements', 'implementing', 'implemented',
|
|
'improves', 'improving', 'improved',
|
|
'keeps', 'keeping', 'kept',
|
|
'kills', 'killing', 'killed',
|
|
'makes', 'making', 'made',
|
|
'merges', 'merging', 'merged',
|
|
'moves', 'moving', 'moved',
|
|
'permits', 'permitting', 'permitted',
|
|
'prevents', 'preventing', 'prevented',
|
|
'pushes', 'pushing', 'pushed',
|
|
'rebases', 'rebasing', 'rebased',
|
|
'refactors', 'refactoring', 'refactored',
|
|
'removes', 'removing', 'removed',
|
|
'renames', 'renaming', 'renamed',
|
|
'reorders', 'reordering', 'reordered',
|
|
'replaces', 'replacing', 'replaced',
|
|
'requires', 'requiring', 'required',
|
|
'restores', 'restoring', 'restored',
|
|
'sends', 'sending', 'sent',
|
|
'sets', 'setting',
|
|
'separates', 'separating', 'separated',
|
|
'shows', 'showing', 'showed',
|
|
'simplifies', 'simplifying', 'simplified',
|
|
'skips', 'skipping', 'skipped',
|
|
'sorts', 'sorting',
|
|
'speeds', 'speeding', 'sped',
|
|
'starts', 'starting', 'started',
|
|
'supports', 'supporting', 'supported',
|
|
'takes', 'taking', 'took',
|
|
'testing', 'tested', # 'tests' excluded to reduce false negative
|
|
'truncates', 'truncating', 'truncated',
|
|
'updates', 'updating', 'updated',
|
|
'uses', 'using', 'used',
|
|
}
|
|
|
|
imperative_forms = sorted([
|
|
'add', 'allow', 'amend', 'bump', 'calculate', 'change', 'clean', 'commit',
|
|
'correct', 'create', 'darken', 'disable', 'display', 'document', 'dry',
|
|
'end', 'enforce', 'enqueue', 'extract', 'finish', 'fix', 'format', 'guard',
|
|
'handle', 'hide', 'ignore', 'implement', 'improve', 'increase', 'keep',
|
|
'kill', 'make', 'merge', 'move', 'permit', 'prevent', 'push', 'rebase',
|
|
'refactor', 'remove', 'rename', 'reorder', 'replace', 'require', 'restore',
|
|
'send', 'separate', 'set', 'show', 'simplify', 'skip', 'sort', 'speed',
|
|
'start', 'support', 'take', 'test', 'truncate', 'update', 'use',
|
|
])
|
|
|
|
|
|
def head_binary_search(key: Text, words: List[str]) -> str:
|
|
""" Find the imperative mood version of `word` by looking at the first
|
|
3 characters. """
|
|
|
|
# Edge case: 'disable' and 'display' have the same 3 starting letters.
|
|
if key in ['displays', 'displaying', 'displayed']:
|
|
return 'display'
|
|
|
|
lower = 0
|
|
upper = len(words) - 1
|
|
|
|
while True:
|
|
if lower > upper:
|
|
# Should not happen
|
|
raise Exception(f"Cannot find imperative mood of {key}")
|
|
|
|
mid = (lower + upper) // 2
|
|
imperative_form = words[mid]
|
|
|
|
if key[:3] == imperative_form[:3]:
|
|
return imperative_form
|
|
elif key < imperative_form:
|
|
upper = mid - 1
|
|
elif key > imperative_form:
|
|
lower = mid + 1
|
|
|
|
|
|
class ImperativeMood(LineRule):
|
|
""" This rule will enforce that the commit message title uses imperative
|
|
mood. This is done by checking if the first word is in `WORD_SET`, if so
|
|
show the word in the correct mood. """
|
|
|
|
name = "title-imperative-mood"
|
|
id = "Z1"
|
|
target = CommitMessageTitle
|
|
|
|
error_msg = ('The first word in commit title should be in imperative mood '
|
|
'("{word}" -> "{imperative}"): "{title}"')
|
|
|
|
def validate(self, line: Text, commit: GitCommit) -> List[RuleViolation]:
|
|
violations = []
|
|
|
|
# Ignore the section tag (ie `<section tag>: <message body>.`)
|
|
words = line.split(': ', 1)[-1].split()
|
|
first_word = words[0].lower()
|
|
|
|
if first_word in WORD_SET:
|
|
imperative = head_binary_search(first_word, imperative_forms)
|
|
violation = RuleViolation(self.id, self.error_msg.format(
|
|
word=first_word,
|
|
imperative=imperative,
|
|
title=commit.message.title,
|
|
))
|
|
|
|
violations.append(violation)
|
|
|
|
return violations
|
|
|
|
|
|
class TitleMatchRegexAllowException(LineRule):
|
|
"""Allows revert commits contrary to the built-in title-match-regex rule"""
|
|
|
|
name = 'title-match-regex-allow-exception'
|
|
id = 'Z2'
|
|
target = CommitMessageTitle
|
|
options_spec = [StrOption('regex', ".*", "Regex the title should match")]
|
|
|
|
def validate(self, title: Text, commit: GitCommit) -> List[RuleViolation]:
|
|
|
|
regex = self.options['regex'].value
|
|
pattern = re.compile(regex, re.UNICODE)
|
|
if not pattern.search(title) and not title.startswith("Revert \""):
|
|
violation_msg = f"Title does not match regex ({regex})"
|
|
return [RuleViolation(self.id, violation_msg, title)]
|
|
|
|
return []
|