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:05:11 -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:05:11 -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.
|
2021-05-28 05:05:11 -04:00
|
|
|
if key in ["displays", "displaying", "displayed"]:
|
|
|
|
return "display"
|
2021-02-24 23:13:09 -05:00
|
|
|
|
|
|
|
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 = (
|
2021-05-28 05:05:11 -04:00
|
|
|
"The first word in commit title should be in imperative mood "
|
2021-05-28 05:03:46 -04:00
|
|
|
'("{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>.`)
|
2021-05-28 05:05:11 -04:00
|
|
|
words = line.split(": ", 1)[-1].split()
|
2021-02-24 23:13:09 -05:00
|
|
|
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
|