2021-02-24 23:13:09 -05:00
|
|
|
# This file is copied from the original at tools/lib/gitlint-rules.py in zulip/zulip.
|
|
|
|
# Please don't edit here; instead update the zulip/zulip copy and then resync this file.
|
|
|
|
|
|
|
|
from typing import List
|
|
|
|
|
|
|
|
from gitlint.git import GitCommit
|
|
|
|
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 = {
|
2021-05-28 05:03:46 -04:00
|
|
|
'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',
|
2021-02-24 23:13:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
imperative_forms = [
|
2021-05-28 05:03:46 -04:00
|
|
|
'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',
|
2021-02-24 23:13:09 -05:00
|
|
|
]
|
|
|
|
imperative_forms.sort()
|
|
|
|
|
|
|
|
|
|
|
|
def head_binary_search(key: str, words: List[str]) -> str:
|
2021-05-28 05:03:46 -04:00
|
|
|
"""Find the imperative mood version of `word` by looking at the first
|
|
|
|
3 characters."""
|
2021-02-24 23:13:09 -05:00
|
|
|
|
|
|
|
# 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):
|
2021-05-28 05:03:46 -04:00
|
|
|
"""This rule will enforce that the commit message title uses imperative
|
2021-02-24 23:13:09 -05:00
|
|
|
mood. This is done by checking if the first word is in `WORD_SET`, if so
|
2021-05-28 05:03:46 -04:00
|
|
|
show the word in the correct mood."""
|
2021-02-24 23:13:09 -05:00
|
|
|
|
|
|
|
name = "title-imperative-mood"
|
|
|
|
id = "Z1"
|
|
|
|
target = CommitMessageTitle
|
|
|
|
|
2021-05-28 05:03:46 -04:00
|
|
|
error_msg = (
|
|
|
|
'The first word in commit title should be in imperative mood '
|
|
|
|
'("{word}" -> "{imperative}"): "{title}"'
|
|
|
|
)
|
2021-02-24 23:13:09 -05:00
|
|
|
|
|
|
|
def validate(self, line: str, 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)
|
2021-05-28 05:03:46 -04:00
|
|
|
violation = RuleViolation(
|
|
|
|
self.id,
|
|
|
|
self.error_msg.format(
|
|
|
|
word=first_word,
|
|
|
|
imperative=imperative,
|
|
|
|
title=commit.message.title,
|
|
|
|
),
|
|
|
|
)
|
2021-02-24 23:13:09 -05:00
|
|
|
|
|
|
|
violations.append(violation)
|
|
|
|
|
|
|
|
return violations
|