From cce18ed11ba0900299beededb207f12b416d550f Mon Sep 17 00:00:00 2001 From: LoopThrough-i-j <dganguly1120@gmail.com> Date: Thu, 25 Feb 2021 09:43:09 +0530 Subject: [PATCH] lint: Setup gitlint. Setup gitlint for developers to write well formatted commit messages. Note: .gitlint, gitlint-rules.py and lint-commits are taken directly from zulip/zulip with minor changes. --- .gitlint | 15 +++++ requirements.txt | 1 + tools/gitlint-rules.py | 145 +++++++++++++++++++++++++++++++++++++++++ tools/lint | 5 ++ tools/lint-commits | 27 ++++++++ 5 files changed, 193 insertions(+) create mode 100644 .gitlint create mode 100644 tools/gitlint-rules.py create mode 100755 tools/lint-commits diff --git a/.gitlint b/.gitlint new file mode 100644 index 00000000..951649a8 --- /dev/null +++ b/.gitlint @@ -0,0 +1,15 @@ +# This file is copied from the original .gitlint at zulip/zulip. +# Please don't edit here; instead update the zulip/zulip copy and then resync this file. + +[general] +ignore=title-trailing-punctuation, body-min-length, body-is-missing +extra-path=tools/gitlint-rules.py + +[title-match-regex] +regex=^(.+:\ )?[A-Z].+\.$ + +[title-max-length] +line-length=76 + +[body-max-line-length] +line-length=76 diff --git a/requirements.txt b/requirements.txt index 286366b2..8ff55d0a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ pytest -e ./zulip_botserver -e git+https://github.com/zulip/zulint@14e3974001bf8442a6a3486125865660f1f2eb68#egg=zulint==1.0.0 mypy==0.790 +gitlint>=0.13.0 diff --git a/tools/gitlint-rules.py b/tools/gitlint-rules.py new file mode 100644 index 00000000..322f96f0 --- /dev/null +++ b/tools/gitlint-rules.py @@ -0,0 +1,145 @@ +# 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 = { + '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 diff --git a/tools/lint b/tools/lint index 58d34b70..9baa8797 100755 --- a/tools/lint +++ b/tools/lint @@ -15,6 +15,7 @@ EXCLUDED_FILES = [ def run() -> None: parser = argparse.ArgumentParser() add_default_linter_arguments(parser) + parser.add_argument('--no-gitlint', action='store_true', help='Disable gitlint') args = parser.parse_args() linter_config = LinterConfig(args) @@ -27,6 +28,10 @@ def run() -> None: linter_config.external_linter('flake8', ['flake8'], ['py'], description="Standard Python linter (config: .flake8)") + if not args.no_gitlint: + linter_config.external_linter('gitlint', ['tools/lint-commits'], + description="Git Lint for commit messages") + @linter_config.lint def custom_py() -> int: """Runs custom checks for python files (config: tools/linter_lib/custom_check.py)""" diff --git a/tools/lint-commits b/tools/lint-commits new file mode 100755 index 00000000..3d6422a9 --- /dev/null +++ b/tools/lint-commits @@ -0,0 +1,27 @@ +#!/bin/bash + +# This file is copied from the original tools/commit-message-lint at zulip/zulip, +# Edited at Line 14 Col 97 (zulip -> python-zulip-api) +# Please don't edit here; instead update the zulip/zulip copy and then resync this file. + +# Lint all commit messages that are newer than upstream/master if running +# locally or the commits in the push or PR Gh-Actions. + +# The rules can be found in /.gitlint + +if [[ " +$(git remote -v) +" =~ ' +'([^[:space:]]*)[[:space:]]*(https://github\.com/|ssh://git@github\.com/|git@github\.com:)zulip/python-zulip-api(\.git|/)?\ \(fetch\)' +' ]]; then + range="${BASH_REMATCH[1]}/master..HEAD" +else + range="upstream/master..HEAD" +fi + +commits=$(git log "$range" | wc -l) +if [ "$commits" -gt 0 ]; then + # Only run gitlint with non-empty commit lists, to avoid a printed + # warning. + gitlint --commits "$range" +fi