Skip to content

Commit

Permalink
ci: Add gitlint to pipeline
Browse files Browse the repository at this point in the history
This uses my personal preferences [1] to lint the project's history.

[1] https://github.com/Notgnoshi/dotfiles/tree/master/stowdir/.config/gitlint
  • Loading branch information
Notgnoshi committed Apr 6, 2024
1 parent f1cf3c3 commit 6d25c73
Show file tree
Hide file tree
Showing 4 changed files with 401 additions and 7 deletions.
26 changes: 26 additions & 0 deletions .github/gitlint/body_max_line_length_with_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import re

from gitlint.rules import BodyMaxLineLength


class BodyMaxLineLengthWithExceptions(BodyMaxLineLength):
"""Extend the existing body-max-line-length rule.
Allow narrow exceptions, specifically leading block quote-style
indent and footnote-style URLs, to the line length enforcement.
"""

name = "body-max-line-length-with-exceptions"
id = "UC2"

def validate(self, line, commit):
# Allow block-quoted lines to exceed the line length limit.
if line.startswith(" " * 4):
return None

# Allow footnote lines to exceed the line length limit.
ret = re.match(r"^\[\d+\] ", line)
if ret is not None:
return None

return super().validate(line, commit)
23 changes: 23 additions & 0 deletions .github/gitlint/gitlint.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[general]
ignore=body-is-missing,body-max-line-length

ignore-merge-commits=false
# If you're reverting a commit, the revert commit should darn well contain enough information about WHY
ignore-revert-commits=false
ignore-fixup-commits=false
ignore-fixup-amend-commits=false
ignore-squash-commits=false

# This will be the default in the future, so enable now
regex-style-search=true

[title-must-not-contain-word]
words=wip,fixup,fixups,squash,fixme,dropme,dontmerge,donotmerge

# 72 should be the target, but don't fail until it's past 80.
[body-max-line-length-with-exceptions]
line-length=80

# Not every commit needs a body. Some are simple enough just a title will do
[body-min-length]
min-length=0
321 changes: 321 additions & 0 deletions .github/gitlint/title_imperative_mood.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
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
if key < imperative_form:
upper = mid - 1
elif key > imperative_form:
lower = mid + 1


class ImperativeMood(LineRule):
"""A rule to 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 = "UC1"
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]:
"""Validate the given commit message."""
violations = []

# Ignore the section tag (ie `<section tag>: <message body>.`)
words = line.split(": ", 1)[-1].split()

if not words:
return None

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
Loading

0 comments on commit 6d25c73

Please sign in to comment.