From 6f442e774b9236c999f36c2d7af17640f49bff99 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Tue, 9 Apr 2024 15:38:02 +0200 Subject: Adding upstream version 0.19.1. Signed-off-by: Daniel Baumann --- gitlint-core/LICENSE | 22 + gitlint-core/README.md | 26 + gitlint-core/gitlint/__init__.py | 8 + gitlint-core/gitlint/cache.py | 54 ++ gitlint-core/gitlint/cli.py | 499 +++++++++++++ gitlint-core/gitlint/config.py | 561 ++++++++++++++ gitlint-core/gitlint/contrib/__init__.py | 0 gitlint-core/gitlint/contrib/rules/__init__.py | 0 .../gitlint/contrib/rules/authors_commit.py | 45 ++ .../gitlint/contrib/rules/conventional_commit.py | 37 + .../contrib/rules/disallow_cleanup_commits.py | 22 + gitlint-core/gitlint/contrib/rules/signedoff_by.py | 17 + gitlint-core/gitlint/deprecation.py | 39 + gitlint-core/gitlint/display.py | 36 + gitlint-core/gitlint/exception.py | 2 + gitlint-core/gitlint/files/commit-msg | 35 + gitlint-core/gitlint/files/gitlint | 140 ++++ gitlint-core/gitlint/git.py | 510 +++++++++++++ gitlint-core/gitlint/hooks.py | 65 ++ gitlint-core/gitlint/lint.py | 123 +++ gitlint-core/gitlint/options.py | 146 ++++ gitlint-core/gitlint/rule_finder.py | 155 ++++ gitlint-core/gitlint/rules.py | 485 ++++++++++++ gitlint-core/gitlint/shell.py | 78 ++ gitlint-core/gitlint/tests/__init__.py | 0 gitlint-core/gitlint/tests/base.py | 227 ++++++ gitlint-core/gitlint/tests/cli/test_cli.py | 736 ++++++++++++++++++ gitlint-core/gitlint/tests/cli/test_cli_hooks.py | 277 +++++++ gitlint-core/gitlint/tests/config/test_config.py | 320 ++++++++ .../gitlint/tests/config/test_config_builder.py | 275 +++++++ .../gitlint/tests/config/test_config_precedence.py | 98 +++ .../gitlint/tests/config/test_rule_collection.py | 62 ++ gitlint-core/gitlint/tests/contrib/__init__.py | 0 .../gitlint/tests/contrib/rules/__init__.py | 0 .../tests/contrib/rules/test_authors_commit.py | 105 +++ .../contrib/rules/test_conventional_commit.py | 82 ++ .../contrib/rules/test_disallow_cleanup_commits.py | 34 + .../tests/contrib/rules/test_signedoff_by.py | 28 + .../gitlint/tests/contrib/test_contrib_rules.py | 69 ++ .../tests/expected/cli/test_cli/test_contrib_1 | 2 + .../tests/expected/cli/test_cli/test_debug_1 | 139 ++++ .../expected/cli/test_cli/test_input_stream_1 | 3 + .../cli/test_cli/test_input_stream_debug_1 | 3 + .../cli/test_cli/test_input_stream_debug_2 | 89 +++ .../tests/expected/cli/test_cli/test_lint_commit_1 | 2 + .../cli/test_cli/test_lint_multiple_commits_1 | 8 + .../test_cli/test_lint_multiple_commits_config_1 | 6 + .../cli/test_cli/test_lint_multiple_commits_csv_1 | 8 + .../cli/test_cli/test_lint_staged_msg_filename_1 | 2 + .../cli/test_cli/test_lint_staged_msg_filename_2 | 93 +++ .../expected/cli/test_cli/test_lint_staged_stdin_1 | 3 + .../expected/cli/test_cli/test_lint_staged_stdin_2 | 95 +++ .../tests/expected/cli/test_cli/test_named_rules_1 | 4 + .../tests/expected/cli/test_cli/test_named_rules_2 | 92 +++ .../cli/test_cli_hooks/test_hook_config_1_stderr | 2 + .../cli/test_cli_hooks/test_hook_config_1_stdout | 5 + .../cli/test_cli_hooks/test_hook_edit_1_stderr | 6 + .../cli/test_cli_hooks/test_hook_edit_1_stdout | 14 + .../test_cli_hooks/test_hook_local_commit_1_stderr | 2 + .../test_cli_hooks/test_hook_local_commit_1_stdout | 4 + .../cli/test_cli_hooks/test_hook_no_1_stderr | 2 + .../cli/test_cli_hooks/test_hook_no_1_stdout | 8 + .../cli/test_cli_hooks/test_hook_no_tty_1_stderr | 2 + .../cli/test_cli_hooks/test_hook_no_tty_1_stdout | 5 + .../test_hook_stdin_no_violations_1_stdout | 2 + .../test_hook_stdin_violations_1_stderr | 2 + .../test_hook_stdin_violations_1_stdout | 5 + .../cli/test_cli_hooks/test_hook_yes_1_stderr | 2 + .../cli/test_cli_hooks/test_hook_yes_1_stdout | 4 + .../cli/test_cli_hooks/test_run_hook_negative_1 | 2 + .../cli/test_cli_hooks/test_run_hook_negative_2 | 2 + gitlint-core/gitlint/tests/git/test_git.py | 121 +++ gitlint-core/gitlint/tests/git/test_git_commit.py | 825 +++++++++++++++++++++ gitlint-core/gitlint/tests/git/test_git_context.py | 73 ++ gitlint-core/gitlint/tests/rules/__init__.py | 0 .../gitlint/tests/rules/test_body_rules.py | 235 ++++++ .../tests/rules/test_configuration_rules.py | 178 +++++ .../gitlint/tests/rules/test_meta_rules.py | 80 ++ gitlint-core/gitlint/tests/rules/test_rules.py | 32 + .../gitlint/tests/rules/test_title_rules.py | 200 +++++ .../gitlint/tests/rules/test_user_rules.py | 266 +++++++ .../gitlint/tests/samples/commit_message/fixup | 1 + .../tests/samples/commit_message/fixup_amend | 1 + .../gitlint/tests/samples/commit_message/merge | 3 + .../tests/samples/commit_message/no-violations | 6 + .../gitlint/tests/samples/commit_message/revert | 3 + .../gitlint/tests/samples/commit_message/sample1 | 14 + .../gitlint/tests/samples/commit_message/sample2 | 1 + .../gitlint/tests/samples/commit_message/sample3 | 6 + .../gitlint/tests/samples/commit_message/sample4 | 7 + .../gitlint/tests/samples/commit_message/sample5 | 7 + .../gitlint/tests/samples/commit_message/squash | 3 + gitlint-core/gitlint/tests/samples/config/AUTHORS | 2 + .../gitlint/tests/samples/config/gitlintconfig | 15 + .../tests/samples/config/invalid-option-value | 11 + .../gitlint/tests/samples/config/named-rules | 8 + .../gitlint/tests/samples/config/no-sections | 1 + .../samples/config/nonexisting-general-option | 13 + .../tests/samples/config/nonexisting-option | 11 + .../gitlint/tests/samples/config/nonexisting-rule | 11 + .../tests/samples/user_rules/bogus-file.txt | 2 + .../user_rules/import_exception/invalid_python.py | 2 + .../user_rules/incorrect_linerule/my_line_rule.py | 8 + .../tests/samples/user_rules/my_commit_rules.foo | 16 + .../tests/samples/user_rules/my_commit_rules.py | 25 + .../samples/user_rules/parent_package/__init__.py | 12 + .../user_rules/parent_package/my_commit_rules.py | 10 + gitlint-core/gitlint/tests/test_cache.py | 55 ++ gitlint-core/gitlint/tests/test_deprecation.py | 26 + gitlint-core/gitlint/tests/test_display.py | 60 ++ gitlint-core/gitlint/tests/test_hooks.py | 139 ++++ gitlint-core/gitlint/tests/test_lint.py | 296 ++++++++ gitlint-core/gitlint/tests/test_options.py | 240 ++++++ gitlint-core/gitlint/tests/test_utils.py | 70 ++ gitlint-core/gitlint/utils.py | 87 +++ gitlint-core/pyproject.toml | 71 ++ 116 files changed, 9289 insertions(+) create mode 100644 gitlint-core/LICENSE create mode 100644 gitlint-core/README.md create mode 100644 gitlint-core/gitlint/__init__.py create mode 100644 gitlint-core/gitlint/cache.py create mode 100644 gitlint-core/gitlint/cli.py create mode 100644 gitlint-core/gitlint/config.py create mode 100644 gitlint-core/gitlint/contrib/__init__.py create mode 100644 gitlint-core/gitlint/contrib/rules/__init__.py create mode 100644 gitlint-core/gitlint/contrib/rules/authors_commit.py create mode 100644 gitlint-core/gitlint/contrib/rules/conventional_commit.py create mode 100644 gitlint-core/gitlint/contrib/rules/disallow_cleanup_commits.py create mode 100644 gitlint-core/gitlint/contrib/rules/signedoff_by.py create mode 100644 gitlint-core/gitlint/deprecation.py create mode 100644 gitlint-core/gitlint/display.py create mode 100644 gitlint-core/gitlint/exception.py create mode 100644 gitlint-core/gitlint/files/commit-msg create mode 100644 gitlint-core/gitlint/files/gitlint create mode 100644 gitlint-core/gitlint/git.py create mode 100644 gitlint-core/gitlint/hooks.py create mode 100644 gitlint-core/gitlint/lint.py create mode 100644 gitlint-core/gitlint/options.py create mode 100644 gitlint-core/gitlint/rule_finder.py create mode 100644 gitlint-core/gitlint/rules.py create mode 100644 gitlint-core/gitlint/shell.py create mode 100644 gitlint-core/gitlint/tests/__init__.py create mode 100644 gitlint-core/gitlint/tests/base.py create mode 100644 gitlint-core/gitlint/tests/cli/test_cli.py create mode 100644 gitlint-core/gitlint/tests/cli/test_cli_hooks.py create mode 100644 gitlint-core/gitlint/tests/config/test_config.py create mode 100644 gitlint-core/gitlint/tests/config/test_config_builder.py create mode 100644 gitlint-core/gitlint/tests/config/test_config_precedence.py create mode 100644 gitlint-core/gitlint/tests/config/test_rule_collection.py create mode 100644 gitlint-core/gitlint/tests/contrib/__init__.py create mode 100644 gitlint-core/gitlint/tests/contrib/rules/__init__.py create mode 100644 gitlint-core/gitlint/tests/contrib/rules/test_authors_commit.py create mode 100644 gitlint-core/gitlint/tests/contrib/rules/test_conventional_commit.py create mode 100644 gitlint-core/gitlint/tests/contrib/rules/test_disallow_cleanup_commits.py create mode 100644 gitlint-core/gitlint/tests/contrib/rules/test_signedoff_by.py create mode 100644 gitlint-core/gitlint/tests/contrib/test_contrib_rules.py create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli/test_contrib_1 create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli/test_debug_1 create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli/test_input_stream_1 create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_1 create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_2 create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_commit_1 create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_multiple_commits_1 create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_multiple_commits_config_1 create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_multiple_commits_csv_1 create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_1 create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_2 create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_1 create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_2 create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli/test_named_rules_1 create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli/test_named_rules_2 create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_hook_config_1_stderr create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_hook_config_1_stdout create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_hook_edit_1_stderr create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_hook_edit_1_stdout create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_hook_local_commit_1_stderr create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_hook_local_commit_1_stdout create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_1_stderr create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_1_stdout create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_tty_1_stderr create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_tty_1_stdout create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_no_violations_1_stdout create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_violations_1_stderr create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_violations_1_stdout create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_hook_yes_1_stderr create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_hook_yes_1_stdout create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_run_hook_negative_1 create mode 100644 gitlint-core/gitlint/tests/expected/cli/test_cli_hooks/test_run_hook_negative_2 create mode 100644 gitlint-core/gitlint/tests/git/test_git.py create mode 100644 gitlint-core/gitlint/tests/git/test_git_commit.py create mode 100644 gitlint-core/gitlint/tests/git/test_git_context.py create mode 100644 gitlint-core/gitlint/tests/rules/__init__.py create mode 100644 gitlint-core/gitlint/tests/rules/test_body_rules.py create mode 100644 gitlint-core/gitlint/tests/rules/test_configuration_rules.py create mode 100644 gitlint-core/gitlint/tests/rules/test_meta_rules.py create mode 100644 gitlint-core/gitlint/tests/rules/test_rules.py create mode 100644 gitlint-core/gitlint/tests/rules/test_title_rules.py create mode 100644 gitlint-core/gitlint/tests/rules/test_user_rules.py create mode 100644 gitlint-core/gitlint/tests/samples/commit_message/fixup create mode 100644 gitlint-core/gitlint/tests/samples/commit_message/fixup_amend create mode 100644 gitlint-core/gitlint/tests/samples/commit_message/merge create mode 100644 gitlint-core/gitlint/tests/samples/commit_message/no-violations create mode 100644 gitlint-core/gitlint/tests/samples/commit_message/revert create mode 100644 gitlint-core/gitlint/tests/samples/commit_message/sample1 create mode 100644 gitlint-core/gitlint/tests/samples/commit_message/sample2 create mode 100644 gitlint-core/gitlint/tests/samples/commit_message/sample3 create mode 100644 gitlint-core/gitlint/tests/samples/commit_message/sample4 create mode 100644 gitlint-core/gitlint/tests/samples/commit_message/sample5 create mode 100644 gitlint-core/gitlint/tests/samples/commit_message/squash create mode 100644 gitlint-core/gitlint/tests/samples/config/AUTHORS create mode 100644 gitlint-core/gitlint/tests/samples/config/gitlintconfig create mode 100644 gitlint-core/gitlint/tests/samples/config/invalid-option-value create mode 100644 gitlint-core/gitlint/tests/samples/config/named-rules create mode 100644 gitlint-core/gitlint/tests/samples/config/no-sections create mode 100644 gitlint-core/gitlint/tests/samples/config/nonexisting-general-option create mode 100644 gitlint-core/gitlint/tests/samples/config/nonexisting-option create mode 100644 gitlint-core/gitlint/tests/samples/config/nonexisting-rule create mode 100644 gitlint-core/gitlint/tests/samples/user_rules/bogus-file.txt create mode 100644 gitlint-core/gitlint/tests/samples/user_rules/import_exception/invalid_python.py create mode 100644 gitlint-core/gitlint/tests/samples/user_rules/incorrect_linerule/my_line_rule.py create mode 100644 gitlint-core/gitlint/tests/samples/user_rules/my_commit_rules.foo create mode 100644 gitlint-core/gitlint/tests/samples/user_rules/my_commit_rules.py create mode 100644 gitlint-core/gitlint/tests/samples/user_rules/parent_package/__init__.py create mode 100644 gitlint-core/gitlint/tests/samples/user_rules/parent_package/my_commit_rules.py create mode 100644 gitlint-core/gitlint/tests/test_cache.py create mode 100644 gitlint-core/gitlint/tests/test_deprecation.py create mode 100644 gitlint-core/gitlint/tests/test_display.py create mode 100644 gitlint-core/gitlint/tests/test_hooks.py create mode 100644 gitlint-core/gitlint/tests/test_lint.py create mode 100644 gitlint-core/gitlint/tests/test_options.py create mode 100644 gitlint-core/gitlint/tests/test_utils.py create mode 100644 gitlint-core/gitlint/utils.py create mode 100644 gitlint-core/pyproject.toml (limited to 'gitlint-core') diff --git a/gitlint-core/LICENSE b/gitlint-core/LICENSE new file mode 100644 index 0000000..122bd28 --- /dev/null +++ b/gitlint-core/LICENSE @@ -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. + diff --git a/gitlint-core/README.md b/gitlint-core/README.md new file mode 100644 index 0000000..dfbbe7f --- /dev/null +++ b/gitlint-core/README.md @@ -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.** + + + + + +## 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). diff --git a/gitlint-core/gitlint/__init__.py b/gitlint-core/gitlint/__init__.py new file mode 100644 index 0000000..a2339fd --- /dev/null +++ b/gitlint-core/gitlint/__init__.py @@ -0,0 +1,8 @@ +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") diff --git a/gitlint-core/gitlint/cache.py b/gitlint-core/gitlint/cache.py new file mode 100644 index 0000000..a3dd0c8 --- /dev/null +++ b/gitlint-core/gitlint/cache.py @@ -0,0 +1,54 @@ +class PropertyCache: + """Mixin class providing a simple cache.""" + + def __init__(self): + self._cache = {} + + def _try_cache(self, cache_key, cache_populate_func): + """Tries to get a value from the cache identified by `cache_key`. + If no value is found in the cache, do a function call to `cache_populate_func` to populate the cache + and then return the value from the cache.""" + if cache_key not in self._cache: + cache_populate_func() + return self._cache[cache_key] + + +def cache(original_func=None, cachekey=None): + """Cache decorator. Caches function return values. + Requires the parent class to extend and initialize PropertyCache. + Usage: + # Use function name as cache key + @cache + def myfunc(args): + ... + + # Specify cache key + @cache(cachekey="foobar") + def myfunc(args): + ... + """ + + # Decorators with optional arguments are a bit convoluted in python, see some of the links below for details. + + def cache_decorator(func): + # Use 'nonlocal' keyword to access parent function variable: + # https://stackoverflow.com/a/14678445/381010 + nonlocal cachekey + if not cachekey: + cachekey = func.__name__ + + def wrapped(*args): + def cache_func_result(): + # Call decorated function and store its result in the cache + args[0]._cache[cachekey] = func(*args) + + return args[0]._try_cache(cachekey, cache_func_result) + + return wrapped + + # To support optional kwargs for decorators, we need to check if a function is passed as first argument or not. + # https://stackoverflow.com/a/24617244/381010 + if original_func: + return cache_decorator(original_func) + + return cache_decorator diff --git a/gitlint-core/gitlint/cli.py b/gitlint-core/gitlint/cli.py new file mode 100644 index 0000000..619f006 --- /dev/null +++ b/gitlint-core/gitlint/cli.py @@ -0,0 +1,499 @@ +import copy +import logging +import os +import platform +import stat +import sys + +import click + +import gitlint +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 + +# Error codes +GITLINT_SUCCESS = 0 +MAX_VIOLATION_ERROR_CODE = 252 +USAGE_ERROR_CODE = 253 +GIT_CONTEXT_ERROR_CODE = 254 +CONFIG_ERROR_CODE = 255 + +DEFAULT_CONFIG_FILE = ".gitlint" +# -n: disable swap files. This fixes a vim error on windows (E303: Unable to open swap file for ) +DEFAULT_COMMIT_MSG_EDITOR = "vim -n" + +# Since we use the return code to denote the amount of errors, we need to change the default click usage error code +click.UsageError.exit_code = USAGE_ERROR_CODE + +# We don't use logging.getLogger(__main__) here because that will cause DEBUG output to be lost +# when invoking gitlint as a python module (python -m gitlint.cli) +LOG = logging.getLogger("gitlint.cli") + + +class GitLintUsageError(GitlintError): + """Exception indicating there is an issue with how gitlint is used.""" + + +def setup_logging(): + """Setup gitlint 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.WARN) + handler = logging.StreamHandler() + formatter = logging.Formatter(LOG_FORMAT) + handler.setFormatter(formatter) + root_log.addHandler(handler) + + # Deprecated log, to log deprecation warnings + DEPRECATED_LOG.propagate = False # Don't propagate to child logger + DEPRECATED_LOG.setLevel(logging.WARNING) + deprecated_log_handler = logging.StreamHandler() + deprecated_log_handler.setFormatter(logging.Formatter(DEPRECATED_LOG_FORMAT)) + DEPRECATED_LOG.addHandler(deprecated_log_handler) + + +def log_system_info(): + LOG.debug("Platform: %s", platform.platform()) + LOG.debug("Python version: %s", sys.version) + 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("TERMINAL_ENCODING: %s", gitlint.utils.TERMINAL_ENCODING) + LOG.debug("FILE_ENCODING: %s", gitlint.utils.FILE_ENCODING) + + +def build_config( + target, + config_path, + c, + extra_path, + ignore, + contrib, + ignore_stdin, + staged, + fail_without_commits, + verbose, + silent, + debug, +): + """Creates a LintConfig object based on a set of commandline parameters.""" + config_builder = LintConfigBuilder() + # Config precedence: + # First, load default config or config from configfile + if config_path: + config_builder.set_from_config_file(config_path) + elif os.path.exists(DEFAULT_CONFIG_FILE): + config_builder.set_from_config_file(DEFAULT_CONFIG_FILE) + + # Then process any commandline configuration flags + config_builder.set_config_from_string_list(c) + + # Finally, overwrite with any convenience commandline flags + if ignore: + config_builder.set_option("general", "ignore", ignore) + + if contrib: + config_builder.set_option("general", "contrib", contrib) + + if ignore_stdin: + config_builder.set_option("general", "ignore-stdin", ignore_stdin) + + if silent: + config_builder.set_option("general", "verbosity", 0) + elif verbose > 0: + config_builder.set_option("general", "verbosity", verbose) + + if extra_path: + config_builder.set_option("general", "extra-path", extra_path) + + if target: + config_builder.set_option("general", "target", target) + + if debug: + config_builder.set_option("general", "debug", debug) + + if staged: + config_builder.set_option("general", "staged", staged) + + if fail_without_commits: + config_builder.set_option("general", "fail-without-commits", fail_without_commits) + + config = config_builder.build() + + return config, config_builder + + +def get_stdin_data(): + """Helper function that returns data sent to stdin or False if nothing is sent""" + # STDIN can only be 3 different types of things ("modes") + # 1. An interactive terminal device (i.e. a TTY -> sys.stdin.isatty() or stat.S_ISCHR) + # 2. A (named) pipe (stat.S_ISFIFO) + # 3. A regular file (stat.S_ISREG) + # Technically, STDIN can also be other device type like a named unix socket (stat.S_ISSOCK), but we don't + # support that in gitlint (at least not today). + # + # Now, the behavior that we want is the following: + # If someone sends something directly to gitlint via a pipe or a regular file, read it. If not, read from the + # local repository. + # Note that we don't care about whether STDIN is a TTY or not, we only care whether data is via a pipe or regular + # file. + # However, in case STDIN is not a TTY, it HAS to be one of the 2 other things (pipe or regular file), even if + # no-one is actually sending anything to gitlint over them. In this case, we still want to read from the local + # repository. + # To support this use-case (which is common in CI runners such as Jenkins and Gitlab), we need to actually attempt + # to read from STDIN in case it's a pipe or regular file. In case that fails, then we'll fall back to reading + # from the local repo. + + mode = os.fstat(sys.stdin.fileno()).st_mode + stdin_is_pipe_or_file = stat.S_ISFIFO(mode) or stat.S_ISREG(mode) + if stdin_is_pipe_or_file: + input_data = sys.stdin.read() + # Only return the input data if there's actually something passed + # i.e. don't consider empty piped data + if input_data: + return str(input_data) + return False + + +def build_git_context(lint_config, msg_filename, commit_hash, refspec): + """Builds a git context based on passed parameters and order of precedence""" + + # Determine which GitContext method to use if a custom message is passed + from_commit_msg = GitContext.from_commit_msg + if lint_config.staged: + LOG.debug("Fetching additional meta-data from staged commit") + + def from_commit_msg(message): + return GitContext.from_staged_commit(message, lint_config.target) + + # Order of precedence: + # 1. Any data specified via --msg-filename + if msg_filename: + LOG.debug("Using --msg-filename.") + return from_commit_msg(str(msg_filename.read())) + + # 2. Any data sent to stdin (unless stdin is being ignored) + if not lint_config.ignore_stdin: + stdin_input = get_stdin_data() + if stdin_input: + LOG.debug("Stdin data: '%s'", stdin_input) + LOG.debug("Stdin detected and not ignored. Using as input.") + return from_commit_msg(stdin_input) + + if lint_config.staged: + raise GitLintUsageError( + "The 'staged' option (--staged) can only be used when using '--msg-filename' or " + "when piping data to gitlint via stdin." + ) + + # 3. Fallback to reading from local repository + LOG.debug("No --msg-filename flag, no or empty data passed to stdin. Using the local repo.") + + if commit_hash and refspec: + raise GitLintUsageError("--commit and --commits are mutually exclusive, use one or the other.") + + # 3.1 Linting a range of commits + 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(",") 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) + + # 3.2 Linting a specific commit + if commit_hash: + return GitContext.from_local_repository(lint_config.target, commit_hashes=[commit_hash]) + + # 3.3 Fallback to linting the current HEAD + return GitContext.from_local_repository(lint_config.target) + + +def handle_gitlint_error(ctx, exc): + """Helper function to handle exceptions""" + if isinstance(exc, GitContextError): + click.echo(exc) + ctx.exit(GIT_CONTEXT_ERROR_CODE) + elif isinstance(exc, GitLintUsageError): + click.echo(f"Error: {exc}") + ctx.exit(USAGE_ERROR_CODE) + elif isinstance(exc, LintConfigError): + click.echo(f"Config Error: {exc}") + ctx.exit(CONFIG_ERROR_CODE) + + +class ContextObj: + """Simple class to hold data that is passed between Click commands via the Click context.""" + + def __init__(self, config, config_builder, commit_hash, refspec, msg_filename, gitcontext=None): + self.config = config + self.config_builder = config_builder + self.commit_hash = commit_hash + self.refspec = refspec + self.msg_filename = msg_filename + self.gitcontext = gitcontext + + +# fmt: off +@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", + 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", + 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, + help="Config flags in format .