1
0
Fork 0

Merging upstream version 0.19.1.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-13 06:07:32 +01:00
parent 61e6dccee9
commit 2efee3d3ab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
111 changed files with 2058 additions and 1676 deletions

View file

@ -1 +0,0 @@
../LICENSE

22
gitlint-core/LICENSE Normal file
View file

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Joris Roovers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,3 +0,0 @@
include README.md
include LICENSE
recursive-exclude gitlint/tests *

View file

@ -1 +0,0 @@
../README.md

26
gitlint-core/README.md Normal file
View file

@ -0,0 +1,26 @@
# Gitlint-core
# gitlint: [jorisroovers.github.io/gitlint](http://jorisroovers.github.io/gitlint/) #
[![Tests](https://github.com/jorisroovers/gitlint/workflows/Tests%20and%20Checks/badge.svg)](https://github.com/jorisroovers/gitlint/actions?query=workflow%3A%22Tests+and+Checks%22)
[![Coverage Status](https://coveralls.io/repos/github/jorisroovers/gitlint/badge.svg?branch=fix-coveralls)](https://coveralls.io/github/jorisroovers/gitlint?branch=fix-coveralls)
[![PyPi Package](https://img.shields.io/pypi/v/gitlint.png)](https://pypi.python.org/pypi/gitlint)
![Supported Python Versions](https://img.shields.io/pypi/pyversions/gitlint.svg)
Git commit message linter written in python, checks your commit messages for style.
**See [jorisroovers.github.io/gitlint](http://jorisroovers.github.io/gitlint/) for full documentation.**
<a href="http://jorisroovers.github.io/gitlint/" target="_blank">
<img src="https://raw.githubusercontent.com/jorisroovers/gitlint/main/docs/images/readme-gitlint.png" />
</a>
## Contributing ##
All contributions are welcome and very much appreciated!
**I'm [looking for contributors](https://github.com/jorisroovers/gitlint/issues/134) that are interested in taking a more active co-maintainer role as it's becoming increasingly difficult for me to find time to maintain gitlint. Please leave a comment in [#134](https://github.com/jorisroovers/gitlint/issues/134) if you're interested!**
See [jorisroovers.github.io/gitlint/contributing](http://jorisroovers.github.io/gitlint/contributing) for details on
how to get started - it's easy!
We maintain a [loose project plan on Github Projects](https://github.com/users/jorisroovers/projects/1/views/1).

View file

@ -1 +1,8 @@
__version__ = "0.19.0dev"
import sys
if sys.version_info >= (3, 8):
from importlib import metadata # pragma: nocover
else:
import importlib_metadata as metadata # pragma: nocover
__version__ = metadata.version("gitlint-core")

View file

@ -13,7 +13,7 @@ class PropertyCache:
return self._cache[cache_key]
def cache(original_func=None, cachekey=None): # pylint: disable=unused-argument
def cache(original_func=None, cachekey=None):
"""Cache decorator. Caches function return values.
Requires the parent class to extend and initialize PropertyCache.
Usage:

View file

@ -1,22 +1,22 @@
# pylint: disable=bad-option-value,wrong-import-position
# We need to disable the import position checks because of the windows check that we need to do below
import copy
import logging
import os
import platform
import stat
import sys
import click
import gitlint
from gitlint.lint import GitLinter
from gitlint.config import LintConfigBuilder, LintConfigError, LintConfigGenerator
from gitlint.deprecation import LOG as DEPRECATED_LOG, DEPRECATED_LOG_FORMAT
from gitlint.git import GitContext, GitContextError, git_version
from gitlint import hooks
from gitlint.config import LintConfigBuilder, LintConfigError, LintConfigGenerator
from gitlint.deprecation import DEPRECATED_LOG_FORMAT
from gitlint.deprecation import LOG as DEPRECATED_LOG
from gitlint.exception import GitlintError
from gitlint.git import GitContext, GitContextError, git_version
from gitlint.lint import GitLinter
from gitlint.shell import shell
from gitlint.utils import LOG_FORMAT
from gitlint.exception import GitlintError
# Error codes
GITLINT_SUCCESS = 0
@ -40,8 +40,6 @@ LOG = logging.getLogger("gitlint.cli")
class GitLintUsageError(GitlintError):
"""Exception indicating there is an issue with how gitlint is used."""
pass
def setup_logging():
"""Setup gitlint logging"""
@ -49,7 +47,7 @@ def setup_logging():
# Root log, mostly used for debug
root_log = logging.getLogger("gitlint")
root_log.propagate = False # Don't propagate to child loggers, the gitlint root logger handles everything
root_log.setLevel(logging.ERROR)
root_log.setLevel(logging.WARN)
handler = logging.StreamHandler()
formatter = logging.Formatter(LOG_FORMAT)
handler.setFormatter(formatter)
@ -69,10 +67,11 @@ def log_system_info():
LOG.debug("Git version: %s", git_version())
LOG.debug("Gitlint version: %s", gitlint.__version__)
LOG.debug("GITLINT_USE_SH_LIB: %s", os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]"))
LOG.debug("DEFAULT_ENCODING: %s", gitlint.utils.DEFAULT_ENCODING)
LOG.debug("TERMINAL_ENCODING: %s", gitlint.utils.TERMINAL_ENCODING)
LOG.debug("FILE_ENCODING: %s", gitlint.utils.FILE_ENCODING)
def build_config( # pylint: disable=too-many-arguments
def build_config(
target,
config_path,
c,
@ -172,11 +171,9 @@ def build_git_context(lint_config, msg_filename, commit_hash, refspec):
from_commit_msg = GitContext.from_commit_msg
if lint_config.staged:
LOG.debug("Fetching additional meta-data from staged commit")
from_commit_msg = (
lambda message: GitContext.from_staged_commit( # pylint: disable=unnecessary-lambda-assignment
message, lint_config.target
)
)
def from_commit_msg(message):
return GitContext.from_staged_commit(message, lint_config.target)
# Order of precedence:
# 1. Any data specified via --msg-filename
@ -208,7 +205,7 @@ def build_git_context(lint_config, msg_filename, commit_hash, refspec):
if refspec:
# 3.1.1 Not real refspec, but comma-separated list of commit hashes
if "," in refspec:
commit_hashes = [hash.strip() for hash in refspec.split(",")]
commit_hashes = [hash.strip() for hash in refspec.split(",") if hash]
return GitContext.from_local_repository(lint_config.target, commit_hashes=commit_hashes)
# 3.1.2 Real refspec
return GitContext.from_local_repository(lint_config.target, refspec=refspec)
@ -247,43 +244,43 @@ class ContextObj:
# fmt: off
@click.group(invoke_without_command=True, context_settings={'max_content_width': 120},
@click.group(invoke_without_command=True, context_settings={"max_content_width": 120},
epilog="When no COMMAND is specified, gitlint defaults to 'gitlint lint'.")
@click.option('--target', envvar='GITLINT_TARGET',
@click.option("--target", envvar="GITLINT_TARGET",
type=click.Path(exists=True, resolve_path=True, file_okay=False, readable=True),
help="Path of the target git repository. [default: current working directory]")
@click.option('-C', '--config', envvar='GITLINT_CONFIG',
@click.option("-C", "--config", envvar="GITLINT_CONFIG",
type=click.Path(exists=True, dir_okay=False, readable=True, resolve_path=True),
help=f"Config file location [default: {DEFAULT_CONFIG_FILE}]")
@click.option('-c', multiple=True,
@click.option("-c", multiple=True,
help="Config flags in format <rule>.<option>=<value> (e.g.: -c T1.line-length=80). " +
"Flag can be used multiple times to set multiple config values.") # pylint: disable=bad-continuation
@click.option('--commit', envvar='GITLINT_COMMIT', default=None, help="Hash (SHA) of specific commit to lint.")
@click.option('--commits', envvar='GITLINT_COMMITS', default=None,
"Flag can be used multiple times to set multiple config values.")
@click.option("--commit", envvar="GITLINT_COMMIT", default=None, help="Hash (SHA) of specific commit to lint.")
@click.option("--commits", envvar="GITLINT_COMMITS", default=None,
help="The range of commits (refspec or comma-separated hashes) to lint. [default: HEAD]")
@click.option('-e', '--extra-path', envvar='GITLINT_EXTRA_PATH',
@click.option("-e", "--extra-path", envvar="GITLINT_EXTRA_PATH",
help="Path to a directory or python module with extra user-defined rules",
type=click.Path(exists=True, resolve_path=True, readable=True))
@click.option('--ignore', envvar='GITLINT_IGNORE', default="", help="Ignore rules (comma-separated by id or name).")
@click.option('--contrib', envvar='GITLINT_CONTRIB', default="",
@click.option("--ignore", envvar="GITLINT_IGNORE", default="", help="Ignore rules (comma-separated by id or name).")
@click.option("--contrib", envvar="GITLINT_CONTRIB", default="",
help="Contrib rules to enable (comma-separated by id or name).")
@click.option('--msg-filename', type=click.File(encoding=gitlint.utils.DEFAULT_ENCODING),
@click.option("--msg-filename", type=click.File(encoding=gitlint.utils.FILE_ENCODING),
help="Path to a file containing a commit-msg.")
@click.option('--ignore-stdin', envvar='GITLINT_IGNORE_STDIN', is_flag=True,
@click.option("--ignore-stdin", envvar="GITLINT_IGNORE_STDIN", is_flag=True,
help="Ignore any stdin data. Useful for running in CI server.")
@click.option('--staged', envvar='GITLINT_STAGED', is_flag=True,
@click.option("--staged", envvar="GITLINT_STAGED", is_flag=True,
help="Attempt smart guesses about meta info (like author name, email, branch, changed files, etc) " +
"for staged commits.")
@click.option('--fail-without-commits', envvar='GITLINT_FAIL_WITHOUT_COMMITS', is_flag=True,
@click.option("--fail-without-commits", envvar="GITLINT_FAIL_WITHOUT_COMMITS", is_flag=True,
help="Hard fail when the target commit range is empty.")
@click.option('-v', '--verbose', envvar='GITLINT_VERBOSITY', count=True, default=0,
help="Verbosity, more v's for more verbose output (e.g.: -v, -vv, -vvv). [default: -vvv]", )
@click.option('-s', '--silent', envvar='GITLINT_SILENT', is_flag=True,
@click.option("-v", "--verbose", envvar="GITLINT_VERBOSITY", count=True, default=0,
help="Verbosity, use multiple times for more verbose output (e.g.: -v, -vv, -vvv). [default: -vvv]", )
@click.option("-s", "--silent", envvar="GITLINT_SILENT", is_flag=True,
help="Silent mode (no output). Takes precedence over -v, -vv, -vvv.")
@click.option('-d', '--debug', envvar='GITLINT_DEBUG', help="Enable debugging output.", is_flag=True)
@click.option("-d", "--debug", envvar="GITLINT_DEBUG", help="Enable debugging output.", is_flag=True)
@click.version_option(version=gitlint.__version__)
@click.pass_context
def cli( # pylint: disable=too-many-arguments
def cli(
ctx, target, config, c, commit, commits, extra_path, ignore, contrib,
msg_filename, ignore_stdin, staged, fail_without_commits, verbose,
silent, debug,
@ -499,5 +496,4 @@ def generate_config(ctx):
# Let's Party!
setup_logging()
if __name__ == "__main__":
# pylint: disable=no-value-for-parameter
cli() # pragma: no cover

View file

@ -1,17 +1,19 @@
from configparser import ConfigParser, Error as ConfigParserError
import copy
import re
import os
import re
import shutil
from collections import OrderedDict
from gitlint.utils import DEFAULT_ENCODING
from gitlint import rules # For some weird reason pylint complains about this, pylint: disable=unused-import
from gitlint import options
from gitlint import rule_finder
from configparser import ConfigParser
from configparser import Error as ConfigParserError
from gitlint import (
options,
rule_finder,
rules,
)
from gitlint.contrib import rules as contrib_rules
from gitlint.exception import GitlintError
from gitlint.utils import FILE_ENCODING
def handle_option_error(func):
@ -31,7 +33,7 @@ class LintConfigError(GitlintError):
pass
class LintConfig: # pylint: disable=too-many-instance-attributes
class LintConfig:
"""Class representing gitlint configuration.
Contains active config as well as number of methods to easily get/set the config.
"""
@ -105,7 +107,7 @@ class LintConfig: # pylint: disable=too-many-instance-attributes
@handle_option_error
def verbosity(self, value):
self._verbosity.set(value)
if self.verbosity < 0 or self.verbosity > 3:
if self.verbosity < 0 or self.verbosity > 3: # noqa: PLR2004 (Magic value used in comparison)
raise LintConfigError("Option 'verbosity' must be set between 0 and 3")
@property
@ -294,7 +296,7 @@ class LintConfig: # pylint: disable=too-many-instance-attributes
if not hasattr(self, attr_name) or attr_name[0] == "_":
raise LintConfigError(f"'{option_name}' is not a valid gitlint option")
# else:
# else
setattr(self, attr_name, option_value)
def __eq__(self, other):
@ -384,7 +386,7 @@ class RuleCollection:
"""Deletes all rules from the collection that match a given attribute name and value"""
# Create a new list based on _rules.values() because in python 3, values() is a ValuesView as opposed to a list
# This means you can't modify the ValueView while iterating over it.
for rule in [r for r in self._rules.values()]: # pylint: disable=unnecessary-comprehension
for rule in list(self._rules.values()):
if hasattr(rule, attr_name) and (getattr(rule, attr_name) == attr_val):
del self._rules[rule.id]
@ -466,7 +468,7 @@ class LintConfigBuilder:
try:
parser = ConfigParser()
with open(filename, encoding=DEFAULT_ENCODING) as config_file:
with open(filename, encoding=FILE_ENCODING) as config_file:
parser.read_file(config_file, filename)
for section_name in parser.sections():
@ -528,14 +530,15 @@ class LintConfigBuilder:
for section_name, section_dict in self._config_blueprint.items():
for option_name, option_value in section_dict.items():
qualified_section_name = section_name
# Skip over the general section, as we've already done that above
if section_name != "general":
if qualified_section_name != "general":
# If the section name contains a colon (:), then this section is defining a Named Rule
# Which means we need to instantiate that Named Rule in the config.
if self.RULE_QUALIFIER_SYMBOL in section_name:
section_name = self._add_named_rule(config, section_name)
qualified_section_name = self._add_named_rule(config, qualified_section_name)
config.set_rule_option(section_name, option_name, option_value)
config.set_rule_option(qualified_section_name, option_name, option_value)
return config

View file

@ -2,7 +2,6 @@ import re
from pathlib import Path
from typing import Tuple
from gitlint.rules import CommitRule, RuleViolation

View file

@ -1,6 +1,5 @@
import logging
LOG = logging.getLogger("gitlint.deprecated")
DEPRECATED_LOG_FORMAT = "%(levelname)s: %(message)s"

View file

@ -1,4 +1,4 @@
from sys import stdout, stderr
from sys import stderr, stdout
class Display:
@ -17,20 +17,20 @@ class Display:
if self.config.verbosity >= verbosity:
stream.write(message + "\n")
def v(self, message, exact=False): # pylint: disable=invalid-name
def v(self, message, exact=False):
self._output(message, 1, exact, stdout)
def vv(self, message, exact=False): # pylint: disable=invalid-name
def vv(self, message, exact=False):
self._output(message, 2, exact, stdout)
def vvv(self, message, exact=False): # pylint: disable=invalid-name
def vvv(self, message, exact=False):
self._output(message, 3, exact, stdout)
def e(self, message, exact=False): # pylint: disable=invalid-name
def e(self, message, exact=False):
self._output(message, 1, exact, stderr)
def ee(self, message, exact=False): # pylint: disable=invalid-name
def ee(self, message, exact=False):
self._output(message, 2, exact, stderr)
def eee(self, message, exact=False): # pylint: disable=invalid-name
def eee(self, message, exact=False):
self._output(message, 3, exact, stderr)

View file

@ -1,4 +1,2 @@
class GitlintError(Exception):
"""Based Exception class for all gitlint exceptions"""
pass

View file

@ -5,13 +5,12 @@ from pathlib import Path
import arrow
from gitlint import shell as sh
from gitlint.cache import PropertyCache, cache
from gitlint.exception import GitlintError
# import exceptions separately, this makes it a little easier to mock them out in the unit tests
from gitlint.shell import CommandNotFound, ErrorReturnCode
from gitlint.cache import PropertyCache, cache
from gitlint.exception import GitlintError
# For now, the git date format we use is fixed, but technically this format is determined by `git config log.date`
# We should fix this at some point :-)
GIT_TIMEFORMAT = "YYYY-MM-DD HH:mm:ss Z"
@ -22,8 +21,6 @@ LOG = logging.getLogger(__name__)
class GitContextError(GitlintError):
"""Exception indicating there is an issue with the git context"""
pass
class GitNotInstalledError(GitContextError):
def __init__(self):
@ -46,7 +43,7 @@ def _git(*command_parts, **kwargs):
git_kwargs.update(kwargs)
try:
LOG.debug(command_parts)
result = sh.git(*command_parts, **git_kwargs) # pylint: disable=unexpected-keyword-arg
result = sh.git(*command_parts, **git_kwargs)
# If we reach this point and the result has an exit_code that is larger than 0, this means that we didn't
# get an exception (which is the default sh behavior for non-zero exit codes) and so the user is expecting
# a non-zero exit code -> just return the entire result
@ -80,7 +77,7 @@ def git_commentchar(repository_path=None):
"""Shortcut for retrieving comment char from git config"""
commentchar = _git("config", "--get", "core.commentchar", _cwd=repository_path, _ok_code=[0, 1])
# git will return an exit code of 1 if it can't find a config value, in this case we fall-back to # as commentchar
if hasattr(commentchar, "exit_code") and commentchar.exit_code == 1: # pylint: disable=no-member
if hasattr(commentchar, "exit_code") and commentchar.exit_code == 1:
commentchar = "#"
return commentchar.replace("\n", "")
@ -174,11 +171,6 @@ class GitChangedFileStats:
def __str__(self) -> str:
return f"{self.filepath}: {self.additions} additions, {self.deletions} deletions"
def __repr__(self) -> str:
return (
f'GitChangedFileStats(filepath="{self.filepath}", additions={self.additions}, deletions={self.deletions})'
)
class GitCommit:
"""Class representing a git commit.
@ -193,7 +185,7 @@ class GitCommit:
message,
sha=None,
date=None,
author_name=None, # pylint: disable=too-many-arguments
author_name=None,
author_email=None,
parents=None,
changed_files_stats=None,
@ -289,7 +281,7 @@ class LocalGitCommit(GitCommit, PropertyCache):
startup time and reduces gitlint's memory footprint.
"""
def __init__(self, context, sha): # pylint: disable=super-init-not-called
def __init__(self, context, sha):
PropertyCache.__init__(self)
self.context = context
self.sha = sha
@ -382,7 +374,7 @@ class StagedLocalGitCommit(GitCommit, PropertyCache):
information.
"""
def __init__(self, context, commit_message): # pylint: disable=super-init-not-called
def __init__(self, context, commit_message):
PropertyCache.__init__(self)
self.context = context
self.message = commit_message

View file

@ -1,10 +1,10 @@
import shutil
import os
import shutil
import stat
from gitlint.utils import DEFAULT_ENCODING
from gitlint.git import git_hooks_dir
from gitlint.exception import GitlintError
from gitlint.git import git_hooks_dir
from gitlint.utils import FILE_ENCODING
COMMIT_MSG_HOOK_SRC_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "files", "commit-msg")
COMMIT_MSG_HOOK_DST_PATH = "commit-msg"
@ -52,9 +52,9 @@ class GitHookInstaller:
if not os.path.exists(dest_path):
raise GitHookInstallerError(f"There is no commit-msg hook present in {dest_path}.")
with open(dest_path, encoding=DEFAULT_ENCODING) as fp:
with open(dest_path, encoding=FILE_ENCODING) as fp:
lines = fp.readlines()
if len(lines) < 2 or lines[1] != GITLINT_HOOK_IDENTIFIER:
if len(lines) < 2 or lines[1] != GITLINT_HOOK_IDENTIFIER: # noqa: PLR2004 (Magic value used in comparison)
msg = (
f"The commit-msg hook in {dest_path} was not installed by gitlint (or it was modified).\n"
"Uninstallation of 3th party or modified gitlint hooks is not supported."

View file

@ -1,7 +1,7 @@
# pylint: disable=logging-not-lazy
import logging
from gitlint import rules as gitlint_rules
from gitlint import display
from gitlint import rules as gitlint_rules
from gitlint.deprecation import Deprecation
LOG = logging.getLogger(__name__)

View file

@ -1,6 +1,6 @@
from abc import abstractmethod
import os
import re
from abc import abstractmethod
from gitlint.exception import GitlintError
@ -37,7 +37,6 @@ class RuleOption:
@abstractmethod
def set(self, value):
"""Validates and sets the option's value"""
pass # pragma: no cover
def __str__(self):
return f"({self.name}: {self.value} ({self.description}))"

View file

@ -1,10 +1,10 @@
import fnmatch
import importlib
import inspect
import os
import sys
import importlib
from gitlint import rules, options
from gitlint import options, rules
def find_rule_classes(extra_path):
@ -55,7 +55,7 @@ def find_rule_classes(extra_path):
importlib.import_module(module)
except Exception as e:
raise rules.UserRuleError(f"Error while importing extra-path module '{module}': {e}")
raise rules.UserRuleError(f"Error while importing extra-path module '{module}': {e}") from e
# Find all rule classes in the module. We do this my inspecting all members of the module and checking
# 1) is it a class, if not, skip
@ -67,11 +67,7 @@ def find_rule_classes(extra_path):
for _, clazz in inspect.getmembers(sys.modules[module])
if inspect.isclass(clazz) # check isclass to ensure clazz.__module__ exists
and clazz.__module__ == module # ignore imported classes
and (
issubclass(clazz, rules.LineRule)
or issubclass(clazz, rules.CommitRule)
or issubclass(clazz, rules.ConfigurationRule)
)
and (issubclass(clazz, (rules.LineRule, rules.CommitRule, rules.ConfigurationRule)))
]
)
@ -82,7 +78,7 @@ def find_rule_classes(extra_path):
return rule_classes
def assert_valid_rule_class(clazz, rule_type="User-defined"): # pylint: disable=too-many-branches
def assert_valid_rule_class(clazz, rule_type="User-defined"): # noqa: PLR0912 (too many branches)
"""
Asserts that a given rule clazz is valid by checking a number of its properties:
- Rules must extend from LineRule, CommitRule or ConfigurationRule
@ -97,11 +93,7 @@ def assert_valid_rule_class(clazz, rule_type="User-defined"): # pylint: disable
"""
# Rules must extend from LineRule, CommitRule or ConfigurationRule
if not (
issubclass(clazz, rules.LineRule)
or issubclass(clazz, rules.CommitRule)
or issubclass(clazz, rules.ConfigurationRule)
):
if not issubclass(clazz, (rules.LineRule, rules.CommitRule, rules.ConfigurationRule)):
msg = (
f"{rule_type} rule class '{clazz.__name__}' "
f"must extend from {rules.CommitRule.__module__}.{rules.LineRule.__name__}, "
@ -142,17 +134,18 @@ def assert_valid_rule_class(clazz, rule_type="User-defined"): # pylint: disable
# Line/Commit rules must have a `validate` method
# We use isroutine() as it's both python 2 and 3 compatible. Details: http://stackoverflow.com/a/17019998/381010
if issubclass(clazz, rules.LineRule) or issubclass(clazz, rules.CommitRule):
if issubclass(clazz, (rules.LineRule, rules.CommitRule)):
if not hasattr(clazz, "validate") or not inspect.isroutine(clazz.validate):
raise rules.UserRuleError(f"{rule_type} rule class '{clazz.__name__}' must have a 'validate' method")
# Configuration rules must have an `apply` method
elif issubclass(clazz, rules.ConfigurationRule):
elif issubclass(clazz, rules.ConfigurationRule): # noqa: SIM102
if not hasattr(clazz, "apply") or not inspect.isroutine(clazz.apply):
msg = f"{rule_type} Configuration rule class '{clazz.__name__}' must have an 'apply' method"
raise rules.UserRuleError(msg)
# LineRules must have a valid target: rules.CommitMessageTitle or rules.CommitMessageBody
if issubclass(clazz, rules.LineRule):
if issubclass(clazz, rules.LineRule): # noqa: SIM102
if clazz.target not in [rules.CommitMessageTitle, rules.CommitMessageBody]:
msg = (
f"The target attribute of the {rule_type.lower()} LineRule class '{clazz.__name__}' "

View file

@ -1,11 +1,10 @@
# pylint: disable=inconsistent-return-statements
import copy
import logging
import re
from gitlint.options import IntOption, BoolOption, StrOption, ListOption, RegexOption
from gitlint.exception import GitlintError
from gitlint.deprecation import Deprecation
from gitlint.exception import GitlintError
from gitlint.options import BoolOption, IntOption, ListOption, RegexOption, StrOption
class Rule:
@ -50,40 +49,28 @@ class Rule:
class ConfigurationRule(Rule):
"""Class representing rules that can dynamically change the configuration of gitlint during runtime."""
pass
class CommitRule(Rule):
"""Class representing rules that act on an entire commit at once"""
pass
class LineRule(Rule):
"""Class representing rules that act on a line by line basis"""
pass
class LineRuleTarget:
"""Base class for LineRule targets. A LineRuleTarget specifies where a given rule will be applied
(e.g. commit message title, commit message body).
Each LineRule MUST have a target specified."""
pass
class CommitMessageTitle(LineRuleTarget):
"""Target class used for rules that apply to a commit message title"""
pass
class CommitMessageBody(LineRuleTarget):
"""Target class used for rules that apply to a commit message body"""
pass
class RuleViolation:
"""Class representing a violation of a rule. I.e.: When a rule is broken, the rule will instantiate this class
@ -107,8 +94,6 @@ class RuleViolation:
class UserRuleError(GitlintError):
"""Error used to indicate that an error occurred while trying to load a user rule"""
pass
class MaxLineLength(LineRule):
name = "max-line-length"
@ -305,7 +290,7 @@ class BodyMissing(CommitRule):
# ignore merges when option tells us to, which may have no body
if self.options["ignore-merge-commits"].value and commit.is_merge_commit:
return
if len(commit.message.body) < 2 or not "".join(commit.message.body).strip():
if len(commit.message.body) < 2 or not "".join(commit.message.body).strip(): # noqa: PLR2004 (Magic value)
return [RuleViolation(self.id, "Body message is missing", None, 3)]
@ -319,7 +304,7 @@ class BodyChangedFileMention(CommitRule):
for needs_mentioned_file in self.options["files"].value:
# if a file that we need to look out for is actually changed, then check whether it occurs
# in the commit msg body
if needs_mentioned_file in commit.changed_files:
if needs_mentioned_file in commit.changed_files: # noqa: SIM102
if needs_mentioned_file not in " ".join(commit.message.body):
violation_message = f"Body does not mention changed file '{needs_mentioned_file}'"
violations.append(RuleViolation(self.id, violation_message, None, len(commit.message.body) + 1))
@ -370,7 +355,7 @@ class AuthorValidEmail(CommitRule):
# We're replacing regex match with search semantics, see https://github.com/jorisroovers/gitlint/issues/254
# In case the user is using the default regex, we can silently change to using search
# If not, it depends on config (handled by Deprecation class)
if self.DEFAULT_AUTHOR_VALID_EMAIL_REGEX == self.options["regex"].value.pattern:
if self.options["regex"].value.pattern == self.DEFAULT_AUTHOR_VALID_EMAIL_REGEX:
regex_method = self.options["regex"].value.search
else:
regex_method = Deprecation.get_regex_method(self, self.options["regex"])
@ -458,7 +443,7 @@ class IgnoreBodyLines(ConfigurationRule):
new_body.append(line)
commit.message.body = new_body
commit.message.full = "\n".join([commit.message.title] + new_body)
commit.message.full = "\n".join([commit.message.title, *new_body])
class IgnoreByAuthorName(ConfigurationRule):
@ -474,6 +459,17 @@ class IgnoreByAuthorName(ConfigurationRule):
if not self.options["regex"].value:
return
# If commit.author_name is not available, log warning and return
if commit.author_name is None:
warning_msg = (
"%s - %s: skipping - commit.author_name unknown. "
"Suggested fix: Use the --staged flag (or set general.staged=True in .gitlint). "
"More details: https://jorisroovers.com/gitlint/configuration/#staged"
)
self.log.warning(warning_msg, self.name, self.id)
return
regex_method = Deprecation.get_regex_method(self, self.options["regex"])
if regex_method(commit.author_name):

View file

@ -5,7 +5,8 @@ capabilities wrt dealing with more edge-case environments on *nix systems that a
"""
import subprocess
from gitlint.utils import USE_SH_LIB, DEFAULT_ENCODING
from gitlint.utils import TERMINAL_ENCODING, USE_SH_LIB
def shell(cmd):
@ -15,17 +16,17 @@ def shell(cmd):
if USE_SH_LIB:
from sh import git # pylint: disable=unused-import,import-error
# import exceptions separately, this makes it a little easier to mock them out in the unit tests
from sh import CommandNotFound, ErrorReturnCode # pylint: disable=import-error
from sh import (
CommandNotFound,
ErrorReturnCode,
git,
)
else:
class CommandNotFound(Exception):
"""Exception indicating a command was not found during execution"""
pass
class ShResult:
"""Result wrapper class. We use this to more easily migrate from using https://amoffat.github.io/sh/ to using
the builtin subprocess module"""
@ -42,14 +43,12 @@ else:
class ErrorReturnCode(ShResult, Exception):
"""ShResult subclass for unexpected results (acts as an exception)."""
pass
def git(*command_parts, **kwargs):
"""Git shell wrapper.
Implemented as separate function here, so we can do a 'sh' style imports:
`from shell import git`
"""
args = ["git"] + list(command_parts)
args = ["git", *list(command_parts)]
return _exec(*args, **kwargs)
def _exec(*args, **kwargs):
@ -65,7 +64,7 @@ else:
raise CommandNotFound from e
exit_code = p.returncode
stdout = result[0].decode(DEFAULT_ENCODING)
stdout = result[0].decode(TERMINAL_ENCODING)
stderr = result[1] # 'sh' does not decode the stderr bytes to unicode
full_cmd = "" if args is None else " ".join(args)

View file

@ -5,15 +5,15 @@ import os
import re
import shutil
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from gitlint.config import LintConfig
from gitlint.deprecation import Deprecation, LOG as DEPRECATION_LOG
from gitlint.git import GitContext, GitChangedFileStats
from gitlint.utils import LOG_FORMAT, DEFAULT_ENCODING
from gitlint.deprecation import LOG as DEPRECATION_LOG
from gitlint.deprecation import Deprecation
from gitlint.git import GitChangedFileStats, GitContext
from gitlint.utils import FILE_ENCODING, LOG_FORMAT
EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING = (
"WARNING: gitlint.deprecated.regex_style_search {0} - {1}: gitlint will be switching from using "
@ -30,10 +30,28 @@ class BaseTestCase(unittest.TestCase):
# In case of assert failures, print the full error message
maxDiff = None
# Working directory in which tests in this class are executed
working_dir = None
# Originally working dir when the test was started
original_working_dir = None
SAMPLES_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "samples")
EXPECTED_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "expected")
GITLINT_USE_SH_LIB = os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]")
@classmethod
def setUpClass(cls):
# Run tests a temporary directory to shield them from any local git config
cls.original_working_dir = os.getcwd()
cls.working_dir = tempfile.mkdtemp()
os.chdir(cls.working_dir)
@classmethod
def tearDownClass(cls):
# Go back to original working dir and remove our temp working dir
os.chdir(cls.original_working_dir)
shutil.rmtree(cls.working_dir)
def setUp(self):
self.logcapture = LogCapture()
self.logcapture.setFormatter(logging.Formatter(LOG_FORMAT))
@ -77,9 +95,7 @@ class BaseTestCase(unittest.TestCase):
def get_sample(filename=""):
"""Read and return the contents of a file in gitlint/tests/samples"""
sample_path = BaseTestCase.get_sample_path(filename)
with open(sample_path, encoding=DEFAULT_ENCODING) as content:
sample = content.read()
return sample
return Path(sample_path).read_text(encoding=FILE_ENCODING)
@staticmethod
def patch_input(side_effect):
@ -93,8 +109,7 @@ class BaseTestCase(unittest.TestCase):
"""Utility method to read an expected file from gitlint/tests/expected and return it as a string.
Optionally replace template variables specified by variable_dict."""
expected_path = os.path.join(BaseTestCase.EXPECTED_DIR, filename)
with open(expected_path, encoding=DEFAULT_ENCODING) as content:
expected = content.read()
expected = Path(expected_path).read_text(encoding=FILE_ENCODING)
if variable_dict:
expected = expected.format(**variable_dict)
@ -150,22 +165,24 @@ class BaseTestCase(unittest.TestCase):
self.logcapture.clear()
@contextlib.contextmanager
def assertRaisesMessage(self, expected_exception, expected_msg): # pylint: disable=invalid-name
def assertRaisesMessage(self, expected_exception, expected_msg):
"""Asserts an exception has occurred with a given error message"""
try:
yield
except expected_exception as exc:
exception_msg = str(exc)
if exception_msg != expected_msg:
if exception_msg != expected_msg: # pragma: nocover
error = f"Right exception, wrong message:\n got: {exception_msg}\n expected: {expected_msg}"
raise self.fail(error)
raise self.fail(error) from exc
# else: everything is fine, just return
return
except Exception as exc:
raise self.fail(f"Expected '{expected_exception.__name__}' got '{exc.__class__.__name__}'")
except Exception as exc: # pragma: nocover
raise self.fail(f"Expected '{expected_exception.__name__}' got '{exc.__class__.__name__}'") from exc
# No exception raised while we expected one
raise self.fail(f"Expected to raise {expected_exception.__name__}, didn't get an exception at all")
raise self.fail(
f"Expected to raise {expected_exception.__name__}, didn't get an exception at all"
) # pragma: nocover
def object_equality_test(self, obj, attr_list, ctor_kwargs=None):
"""Helper function to easily implement object equality tests.

View file

@ -1,22 +1,15 @@
import io
import os
import sys
import platform
import arrow
import sys
from io import StringIO
from click.testing import CliRunner
from unittest.mock import patch
import arrow
from click.testing import CliRunner
from gitlint import __version__, cli
from gitlint.shell import CommandNotFound
from gitlint.tests.base import BaseTestCase
from gitlint import cli
from gitlint import __version__
from gitlint.utils import DEFAULT_ENCODING
from gitlint.utils import FILE_ENCODING, TERMINAL_ENCODING
class CLITests(BaseTestCase):
@ -46,7 +39,8 @@ class CLITests(BaseTestCase):
"gitlint_version": __version__,
"GITLINT_USE_SH_LIB": BaseTestCase.GITLINT_USE_SH_LIB,
"target": os.path.realpath(os.getcwd()),
"DEFAULT_ENCODING": DEFAULT_ENCODING,
"TERMINAL_ENCODING": TERMINAL_ENCODING,
"FILE_ENCODING": FILE_ENCODING,
}
def test_version(self):
@ -105,6 +99,40 @@ class CLITests(BaseTestCase):
self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_multiple_commits_1"))
self.assertEqual(result.exit_code, 3)
@patch("gitlint.cli.get_stdin_data", return_value=False)
@patch("gitlint.git.sh")
def test_lint_multiple_commits_csv(self, sh, _):
"""Test for --commits option"""
# fmt: off
sh.git.side_effect = [
"6f29bf81a8322a04071bb794666e48c443a90360\n", # git rev-list <SHA>
"25053ccec5e28e1bb8f7551fdbb5ab213ada2401\n",
"4da2656b0dadc76c7ee3fd0243a96cb64007f125\n",
# git log --pretty <FORMAT> <SHA>
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
"commït-title1\n\ncommït-body1",
"#", # git config --get core.commentchar
"3\t5\tcommit-1/file-1\n1\t4\tcommit-1/file-2\n", # git diff-tree
"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
# git log --pretty <FORMAT> <SHA>
"test åuthor2\x00test-email3@föo.com\x002016-12-04 15:28:15 +0100\x00åbc\n"
"commït-title2\n\ncommït-body2",
"8\t3\tcommit-2/file-1\n1\t5\tcommit-2/file-2\n", # git diff-tree
"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
# git log --pretty <FORMAT> <SHA>
"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00åbc\n"
"commït-title3\n\ncommït-body3",
"7\t2\tcommit-3/file-1\n1\t7\tcommit-3/file-2\n", # git diff-tree
"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
]
# fmt: on
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--commits", "6f29bf81,25053cce,4da2656b"])
self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_multiple_commits_csv_1"))
self.assertEqual(result.exit_code, 3)
@patch("gitlint.cli.get_stdin_data", return_value=False)
@patch("gitlint.git.sh")
def test_lint_multiple_commits_config(self, sh, _):
@ -225,8 +253,7 @@ class CLITests(BaseTestCase):
self.assertEqual(result.exit_code, 2)
@patch("gitlint.cli.get_stdin_data", return_value=False)
@patch("gitlint.git.sh")
def test_lint_commit_negative(self, sh, _):
def test_lint_commit_negative(self, _):
"""Negative test for --commit option"""
# Try using --commit and --commits at the same time (not allowed)
@ -298,6 +325,11 @@ class CLITests(BaseTestCase):
self.assertEqual(result.output, "")
expected_kwargs = self.get_system_info_dict()
changed_files_stats = (
f" {os.path.join('commit-1', 'file-1')}: 1 additions, 5 deletions\n"
f" {os.path.join('commit-1', 'file-2')}: 8 additions, 9 deletions"
)
expected_kwargs.update({"changed_files_stats": changed_files_stats})
expected_logs = self.get_expected("cli/test_cli/test_lint_staged_stdin_2", expected_kwargs)
self.assert_logged(expected_logs)
@ -318,7 +350,7 @@ class CLITests(BaseTestCase):
with self.tempdir() as tmpdir:
msg_filename = os.path.join(tmpdir, "msg")
with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f:
with open(msg_filename, "w", encoding=FILE_ENCODING) as f:
f.write("WIP: msg-filename tïtle\n")
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
@ -328,6 +360,11 @@ class CLITests(BaseTestCase):
self.assertEqual(result.output, "")
expected_kwargs = self.get_system_info_dict()
changed_files_stats = (
f" {os.path.join('commit-1', 'file-1')}: 3 additions, 4 deletions\n"
f" {os.path.join('commit-1', 'file-2')}: 4 additions, 7 deletions"
)
expected_kwargs.update({"changed_files_stats": changed_files_stats})
expected_logs = self.get_expected("cli/test_cli/test_lint_staged_msg_filename_2", expected_kwargs)
self.assert_logged(expected_logs)
@ -368,7 +405,7 @@ class CLITests(BaseTestCase):
with self.tempdir() as tmpdir:
msg_filename = os.path.join(tmpdir, "msg")
with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f:
with open(msg_filename, "w", encoding=FILE_ENCODING) as f:
f.write("Commït title\n")
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
@ -458,6 +495,25 @@ class CLITests(BaseTestCase):
self.assertEqual(result.exit_code, 6)
expected_kwargs = self.get_system_info_dict()
changed_files_stats1 = (
f" {os.path.join('commit-1', 'file-1')}: 5 additions, 8 deletions\n"
f" {os.path.join('commit-1', 'file-2')}: 2 additions, 9 deletions"
)
changed_files_stats2 = (
f" {os.path.join('commit-2', 'file-1')}: 5 additions, 8 deletions\n"
f" {os.path.join('commit-2', 'file-2')}: 7 additions, 9 deletions"
)
changed_files_stats3 = (
f" {os.path.join('commit-3', 'file-1')}: 1 additions, 4 deletions\n"
f" {os.path.join('commit-3', 'file-2')}: 3 additions, 4 deletions"
)
expected_kwargs.update(
{
"changed_files_stats1": changed_files_stats1,
"changed_files_stats2": changed_files_stats2,
"changed_files_stats3": changed_files_stats3,
}
)
expected_kwargs.update({"config_path": config_path})
expected_logs = self.get_expected("cli/test_cli/test_debug_1", expected_kwargs)
self.assert_logged(expected_logs)
@ -548,7 +604,7 @@ class CLITests(BaseTestCase):
# Non existing file
config_path = self.get_sample_path("föo")
result = self.cli.invoke(cli.cli, ["--config", config_path])
expected_string = f"Error: Invalid value for '-C' / '--config': File '{config_path}' does not exist."
expected_string = f"Error: Invalid value for '-C' / '--config': File {config_path!r} does not exist."
self.assertEqual(result.output.split("\n")[3], expected_string)
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
@ -569,7 +625,7 @@ class CLITests(BaseTestCase):
# Non existing file
config_path = self.get_sample_path("föo")
result = self.cli.invoke(cli.cli, env={"GITLINT_CONFIG": config_path})
expected_string = f"Error: Invalid value for '-C' / '--config': File '{config_path}' does not exist."
expected_string = f"Error: Invalid value for '-C' / '--config': File {config_path!r} does not exist."
self.assertEqual(result.output.split("\n")[3], expected_string)
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
@ -578,6 +634,11 @@ class CLITests(BaseTestCase):
result = self.cli.invoke(cli.cli, env={"GITLINT_CONFIG": config_path})
self.assertEqual(result.exit_code, self.CONFIG_ERROR_CODE)
def test_config_error(self):
result = self.cli.invoke(cli.cli, ["-c", "foo.bar=hur"])
self.assertEqual(result.output, "Config Error: No such rule 'foo'\n")
self.assertEqual(result.exit_code, self.CONFIG_ERROR_CODE)
@patch("gitlint.cli.get_stdin_data", return_value=False)
def test_target(self, _):
"""Test for the --target option"""
@ -602,7 +663,7 @@ class CLITests(BaseTestCase):
target_path = self.get_sample_path(os.path.join("config", "gitlintconfig"))
result = self.cli.invoke(cli.cli, ["--target", target_path])
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
expected_msg = f"Error: Invalid value for '--target': Directory '{target_path}' is a file."
expected_msg = f"Error: Invalid value for '--target': Directory {target_path!r} is a file."
self.assertEqual(result.output.split("\n")[3], expected_msg)
@patch("gitlint.config.LintConfigGenerator.generate_config")

View file

@ -1,18 +1,12 @@
import io
from io import StringIO
import os
from click.testing import CliRunner
from io import StringIO
from unittest.mock import patch
from gitlint.tests.base import BaseTestCase
from gitlint import cli
from gitlint import hooks
from gitlint import config
from click.testing import CliRunner
from gitlint import cli, config, hooks
from gitlint.shell import ErrorReturnCode
from gitlint.utils import DEFAULT_ENCODING
from gitlint.tests.base import BaseTestCase
from gitlint.utils import FILE_ENCODING
class CLIHookTests(BaseTestCase):
@ -108,7 +102,7 @@ class CLIHookTests(BaseTestCase):
with self.tempdir() as tmpdir:
msg_filename = os.path.join(tmpdir, "hür")
with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f:
with open(msg_filename, "w", encoding=FILE_ENCODING) as f:
f.write("WIP: tïtle\n")
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
@ -134,68 +128,65 @@ class CLIHookTests(BaseTestCase):
# When set_editors[i] == None, ensure we don't fallback to EDITOR set in shell invocating the tests
os.environ.pop("EDITOR", None)
with self.patch_input(["e", "e", "n"]):
with self.tempdir() as tmpdir:
msg_filename = os.path.realpath(os.path.join(tmpdir, "hür"))
with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f:
f.write(commit_messages[i] + "\n")
with self.patch_input(["e", "e", "n"]), self.tempdir() as tmpdir:
msg_filename = os.path.realpath(os.path.join(tmpdir, "hür"))
with open(msg_filename, "w", encoding=FILE_ENCODING) as f:
f.write(commit_messages[i] + "\n")
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"])
self.assertEqual(
result.output,
self.get_expected(
"cli/test_cli_hooks/test_hook_edit_1_stdout", {"commit_msg": commit_messages[i]}
),
)
expected = self.get_expected(
"cli/test_cli_hooks/test_hook_edit_1_stderr", {"commit_msg": commit_messages[i]}
)
self.assertEqual(stderr.getvalue(), expected)
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"])
self.assertEqual(
result.output,
self.get_expected(
"cli/test_cli_hooks/test_hook_edit_1_stdout", {"commit_msg": commit_messages[i]}
),
)
expected = self.get_expected(
"cli/test_cli_hooks/test_hook_edit_1_stderr", {"commit_msg": commit_messages[i]}
)
self.assertEqual(stderr.getvalue(), expected)
# exit code = number of violations
self.assertEqual(result.exit_code, 2)
# exit code = number of violations
self.assertEqual(result.exit_code, 2)
shell.assert_called_with(expected_editors[i] + " " + msg_filename)
self.assert_log_contains("DEBUG: gitlint.cli run-hook: editing commit message")
self.assert_log_contains(f"DEBUG: gitlint.cli run-hook: {expected_editors[i]} {msg_filename}")
shell.assert_called_with(expected_editors[i] + " " + msg_filename)
self.assert_log_contains("DEBUG: gitlint.cli run-hook: editing commit message")
self.assert_log_contains(f"DEBUG: gitlint.cli run-hook: {expected_editors[i]} {msg_filename}")
def test_run_hook_no(self):
"""Test for run-hook subcommand, answering 'n(o)' after commit-hook"""
with self.patch_input(["n"]):
with self.tempdir() as tmpdir:
msg_filename = os.path.join(tmpdir, "hür")
with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f:
f.write("WIP: höok no\n")
with self.patch_input(["n"]), self.tempdir() as tmpdir:
msg_filename = os.path.join(tmpdir, "hür")
with open(msg_filename, "w", encoding=FILE_ENCODING) as f:
f.write("WIP: höok no\n")
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"])
self.assertEqual(result.output, self.get_expected("cli/test_cli_hooks/test_hook_no_1_stdout"))
self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli_hooks/test_hook_no_1_stderr"))
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"])
self.assertEqual(result.output, self.get_expected("cli/test_cli_hooks/test_hook_no_1_stdout"))
self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli_hooks/test_hook_no_1_stderr"))
# We decided not to keep the commit message: hook returns number of violations (>0)
# This will cause git to abort the commit
self.assertEqual(result.exit_code, 2)
self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message declined")
# We decided not to keep the commit message: hook returns number of violations (>0)
# This will cause git to abort the commit
self.assertEqual(result.exit_code, 2)
self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message declined")
def test_run_hook_yes(self):
"""Test for run-hook subcommand, answering 'y(es)' after commit-hook"""
with self.patch_input(["y"]):
with self.tempdir() as tmpdir:
msg_filename = os.path.join(tmpdir, "hür")
with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f:
f.write("WIP: höok yes\n")
with self.patch_input(["y"]), self.tempdir() as tmpdir:
msg_filename = os.path.join(tmpdir, "hür")
with open(msg_filename, "w", encoding=FILE_ENCODING) as f:
f.write("WIP: höok yes\n")
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"])
self.assertEqual(result.output, self.get_expected("cli/test_cli_hooks/test_hook_yes_1_stdout"))
self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli_hooks/test_hook_yes_1_stderr"))
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"])
self.assertEqual(result.output, self.get_expected("cli/test_cli_hooks/test_hook_yes_1_stdout"))
self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli_hooks/test_hook_yes_1_stderr"))
# Exit code is 0 because we decide to keep the commit message
# This will cause git to keep the commit
self.assertEqual(result.exit_code, 0)
self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message accepted")
# Exit code is 0 because we decide to keep the commit message
# This will cause git to keep the commit
self.assertEqual(result.exit_code, 0)
self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message accepted")
@patch("gitlint.cli.get_stdin_data", return_value=False)
@patch("gitlint.git.sh")
@ -207,7 +198,8 @@ class CLIHookTests(BaseTestCase):
error_msg = b"fatal: not a git repository (or any of the parent directories): .git"
sh.git.side_effect = ErrorReturnCode("full command", b"stdout", error_msg)
result = self.cli.invoke(cli.cli, ["run-hook"])
expected = self.get_expected("cli/test_cli_hooks/test_run_hook_negative_1", {"git_repo": os.getcwd()})
expected_kwargs = {"git_repo": os.path.realpath(os.getcwd())}
expected = self.get_expected("cli/test_cli_hooks/test_run_hook_negative_1", expected_kwargs)
self.assertEqual(result.output, expected)
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
@ -276,11 +268,10 @@ class CLIHookTests(BaseTestCase):
"commit-1-branch-1\ncommit-1-branch-2\n",
]
with self.patch_input(["e"]):
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["run-hook"])
expected = self.get_expected("cli/test_cli_hooks/test_hook_local_commit_1_stderr")
self.assertEqual(stderr.getvalue(), expected)
self.assertEqual(result.output, self.get_expected("cli/test_cli_hooks/test_hook_local_commit_1_stdout"))
# If we can't edit the message, run-hook follows regular gitlint behavior and exit code = # violations
self.assertEqual(result.exit_code, 2)
with self.patch_input(["e"]), patch("gitlint.display.stderr", new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["run-hook"])
expected = self.get_expected("cli/test_cli_hooks/test_hook_local_commit_1_stderr")
self.assertEqual(stderr.getvalue(), expected)
self.assertEqual(result.output, self.get_expected("cli/test_cli_hooks/test_hook_local_commit_1_stdout"))
# If we can't edit the message, run-hook follows regular gitlint behavior and exit code = # violations
self.assertEqual(result.exit_code, 2)

View file

@ -1,8 +1,12 @@
from unittest.mock import patch
from gitlint import rules
from gitlint.config import LintConfig, LintConfigError, LintConfigGenerator, GITLINT_CONFIG_TEMPLATE_SRC_PATH
from gitlint import options
from gitlint import options, rules
from gitlint.config import (
GITLINT_CONFIG_TEMPLATE_SRC_PATH,
LintConfig,
LintConfigError,
LintConfigGenerator,
)
from gitlint.tests.base import BaseTestCase
@ -166,7 +170,7 @@ class LintConfigTests(BaseTestCase):
# UserRuleError, RuleOptionError should be re-raised as LintConfigErrors
side_effects = [rules.UserRuleError("üser-rule"), options.RuleOptionError("rüle-option")]
for side_effect in side_effects:
with patch("gitlint.config.rule_finder.find_rule_classes", side_effect=side_effect):
with patch("gitlint.config.rule_finder.find_rule_classes", side_effect=side_effect): # noqa: SIM117
with self.assertRaisesMessage(LintConfigError, str(side_effect)):
config.contrib = "contrib-title-conventional-commits"

View file

@ -1,10 +1,8 @@
import copy
from gitlint.tests.base import BaseTestCase
from gitlint.config import LintConfig, LintConfigBuilder, LintConfigError
from gitlint import rules
from gitlint.config import LintConfig, LintConfigBuilder, LintConfigError
from gitlint.tests.base import BaseTestCase
class LintConfigBuilderTests(BaseTestCase):
@ -256,8 +254,7 @@ class LintConfigBuilderTests(BaseTestCase):
my_rule.options["regex"].set("wrong")
def test_named_rules_negative(self):
# T7 = title-match-regex
# Invalid rule name
# Invalid rule name (T7 = title-match-regex)
for invalid_name in ["", " ", " ", "\t", "\n", "å b", "å:b", "åb:", ":åb"]:
config_builder = LintConfigBuilder()
config_builder.set_option(f"T7:{invalid_name}", "regex", "tëst")

View file

@ -1,12 +1,10 @@
from io import StringIO
from click.testing import CliRunner
from unittest.mock import patch
from gitlint.tests.base import BaseTestCase
from click.testing import CliRunner
from gitlint import cli
from gitlint.config import LintConfigBuilder
from gitlint.tests.base import BaseTestCase
class LintConfigPrecedenceTests(BaseTestCase):

View file

@ -1,4 +1,5 @@
from collections import OrderedDict
from gitlint import rules
from gitlint.config import RuleCollection
from gitlint.tests.base import BaseTestCase

View file

@ -1,10 +1,10 @@
from collections import namedtuple
from unittest.mock import patch
from gitlint.tests.base import BaseTestCase
from gitlint.rules import RuleViolation
from gitlint.config import LintConfig
from gitlint.config import LintConfig
from gitlint.contrib.rules.authors_commit import AllowedAuthors
from gitlint.rules import RuleViolation
from gitlint.tests.base import BaseTestCase
class ContribAuthorsCommitTests(BaseTestCase):
@ -101,6 +101,5 @@ class ContribAuthorsCommitTests(BaseTestCase):
return_value=False,
)
def test_read_authors_file_missing_file(self, _mock_iterdir):
with self.assertRaises(FileNotFoundError) as err:
with self.assertRaisesMessage(FileNotFoundError, "No AUTHORS file found!"):
AllowedAuthors._read_authors_from_file(self.gitcontext)
self.assertEqual(err.exception.args[0], "AUTHORS file not found")

View file

@ -1,7 +1,7 @@
from gitlint.tests.base import BaseTestCase
from gitlint.rules import RuleViolation
from gitlint.contrib.rules.conventional_commit import ConventionalCommit
from gitlint.config import LintConfig
from gitlint.contrib.rules.conventional_commit import ConventionalCommit
from gitlint.rules import RuleViolation
from gitlint.tests.base import BaseTestCase
class ContribConventionalCommitTests(BaseTestCase):

View file

@ -1,8 +1,7 @@
from gitlint.tests.base import BaseTestCase
from gitlint.rules import RuleViolation
from gitlint.contrib.rules.disallow_cleanup_commits import DisallowCleanupCommits
from gitlint.config import LintConfig
from gitlint.contrib.rules.disallow_cleanup_commits import DisallowCleanupCommits
from gitlint.rules import RuleViolation
from gitlint.tests.base import BaseTestCase
class ContribDisallowCleanupCommitsTest(BaseTestCase):

View file

@ -1,8 +1,7 @@
from gitlint.tests.base import BaseTestCase
from gitlint.rules import RuleViolation
from gitlint.contrib.rules.signedoff_by import SignedOffBy
from gitlint.config import LintConfig
from gitlint.contrib.rules.signedoff_by import SignedOffBy
from gitlint.rules import RuleViolation
from gitlint.tests.base import BaseTestCase
class ContribSignedOffByTests(BaseTestCase):

View file

@ -1,9 +1,9 @@
import os
from gitlint.tests.base import BaseTestCase
from gitlint.contrib import rules as contrib_rules
from gitlint.tests.contrib import rules as contrib_tests
from gitlint import rule_finder, rules
from gitlint.contrib import rules as contrib_rules
from gitlint.tests.base import BaseTestCase
from gitlint.tests.contrib import rules as contrib_tests
class ContribRuleTests(BaseTestCase):

View file

@ -4,7 +4,8 @@ DEBUG: gitlint.cli Python version: {python_version}
DEBUG: gitlint.cli Git version: git version 1.2.3
DEBUG: gitlint.cli Gitlint version: {gitlint_version}
DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli TERMINAL_ENCODING: {TERMINAL_ENCODING}
DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING}
DEBUG: gitlint.cli Configuration
config-path: {config_path}
[GENERAL]
@ -88,8 +89,7 @@ Parents: ['a123']
Branches: ['commit-1-branch-1', 'commit-1-branch-2']
Changed Files: ['commit-1/file-1', 'commit-1/file-2']
Changed Files Stats:
commit-1/file-1: 5 additions, 8 deletions
commit-1/file-2: 2 additions, 9 deletions
{changed_files_stats1}
-----------------------
DEBUG: gitlint.git ('log', '25053ccec5e28e1bb8f7551fdbb5ab213ada2401', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B')
DEBUG: gitlint.lint Linting commit 25053ccec5e28e1bb8f7551fdbb5ab213ada2401
@ -112,8 +112,7 @@ Parents: ['b123']
Branches: ['commit-2-branch-1', 'commit-2-branch-2']
Changed Files: ['commit-2/file-1', 'commit-2/file-2']
Changed Files Stats:
commit-2/file-1: 5 additions, 8 deletions
commit-2/file-2: 7 additions, 9 deletions
{changed_files_stats2}
-----------------------
DEBUG: gitlint.git ('log', '4da2656b0dadc76c7ee3fd0243a96cb64007f125', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B')
DEBUG: gitlint.lint Linting commit 4da2656b0dadc76c7ee3fd0243a96cb64007f125
@ -135,7 +134,6 @@ Parents: ['c123']
Branches: ['commit-3-branch-1', 'commit-3-branch-2']
Changed Files: ['commit-3/file-1', 'commit-3/file-2']
Changed Files Stats:
commit-3/file-1: 1 additions, 4 deletions
commit-3/file-2: 3 additions, 4 deletions
{changed_files_stats3}
-----------------------
DEBUG: gitlint.cli Exit Code = 6

View file

@ -4,7 +4,8 @@ DEBUG: gitlint.cli Python version: {python_version}
DEBUG: gitlint.cli Git version: git version 1.2.3
DEBUG: gitlint.cli Gitlint version: {gitlint_version}
DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli TERMINAL_ENCODING: {TERMINAL_ENCODING}
DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING}
DEBUG: gitlint.cli Configuration
config-path: None
[GENERAL]

View file

@ -0,0 +1,8 @@
Commit 6f29bf81a8:
3: B5 Body message is too short (12<20): "commït-body1"
Commit 25053ccec5:
3: B5 Body message is too short (12<20): "commït-body2"
Commit 4da2656b0d:
3: B5 Body message is too short (12<20): "commït-body3"

View file

@ -4,7 +4,8 @@ DEBUG: gitlint.cli Python version: {python_version}
DEBUG: gitlint.cli Git version: git version 1.2.3
DEBUG: gitlint.cli Gitlint version: {gitlint_version}
DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli TERMINAL_ENCODING: {TERMINAL_ENCODING}
DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING}
DEBUG: gitlint.cli Configuration
config-path: None
[GENERAL]
@ -87,7 +88,6 @@ Parents: []
Branches: ['my-branch']
Changed Files: ['commit-1/file-1', 'commit-1/file-2']
Changed Files Stats:
commit-1/file-1: 3 additions, 4 deletions
commit-1/file-2: 4 additions, 7 deletions
{changed_files_stats}
-----------------------
DEBUG: gitlint.cli Exit Code = 2

View file

@ -4,7 +4,8 @@ DEBUG: gitlint.cli Python version: {python_version}
DEBUG: gitlint.cli Git version: git version 1.2.3
DEBUG: gitlint.cli Gitlint version: {gitlint_version}
DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli TERMINAL_ENCODING: {TERMINAL_ENCODING}
DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING}
DEBUG: gitlint.cli Configuration
config-path: None
[GENERAL]
@ -89,7 +90,6 @@ Parents: []
Branches: ['my-branch']
Changed Files: ['commit-1/file-1', 'commit-1/file-2']
Changed Files Stats:
commit-1/file-1: 1 additions, 5 deletions
commit-1/file-2: 8 additions, 9 deletions
{changed_files_stats}
-----------------------
DEBUG: gitlint.cli Exit Code = 3

View file

@ -4,7 +4,8 @@ DEBUG: gitlint.cli Python version: {python_version}
DEBUG: gitlint.cli Git version: git version 1.2.3
DEBUG: gitlint.cli Gitlint version: {gitlint_version}
DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli TERMINAL_ENCODING: {TERMINAL_ENCODING}
DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING}
DEBUG: gitlint.cli Configuration
config-path: {config_path}
[GENERAL]

View file

@ -1,11 +1,15 @@
import os
from unittest.mock import call, patch
from unittest.mock import patch, call
from gitlint.shell import ErrorReturnCode, CommandNotFound
from gitlint.git import (
GitContext,
GitContextError,
GitNotInstalledError,
git_commentchar,
git_hooks_dir,
)
from gitlint.shell import CommandNotFound, ErrorReturnCode
from gitlint.tests.base import BaseTestCase
from gitlint.git import GitContext, GitContextError, GitNotInstalledError, git_commentchar, git_hooks_dir
class GitTests(BaseTestCase):

View file

@ -1,25 +1,21 @@
import copy
import datetime
from pathlib import Path
import dateutil
from unittest.mock import call, patch
import arrow
from unittest.mock import patch, call
from gitlint.tests.base import BaseTestCase
import dateutil
from gitlint.git import (
GitChangedFileStats,
GitContext,
GitCommit,
GitCommitMessage,
GitContext,
GitContextError,
LocalGitCommit,
StagedLocalGitCommit,
GitCommitMessage,
GitChangedFileStats,
)
from gitlint.shell import ErrorReturnCode
from gitlint.tests.base import BaseTestCase
class GitCommitTests(BaseTestCase):
@ -383,7 +379,7 @@ class GitCommitTests(BaseTestCase):
@patch("gitlint.git.sh")
def test_get_latest_commit_fixup_squash_commit(self, sh):
commit_prefixes = {"fixup": "is_fixup_commit", "squash": "is_squash_commit", "amend": "is_fixup_amend_commit"}
for commit_type in commit_prefixes.keys():
for commit_type in commit_prefixes:
sample_sha = "d8ac47e9f2923c7f22d8668e3a1ed04eb4cdbca9"
sh.git.side_effect = [
@ -616,7 +612,7 @@ class GitCommitTests(BaseTestCase):
# mapping between cleanup commit prefixes and the commit object attribute
commit_prefixes = {"fixup": "is_fixup_commit", "squash": "is_squash_commit", "amend": "is_fixup_amend_commit"}
for commit_type in commit_prefixes.keys():
for commit_type in commit_prefixes:
commit_msg = f"{commit_type}! Test message"
gitcontext = GitContext.from_commit_msg(commit_msg)
commit = gitcontext.commits[-1]
@ -642,7 +638,7 @@ class GitCommitTests(BaseTestCase):
@patch("gitlint.git.sh")
@patch("arrow.now")
def test_staged_commit(self, now, sh):
# StagedLocalGitCommit()
"""Test for StagedLocalGitCommit()"""
sh.git.side_effect = [
"#", # git config --get core.commentchar
@ -744,7 +740,7 @@ class GitCommitTests(BaseTestCase):
git.return_value = "foöbar"
# Test simple equality case
now = datetime.datetime.utcnow()
now = datetime.datetime.now(datetime.timezone.utc)
context1 = GitContext()
commit_message1 = GitCommitMessage(context1, "tëst\n\nfoo", "tëst\n\nfoo", "tēst", ["", "föo"])
commit1 = GitCommit(

View file

@ -1,7 +1,7 @@
from unittest.mock import patch, call
from unittest.mock import call, patch
from gitlint.tests.base import BaseTestCase
from gitlint.git import GitContext
from gitlint.tests.base import BaseTestCase
class GitContextTests(BaseTestCase):

View file

@ -1,5 +1,5 @@
from gitlint.tests.base import BaseTestCase
from gitlint import rules
from gitlint.tests.base import BaseTestCase
class BodyRuleTests(BaseTestCase):
@ -100,13 +100,13 @@ class BodyRuleTests(BaseTestCase):
expected_violation = rules.RuleViolation("B5", "Body message is too short (21<120)", "å" * 21, 3)
rule = rules.BodyMinLength({"min-length": 120})
commit = self.gitcommit("Title\n\n{}\n".format("å" * 21)) # pylint: disable=consider-using-f-string
commit = self.gitcommit("Title\n\n{}\n".format("å" * 21))
violations = rule.validate(commit)
self.assertListEqual(violations, [expected_violation])
# Make sure we don't get the error if the body-length is exactly the min-length
rule = rules.BodyMinLength({"min-length": 8})
commit = self.gitcommit("Tïtle\n\n{}\n".format("å" * 8)) # pylint: disable=consider-using-f-string
commit = self.gitcommit("Tïtle\n\n{}\n".format("å" * 8))
violations = rule.validate(commit)
self.assertIsNone(violations)

View file

@ -1,6 +1,9 @@
from gitlint.tests.base import BaseTestCase, EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING
from gitlint import rules
from gitlint.config import LintConfig
from gitlint.tests.base import (
EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING,
BaseTestCase,
)
class ConfigurationRuleTests(BaseTestCase):
@ -89,6 +92,25 @@ class ConfigurationRuleTests(BaseTestCase):
self.assertEqual(config, LintConfig())
self.assert_logged([]) # nothing logged -> nothing ignored
# No author available -> rule is skipped and warning logged
staged_commit = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line")
rule = rules.IgnoreByAuthorName({"regex": "foo"})
config = LintConfig()
rule.apply(config, staged_commit)
self.assertEqual(config, LintConfig())
expected_log_messages = [
"WARNING: gitlint.rules ignore-by-author-name - I4: skipping - commit.author_name unknown. "
"Suggested fix: Use the --staged flag (or set general.staged=True in .gitlint). "
"More details: https://jorisroovers.com/gitlint/configuration/#staged"
]
self.assert_logged(expected_log_messages)
# Non-Matching regex -> expect config to stay the same
rule = rules.IgnoreByAuthorName({"regex": "foo"})
expected_config = LintConfig()
rule.apply(config, commit)
self.assertEqual(config, LintConfig())
# Matching regex -> expect config to ignore all rules
rule = rules.IgnoreByAuthorName({"regex": "(.*)ëst(.*)"})
expected_config = LintConfig()
@ -96,7 +118,7 @@ class ConfigurationRuleTests(BaseTestCase):
rule.apply(config, commit)
self.assertEqual(config, expected_config)
expected_log_messages = [
expected_log_messages += [
EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING.format("I4", "ignore-by-author-name"),
"DEBUG: gitlint.rules Ignoring commit because of rule 'I4': "
"Commit Author Name 'Tëst nåme' matches the regex '(.*)ëst(.*)',"

View file

@ -1,5 +1,8 @@
from gitlint.tests.base import BaseTestCase, EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING
from gitlint.rules import AuthorValidEmail, RuleViolation
from gitlint.tests.base import (
EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING,
BaseTestCase,
)
class MetaRuleTests(BaseTestCase):

View file

@ -1,8 +1,12 @@
from gitlint.tests.base import BaseTestCase
from gitlint.rules import Rule, RuleViolation
from gitlint.tests.base import BaseTestCase
class RuleTests(BaseTestCase):
def test_ruleviolation__str__(self):
expected = '57: rule-ïd Tēst message: "Tēst content"'
self.assertEqual(str(RuleViolation("rule-ïd", "Tēst message", "Tēst content", 57)), expected)
def test_rule_equality(self):
self.assertEqual(Rule(), Rule())
# Ensure rules are not equal if they differ on their attributes
@ -13,9 +17,16 @@ class RuleTests(BaseTestCase):
def test_rule_log(self):
rule = Rule()
self.assertIsNone(rule._log)
rule.log.debug("Tēst message")
self.assert_log_contains("DEBUG: gitlint.rules Tēst message")
# Assert the same logger is reused when logging multiple messages
log = rule._log
rule.log.debug("Anöther message")
self.assertEqual(log, rule._log)
self.assert_log_contains("DEBUG: gitlint.rules Anöther message")
def test_rule_violation_equality(self):
violation1 = RuleViolation("ïd1", "My messåge", "My cöntent", 1)
self.object_equality_test(violation1, ["rule_id", "message", "content", "line_nr"])

View file

@ -1,15 +1,15 @@
from gitlint.tests.base import BaseTestCase
from gitlint.rules import (
TitleMaxLength,
TitleTrailingWhitespace,
TitleHardTab,
TitleMustNotContainWord,
TitleTrailingPunctuation,
TitleLeadingWhitespace,
TitleRegexMatches,
RuleViolation,
TitleHardTab,
TitleLeadingWhitespace,
TitleMaxLength,
TitleMinLength,
TitleMustNotContainWord,
TitleRegexMatches,
TitleTrailingPunctuation,
TitleTrailingWhitespace,
)
from gitlint.tests.base import BaseTestCase
class TitleRuleTests(BaseTestCase):

View file

@ -1,11 +1,10 @@
import os
import sys
from gitlint.tests.base import BaseTestCase
from gitlint.rule_finder import find_rule_classes, assert_valid_rule_class
from gitlint.rules import UserRuleError
from gitlint import options, rules
from gitlint.rule_finder import assert_valid_rule_class, find_rule_classes
from gitlint.rules import UserRuleError
from gitlint.tests.base import BaseTestCase
class UserRuleTests(BaseTestCase):
@ -104,21 +103,21 @@ class UserRuleTests(BaseTestCase):
target = rules.CommitMessageTitle
def validate(self):
pass
pass # pragma: nocover
class MyCommitRuleClass(rules.CommitRule):
id = "UC2"
name = "my-cömmit-rule"
def validate(self):
pass
pass # pragma: nocover
class MyConfigurationRuleClass(rules.ConfigurationRule):
id = "UC3"
name = "my-cönfiguration-rule"
def apply(self):
pass
pass # pragma: nocover
# Just assert that no error is raised
self.assertIsNone(assert_valid_rule_class(MyLineRuleClass))
@ -203,7 +202,7 @@ class UserRuleTests(BaseTestCase):
assert_valid_rule_class(MyRuleClass)
# option_spec is a list, but not of gitlint options
MyRuleClass.options_spec = ["föo", 123] # pylint: disable=bad-option-value,redefined-variable-type
MyRuleClass.options_spec = ["föo", 123]
with self.assertRaisesMessage(UserRuleError, expected_msg):
assert_valid_rule_class(MyRuleClass)
@ -236,8 +235,8 @@ class UserRuleTests(BaseTestCase):
with self.assertRaisesMessage(UserRuleError, expected_msg):
assert_valid_rule_class(MyRuleClass)
# validate attribute - not a method
MyRuleClass.validate = "föo"
# apply attribute - not a method
MyRuleClass.apply = "föo"
with self.assertRaisesMessage(UserRuleError, expected_msg):
assert_valid_rule_class(MyRuleClass)
@ -247,7 +246,7 @@ class UserRuleTests(BaseTestCase):
name = "my-rüle-class"
def validate(self):
pass
pass # pragma: nocover
# no target
expected_msg = (
@ -263,5 +262,5 @@ class UserRuleTests(BaseTestCase):
assert_valid_rule_class(MyRuleClass)
# valid target, no exception should be raised
MyRuleClass.target = rules.CommitMessageTitle # pylint: disable=bad-option-value,redefined-variable-type
MyRuleClass.target = rules.CommitMessageTitle
self.assertIsNone(assert_valid_rule_class(MyRuleClass))

View file

@ -1,5 +1,5 @@
from gitlint.rules import CommitRule, RuleViolation
from gitlint.options import IntOption
from gitlint.rules import CommitRule, RuleViolation
class MyUserCommitRule(CommitRule):
@ -19,7 +19,7 @@ class MyUserCommitRule(CommitRule):
def func_should_be_ignored():
pass
pass # pragma: nocover
global_variable_should_be_ignored = True

View file

@ -9,4 +9,4 @@ class InitFileRule(CommitRule):
options_spec = []
def validate(self, _commit):
return []
return [] # pragma: nocover

View file

@ -1,5 +1,5 @@
from gitlint.tests.base import BaseTestCase
from gitlint.cache import PropertyCache, cache
from gitlint.tests.base import BaseTestCase
class CacheTests(BaseTestCase):

View file

@ -1,7 +1,10 @@
from gitlint.config import LintConfig
from gitlint.deprecation import Deprecation
from gitlint.rules import IgnoreByTitle
from gitlint.tests.base import EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING, BaseTestCase
from gitlint.tests.base import (
EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING,
BaseTestCase,
)
class DeprecationTests(BaseTestCase):

View file

@ -1,9 +1,8 @@
from io import StringIO
from unittest.mock import patch
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
from gitlint.display import Display
from gitlint.config import LintConfig
from gitlint.display import Display
from gitlint.tests.base import BaseTestCase

View file

@ -1,16 +1,15 @@
import os
from unittest.mock import ANY, mock_open, patch
from unittest.mock import patch, ANY, mock_open
from gitlint.tests.base import BaseTestCase
from gitlint.config import LintConfig
from gitlint.hooks import (
COMMIT_MSG_HOOK_DST_PATH,
COMMIT_MSG_HOOK_SRC_PATH,
GITLINT_HOOK_IDENTIFIER,
GitHookInstaller,
GitHookInstallerError,
COMMIT_MSG_HOOK_SRC_PATH,
COMMIT_MSG_HOOK_DST_PATH,
GITLINT_HOOK_IDENTIFIER,
)
from gitlint.tests.base import BaseTestCase
class HookTests(BaseTestCase):
@ -58,9 +57,10 @@ class HookTests(BaseTestCase):
expected_msg = f"{lint_config.target} is not a git repository."
with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
GitHookInstaller.install_commit_msg_hook(lint_config)
isdir.assert_called_with(git_hooks_dir.return_value)
path_exists.assert_not_called()
copy.assert_not_called()
isdir.assert_called_with(git_hooks_dir.return_value)
path_exists.assert_not_called()
copy.assert_not_called()
# mock that there is already a commit hook present
isdir.return_value = True
@ -106,9 +106,10 @@ class HookTests(BaseTestCase):
expected_msg = f"{lint_config.target} is not a git repository."
with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
GitHookInstaller.uninstall_commit_msg_hook(lint_config)
isdir.assert_called_with(git_hooks_dir.return_value)
path_exists.assert_not_called()
remove.assert_not_called()
isdir.assert_called_with(git_hooks_dir.return_value)
path_exists.assert_not_called()
remove.assert_not_called()
# mock that there is no commit hook present
isdir.return_value = True
@ -117,9 +118,10 @@ class HookTests(BaseTestCase):
expected_msg = f"There is no commit-msg hook present in {expected_dst}."
with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
GitHookInstaller.uninstall_commit_msg_hook(lint_config)
isdir.assert_called_with(git_hooks_dir.return_value)
path_exists.assert_called_once_with(expected_dst)
remove.assert_not_called()
isdir.assert_called_with(git_hooks_dir.return_value)
path_exists.assert_called_once_with(expected_dst)
remove.assert_not_called()
# mock that there is a different (=not gitlint) commit hook
isdir.return_value = True

View file

@ -1,11 +1,10 @@
from io import StringIO
from unittest.mock import patch
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
from gitlint.tests.base import BaseTestCase
from gitlint.config import LintConfig, LintConfigBuilder
from gitlint.lint import GitLinter
from gitlint.rules import RuleViolation, TitleMustNotContainWord
from gitlint.config import LintConfig, LintConfigBuilder
from gitlint.tests.base import BaseTestCase
class LintTests(BaseTestCase):

View file

@ -1,12 +1,23 @@
import os
import re
from gitlint.options import (
BoolOption,
IntOption,
ListOption,
PathOption,
RegexOption,
RuleOptionError,
StrOption,
)
from gitlint.tests.base import BaseTestCase
from gitlint.options import IntOption, BoolOption, StrOption, ListOption, PathOption, RegexOption, RuleOptionError
class RuleOptionTests(BaseTestCase):
def test_option__str__(self):
option = StrOption("tëst-option", "åbc", "Test Dëscription")
self.assertEqual(str(option), "(tëst-option: åbc (Test Dëscription))")
def test_option_equality(self):
options = {
IntOption: 123,
@ -158,7 +169,7 @@ class RuleOptionTests(BaseTestCase):
option = PathOption("tëst-directory", ".", "Tëst Description", type="dir")
self.assertEqual(option.name, "tëst-directory")
self.assertEqual(option.description, "Tëst Description")
self.assertEqual(option.value, os.getcwd())
self.assertEqual(option.value, os.path.realpath("."))
self.assertEqual(option.type, "dir")
# re-set value

View file

@ -27,7 +27,7 @@ class UtilsTests(BaseTestCase):
self.assertEqual(utils.use_sh_library(), False)
@patch("gitlint.utils.locale")
def test_default_encoding_non_windows(self, mocked_locale):
def test_terminal_encoding_non_windows(self, mocked_locale):
utils.PLATFORM_IS_WINDOWS = False
mocked_locale.getpreferredencoding.return_value = "foöbar"
self.assertEqual(utils.getpreferredencoding(), "foöbar")
@ -37,7 +37,7 @@ class UtilsTests(BaseTestCase):
self.assertEqual(utils.getpreferredencoding(), "UTF-8")
@patch("os.environ")
def test_default_encoding_windows(self, patched_env):
def test_terminal_encoding_windows(self, patched_env):
utils.PLATFORM_IS_WINDOWS = True
# Mock out os.environ
mock_env = {}

View file

@ -1,9 +1,7 @@
# pylint: disable=bad-option-value,unidiomatic-typecheck,undefined-variable,no-else-return
import codecs
import platform
import os
import locale
import os
import platform
# Note: While we can easily inline the logic related to the constants set in this module, we deliberately create
# small functions that encapsulate that logic as this enables easy unit testing. In particular, by creating functions
@ -40,30 +38,28 @@ def use_sh_library():
USE_SH_LIB = use_sh_library()
########################################################################################################################
# DEFAULT_ENCODING
# TERMINAL_ENCODING
# Encoding used for terminal encoding/decoding.
def getpreferredencoding():
"""Modified version of local.getpreferredencoding() that takes into account LC_ALL, LC_CTYPE, LANG env vars
on windows and falls back to UTF-8."""
fallback_encoding = "UTF-8"
default_encoding = locale.getpreferredencoding() or fallback_encoding
preferred_encoding = locale.getpreferredencoding() or fallback_encoding
# On Windows, we mimic git/linux by trying to read the LC_ALL, LC_CTYPE, LANG env vars manually
# (on Linux/MacOS the `getpreferredencoding()` call will take care of this).
# We fallback to UTF-8
if PLATFORM_IS_WINDOWS:
default_encoding = fallback_encoding
preferred_encoding = fallback_encoding
for env_var in ["LC_ALL", "LC_CTYPE", "LANG"]:
encoding = os.environ.get(env_var, False)
if encoding:
# Support dotted (C.UTF-8) and non-dotted (C or UTF-8) charsets:
# If encoding contains a dot: split and use second part, otherwise use everything
dot_index = encoding.find(".")
if dot_index != -1:
default_encoding = encoding[dot_index + 1 :]
else:
default_encoding = encoding
preferred_encoding = encoding[dot_index + 1 :] if dot_index != -1 else encoding
break
# We've determined what encoding the user *wants*, let's now check if it's actually a valid encoding on the
@ -71,11 +67,21 @@ def getpreferredencoding():
# This scenario is fairly common on Windows where git sets LC_CTYPE=C when invoking the commit-msg hook, which
# is not a valid encoding in Python on Windows.
try:
codecs.lookup(default_encoding) # pylint: disable=no-member
codecs.lookup(preferred_encoding)
except LookupError:
default_encoding = fallback_encoding
preferred_encoding = fallback_encoding
return default_encoding
return preferred_encoding
DEFAULT_ENCODING = getpreferredencoding()
TERMINAL_ENCODING = getpreferredencoding()
########################################################################################################################
# FILE_ENCODING
# Gitlint assumes UTF-8 encoding for all file operations:
# - reading/writing its own hook and config files
# - reading/writing git commit messages
# Git does have i18n.commitEncoding and i18n.logOutputEncoding options which we might want to take into account,
# but that's not supported today.
FILE_ENCODING = "UTF-8"

View file

@ -0,0 +1,71 @@
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
name = "gitlint-core"
dynamic = ["version", "urls"]
description = "Git commit message linter written in python, checks your commit messages for style."
readme = "README.md"
license = "MIT"
requires-python = ">=3.7"
authors = [{ name = "Joris Roovers" }]
keywords = [
"git",
"gitlint",
"lint", #
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Software Development :: Quality Assurance",
"Topic :: Software Development :: Testing",
]
dependencies = [
"arrow>=1",
"Click>=8",
"importlib-metadata >= 1.0 ; python_version < \"3.8\"",
"sh>=1.13.0 ; sys_platform != \"win32\"",
]
[project.optional-dependencies]
trusted-deps = [
"arrow==1.2.3",
"Click==8.1.3",
"sh==1.14.3 ; sys_platform != \"win32\"",
]
[project.scripts]
gitlint = "gitlint.cli:cli"
[tool.hatch.version]
source = "vcs"
raw-options = { root = ".." }
[tool.hatch.build]
include = [
"/gitlint", #
]
exclude = [
"/gitlint/tests", #
]
[tool.hatch.metadata.hooks.vcs.urls]
Homepage = "https://jorisroovers.github.io/gitlint"
Documentation = "https://jorisroovers.github.io/gitlint"
Source = "https://github.com/jorisroovers/gitlint/tree/main/gitlint-core"
Changelog = "https://github.com/jorisroovers/gitlint/blob/main/CHANGELOG.md"
# TODO(jorisroovers): Temporary disable until fixed in hatch-vcs (see #460)
# 'Source Commit' = "https://github.com/jorisroovers/gitlint/tree/{commit_hash}/gitlint-core"

View file

@ -1,2 +0,0 @@
[bdist_wheel]
universal = 1

View file

@ -1,109 +0,0 @@
#!/usr/bin/env python
from setuptools import setup, find_packages
import io
import re
import os
import platform
import sys
description = "Git commit message linter written in python, checks your commit messages for style."
long_description = """
Great for use as a commit-msg git hook or as part of your gating script in a CI pipeline (e.g. jenkins, github actions).
Many of the gitlint validations are based on `well-known`_ community_ `standards`_, others are based on checks that
we've found useful throughout the years. Gitlint has sane defaults, but you can also easily customize it to your
own liking.
Demo and full documentation on `jorisroovers.github.io/gitlint`_.
To see what's new in the latest release, visit the CHANGELOG_.
Source code on `github.com/jorisroovers/gitlint`_.
.. _well-known: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
.. _community: http://addamhardy.com/blog/2013/06/05/good-commit-messages-and-enforcing-them-with-git-hooks/
.. _standards: http://chris.beams.io/posts/git-commit/
.. _jorisroovers.github.io/gitlint: https://jorisroovers.github.io/gitlint
.. _CHANGELOG: https://github.com/jorisroovers/gitlint/blob/main/CHANGELOG.md
.. _github.com/jorisroovers/gitlint: https://github.com/jorisroovers/gitlint
"""
# shamelessly stolen from mkdocs' setup.py: https://github.com/mkdocs/mkdocs/blob/master/setup.py
def get_version(package):
"""Return package version as listed in `__version__` in `init.py`."""
init_py = open(os.path.join(package, "__init__.py"), encoding="UTF-8").read()
return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
setup(
name="gitlint-core",
version=get_version("gitlint"),
description=description,
long_description=long_description,
classifiers=[
"Development Status :: 5 - Production/Stable",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Environment :: Console",
"Intended Audience :: Developers",
"Topic :: Software Development :: Quality Assurance",
"Topic :: Software Development :: Testing",
"License :: OSI Approved :: MIT License",
],
python_requires=">=3.6",
install_requires=[
"Click>=8",
"arrow>=1",
'sh>=1.13.0 ; sys_platform != "win32"',
],
extras_require={
"trusted-deps": [
"Click==8.0.3",
"arrow==1.2.1",
'sh==1.14.2 ; sys_platform != "win32"',
],
},
keywords="gitlint git lint",
author="Joris Roovers",
url="https://jorisroovers.github.io/gitlint",
project_urls={
"Documentation": "https://jorisroovers.github.io/gitlint",
"Source": "https://github.com/jorisroovers/gitlint",
},
license="MIT",
package_data={"gitlint": ["files/*"]},
packages=find_packages(exclude=["examples"]),
entry_points={
"console_scripts": [
"gitlint = gitlint.cli:cli",
],
},
)
# Print a red deprecation warning for python < 3.6 users
if sys.version_info[:2] < (3, 6):
msg = (
"\033[31mDEPRECATION: You're using a python version that has reached end-of-life. "
+ "Gitlint does not support Python < 3.6"
+ "Please upgrade your Python to 3.6 or above.\033[0m"
)
print(msg)
# Print a warning message for Windows users
PLATFORM_IS_WINDOWS = "windows" in platform.system().lower()
if PLATFORM_IS_WINDOWS:
msg = (
"\n\n\n\n\n****************\n"
+ "WARNING: Gitlint support for Windows is still experimental and there are some known issues: "
+ "https://github.com/jorisroovers/gitlint/issues?q=is%3Aissue+is%3Aopen+label%3Awindows "
+ "\n*******************"
)
print(msg)