zulip/tools/lib/gitlint-rules.py

317 lines
5.7 KiB
Python

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 = {
"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 = [
"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",
]
imperative_forms.sort()
def head_binary_search(key: str, 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: 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)
violation = RuleViolation(
self.id,
self.error_msg.format(
word=first_word,
imperative=imperative,
title=commit.message.title,
),
)
violations.append(violation)
return violations