From 4fee3e091a8d79a40f70ff9c1f87b29b9340049a Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 25 Jan 2021 14:26:11 +0100 Subject: Merging upstream version 0.15.0. Signed-off-by: Daniel Baumann --- .github/workflows/checks.yml | 4 +- .pre-commit-hooks.yaml | 3 +- CHANGELOG.md | 17 +- Vagrantfile | 13 +- docs/configuration.md | 19 +- docs/contributing.md | 13 +- docs/index.md | 13 +- docs/rules.md | 4 +- docs/user_defined_rules.md | 7 +- examples/my_commit_rules.py | 7 +- examples/my_configuration_rules.py | 2 +- examples/my_line_rules.py | 2 +- gitlint/__init__.py | 2 +- gitlint/cache.py | 24 +-- gitlint/cli.py | 100 ++++----- gitlint/config.py | 117 +++++------ gitlint/contrib/rules/conventional_commit.py | 7 +- gitlint/display.py | 13 +- gitlint/exception.py | 4 + gitlint/git.py | 118 +++++------ gitlint/hooks.py | 19 +- gitlint/lint.py | 18 +- gitlint/options.py | 58 +++-- gitlint/rule_finder.py | 49 ++--- gitlint/rules.py | 62 ++---- gitlint/shell.py | 15 +- gitlint/tests/base.py | 60 ++---- gitlint/tests/cli/test_cli.py | 234 ++++++++++----------- gitlint/tests/cli/test_cli_hooks.py | 121 ++++++----- gitlint/tests/config/test_config.py | 69 +++--- gitlint/tests/config/test_config_builder.py | 80 +++---- gitlint/tests/config/test_config_precedence.py | 30 +-- gitlint/tests/config/test_rule_collection.py | 22 +- .../contrib/rules/test_conventional_commit.py | 20 +- gitlint/tests/contrib/rules/test_signedoff_by.py | 6 +- gitlint/tests/contrib/test_contrib_rules.py | 9 +- .../cli/test_cli_hooks/test_run_hook_negative_1 | 2 + .../cli/test_cli_hooks/test_run_hook_negative_2 | 2 + gitlint/tests/git/test_git.py | 45 ++-- gitlint/tests/git/test_git_commit.py | 230 ++++++++++---------- gitlint/tests/git/test_git_context.py | 55 +++-- gitlint/tests/rules/test_body_rules.py | 92 ++++---- gitlint/tests/rules/test_configuration_rules.py | 46 ++-- gitlint/tests/rules/test_meta_rules.py | 30 +-- gitlint/tests/rules/test_rules.py | 8 +- gitlint/tests/rules/test_title_rules.py | 90 ++++---- gitlint/tests/rules/test_user_rules.py | 48 ++--- .../tests/samples/user_rules/my_commit_rules.py | 6 +- .../samples/user_rules/parent_package/__init__.py | 2 +- .../user_rules/parent_package/my_commit_rules.py | 2 +- gitlint/tests/test_cache.py | 22 +- gitlint/tests/test_display.py | 55 ++--- gitlint/tests/test_hooks.py | 35 ++- gitlint/tests/test_lint.py | 110 +++++----- gitlint/tests/test_options.py | 113 +++++----- gitlint/tests/test_utils.py | 39 ++-- gitlint/utils.py | 47 ----- mkdocs.yml | 1 + qa/base.py | 20 +- qa/requirements.txt | 6 +- qa/samples/user_rules/extra/extra_rules.py | 33 ++- qa/shell.py | 20 +- qa/test_commits.py | 51 +++-- qa/test_config.py | 30 +-- qa/test_contrib.py | 10 +- qa/test_gitlint.py | 63 +++--- qa/test_hooks.py | 38 ++-- qa/test_named_rules.py | 4 +- qa/test_stdin.py | 14 +- qa/test_user_defined.py | 8 +- qa/utils.py | 49 ----- requirements.txt | 8 +- run_tests.sh | 10 +- setup.py | 25 +-- test-requirements.txt | 14 +- 75 files changed, 1285 insertions(+), 1559 deletions(-) create mode 100644 gitlint/exception.py create mode 100644 gitlint/tests/expected/cli/test_cli_hooks/test_run_hook_negative_1 create mode 100644 gitlint/tests/expected/cli/test_cli_hooks/test_run_hook_negative_2 diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 710245c..15eb7be 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -7,7 +7,7 @@ jobs: runs-on: "ubuntu-latest" strategy: matrix: - python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, pypy2, pypy3] + python-version: [3.6, 3.7, 3.8, 3.9, pypy3] os: ["macos-latest", "ubuntu-latest"] steps: - uses: actions/checkout@v2 @@ -76,7 +76,7 @@ jobs: runs-on: windows-latest strategy: matrix: - python-version: [2.7, 3.6] + python-version: [3.6] steps: - uses: actions/checkout@v2 with: diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 5b3d51a..4b15bfd 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -1,5 +1,6 @@ - id: gitlint name: gitlint language: python - entry: gitlint --staged --msg-filename + entry: gitlint + args: [--staged, --msg-filename] stages: [commit-msg] diff --git a/CHANGELOG.md b/CHANGELOG.md index 509ebc8..7eb2e0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,22 @@ # Changelog # +## v0.15.0 (2020-11-27) ## + +Contributors: +Special thanks to [BrunIF](https://github.com/BrunIF), [lukech](https://github.com/lukech), [Cielquan](https://github.com/Cielquan), [harens](https://github.com/harens) and [sigmavirus24](https://github.com/sigmavirus24). + +**This release drops support for Python 2.7 and Python 3.5 ([both are EOL](https://endoflife.date/python)). Other than a few minor fixes, there are no functional differences from the 0.14.0 release.** + +Other call-outs: +- **Mac users**: Gitlint can now be installed using both homebrew (upgraded to latest) and macports. Special thanks to [@harens](https://github.com/harens) for maintaining these packages (best-effort). +- Bugfix: Gitlint now properly handles exceptions when using its built-in commit-msg hook ([#166](https://github.com/jorisroovers/gitlint/issues/166)). +- All dependencies have been upgraded to the latest available versions (`Click==7.1.2`, `arrow==0.17.0`, `sh==1.14.1`). +- Much under-the-hood refactoring as a result of dropping Python 2.7 + ## v0.14.0 (2020-10-24) ## Contributors: -Special thanks to all contributors for this release, in particular [@mrshu](https://github.com/mrshu), [@glasserc](https://github.com/glasserc), [@strk](https://github.com/strk), [@chgl](https://github.com/chgl), [@melg8](https://github.com/melg8) and [@sigmavirus24](https://github.com/sigmavirus24). +Special thanks to all contributors for this release, in particular [mrshu](https://github.com/mrshu), [glasserc](https://github.com/glasserc), [strk](https://github.com/strk), [chgl](https://github.com/chgl), [melg8](https://github.com/melg8) and [sigmavirus24](https://github.com/sigmavirus24). - **IMPORTANT: Gitlint 0.14.x will be the last gitlint release to support Python 2.7 and Python 3.5, as [both are EOL](https://endoflife.date/python) which makes it difficult to keep supporting them.** @@ -13,7 +26,7 @@ Special thanks to all contributors for this release, in particular [@mrshu](http - **New Rule**: [ignore-body-lines](http://jorisroovers.github.io/gitlint/rules/#i3-ignore-body-lines) allows users to [ignore parts of a commit](http://jorisroovers.github.io/gitlint/gitlint/#ignoring-commits) by matching a regex against the lines in a commit message body ([#126](https://github.com/jorisroovers/gitlint/issues/126)) -- [Named Rules](http://jorisroovers.github.io/gitlint/#named-rules) allow users to have multiple instances of the same rule active at the same time. This is useful when you want to enforce the same rule multiple times but with different options ([#113](https://github.com/jorisroovers/gitlint/issues/130), [#66](https://github.com/jorisroovers/gitlint/issues/130)) +- [Named Rules](http://jorisroovers.github.io/gitlint/#named-rules) allow users to have multiple instances of the same rule active at the same time. This is useful when you want to enforce the same rule multiple times but with different options ([#113](https://github.com/jorisroovers/gitlint/issues/113), [#66](https://github.com/jorisroovers/gitlint/issues/66)) - [User-defined Configuration Rules](http://jorisroovers.github.io/gitlint/user_defined_rules/#configuration-rules) allow users to dynamically change gitlint's configuration and/or the commit *before* any other rules are applied. - The `commit-msg` hook has been re-written in Python (it contained a lot of Bash before), fixing a number of platform specific issues. Existing users will need to reinstall their hooks (`gitlint uninstall-hook; gitlint install-hook`) to make use of this. - Most general options can now be set through environment variables (e.g. set the `general.ignore` option via `GITLINT_IGNORE=T1,T2`). The list of available environment variables can be found in the [configuration documentation](http://jorisroovers.github.io/gitlint/configuration). diff --git a/Vagrantfile b/Vagrantfile index 7684c1a..f913248 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -7,18 +7,19 @@ INSTALL_DEPS=<> /home/vagrant/.bashrc -grep 'source .venv27/bin/activate' /home/vagrant/.bashrc || echo 'source .venv27/bin/activate' >> /home/vagrant/.bashrc +grep 'source .venv36/bin/activate' /home/vagrant/.bashrc || echo 'source .venv36/bin/activate' >> /home/vagrant/.bashrc EOF INSTALL_JENKINS=<= 0.4 | (empty) | Comma-separated list o #### .gitlint ```ini -# Prevent that certain sensitive files are committed by mistake by forcing users to mention them explicitly if they're -# deliberately changing them +# Prevent that certain sensitive files are committed by mistake by forcing +# users to mention them explicitly if they're deliberately changing them [body-changed-file-mention] files=generated.xml,secrets.txt,private-key.pem ``` diff --git a/docs/user_defined_rules.md b/docs/user_defined_rules.md index 13ea544..fd944d1 100644 --- a/docs/user_defined_rules.md +++ b/docs/user_defined_rules.md @@ -152,7 +152,7 @@ class SpecialChars(LineRule): # options can be accessed by looking them up by their name in self.options for char in self.options['special-chars'].value: if char in line: - msg = "Title contains the special character '{0}'".format(char) + msg = f"Title contains the special character '{char}'" violation = RuleViolation(self.id, msg, line) violations.append(violation) @@ -262,8 +262,7 @@ class BodyMaxLineCount(CommitRule): line_count = len(commit.message.body) max_line_count = self.options['max-line-count'].value if line_count > max_line_count: - message = "Body contains too many lines ({0} > {1})".format(line_count, - max_line_count) + message = f"Body contains too many lines ({line_count} > {max_line_count})" return [RuleViolation(self.id, message, line_nr=1)] ``` @@ -371,7 +370,7 @@ class ReleaseConfigurationRule(ConfigurationRule): # You can add any extra properties you want to the commit object, # these will be available later on in all rules. - commit.my_property = u"This is my property" + commit.my_property = "This is my property" ``` For all available properties and methods on the `config` object, have a look at the diff --git a/examples/my_commit_rules.py b/examples/my_commit_rules.py index c64008f..5a66c94 100644 --- a/examples/my_commit_rules.py +++ b/examples/my_commit_rules.py @@ -2,8 +2,6 @@ from gitlint.rules import CommitRule, RuleViolation from gitlint.options import IntOption, ListOption -from gitlint import utils - """ Full details on user-defined rules: https://jorisroovers.com/gitlint/user_defined_rules @@ -37,7 +35,7 @@ class BodyMaxLineCount(CommitRule): line_count = len(commit.message.body) max_line_count = self.options['max-line-count'].value if line_count > max_line_count: - message = "Body contains too many lines ({0} > {1})".format(line_count, max_line_count) + message = f"Body contains too many lines ({line_count} > {max_line_count})" return [RuleViolation(self.id, message, line_nr=1)] @@ -90,8 +88,7 @@ class BranchNamingConventions(CommitRule): break if not valid_branch_name: - msg = "Branch name '{0}' does not start with one of {1}".format(branch, - utils.sstr(allowed_branch_prefixes)) + msg = f"Branch name '{branch}' does not start with one of {allowed_branch_prefixes}" violations.append(RuleViolation(self.id, msg, line_nr=1)) return violations diff --git a/examples/my_configuration_rules.py b/examples/my_configuration_rules.py index 58de048..7c00707 100644 --- a/examples/my_configuration_rules.py +++ b/examples/my_configuration_rules.py @@ -69,4 +69,4 @@ class ReleaseConfigurationRule(ConfigurationRule): # You can add any extra properties you want to the commit object, these will be available later on # in all rules. - commit.my_property = u"This is my property" + commit.my_property = "This is my property" diff --git a/examples/my_line_rules.py b/examples/my_line_rules.py index 820024a..3a1ef36 100644 --- a/examples/my_line_rules.py +++ b/examples/my_line_rules.py @@ -45,7 +45,7 @@ class SpecialChars(LineRule): # options can be accessed by looking them up by their name in self.options for char in self.options['special-chars'].value: if char in line: - msg = "Title contains the special character '{0}'".format(char) + msg = f"Title contains the special character '{char}'" violation = RuleViolation(self.id, msg, line) violations.append(violation) diff --git a/gitlint/__init__.py b/gitlint/__init__.py index 9e78220..9da2f8f 100644 --- a/gitlint/__init__.py +++ b/gitlint/__init__.py @@ -1 +1 @@ -__version__ = "0.14.0" +__version__ = "0.15.0" diff --git a/gitlint/cache.py b/gitlint/cache.py index b7f9e6c..1b6558f 100644 --- a/gitlint/cache.py +++ b/gitlint/cache.py @@ -1,4 +1,4 @@ -class PropertyCache(object): +class PropertyCache: """ Mixin class providing a simple cache. """ def __init__(self): @@ -13,7 +13,7 @@ class PropertyCache(object): return self._cache[cache_key] -def cache(original_func=None, cachekey=None): +def cache(original_func=None, cachekey=None): # pylint: disable=unused-argument """ Cache decorator. Caches function return values. Requires the parent class to extend and initialize PropertyCache. Usage: @@ -28,27 +28,23 @@ def cache(original_func=None, cachekey=None): ... """ - # Decorators with optional arguments are a bit convoluted in python, especially if you want to support both - # Python 2 and 3. See some of the links below for details. + # Decorators with optional arguments are a bit convoluted in python, see some of the links below for details. def cache_decorator(func): - - # If no specific cache key is given, use the function name as cache key - if not cache_decorator.cachekey: - cache_decorator.cachekey = func.__name__ + # 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[cache_decorator.cachekey] = func(*args) - return args[0]._try_cache(cache_decorator.cachekey, cache_func_result) + args[0]._cache[cachekey] = func(*args) + return args[0]._try_cache(cachekey, cache_func_result) return wrapped - # Passing parent function variables to child functions requires special voodoo in python2: - # https://stackoverflow.com/a/14678445/381010 - cache_decorator.cachekey = cachekey # attribute on the function - # 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: diff --git a/gitlint/cli.py b/gitlint/cli.py index f284792..b162e5b 100644 --- a/gitlint/cli.py +++ b/gitlint/cli.py @@ -8,19 +8,20 @@ import stat import sys import click -# Error codes -MAX_VIOLATION_ERROR_CODE = 252 # noqa -USAGE_ERROR_CODE = 253 # noqa -GIT_CONTEXT_ERROR_CODE = 254 # noqa -CONFIG_ERROR_CODE = 255 # noqa - import gitlint from gitlint.lint import GitLinter from gitlint.config import LintConfigBuilder, LintConfigError, LintConfigGenerator from gitlint.git import GitContext, GitContextError, git_version from gitlint import hooks from gitlint.shell import shell -from gitlint.utils import ustr, LOG_FORMAT, IS_PY2 +from gitlint.utils import LOG_FORMAT +from gitlint.exception import GitlintError + +# Error codes +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 ) @@ -34,7 +35,7 @@ click.UsageError.exit_code = USAGE_ERROR_CODE LOG = logging.getLogger("gitlint.cli") -class GitLintUsageError(Exception): +class GitLintUsageError(GitlintError): """ Exception indicating there is an issue with how gitlint is used. """ pass @@ -134,7 +135,7 @@ def get_stdin_data(): # Only return the input data if there's actually something passed # i.e. don't consider empty piped data if input_data: - return ustr(input_data) + return str(input_data) return False @@ -151,7 +152,7 @@ def build_git_context(lint_config, msg_filename, refspec): # 1. Any data specified via --msg-filename if msg_filename: LOG.debug("Using --msg-filename.") - return from_commit_msg(ustr(msg_filename.read())) + 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: @@ -162,15 +163,28 @@ def build_git_context(lint_config, msg_filename, refspec): return from_commit_msg(stdin_input) if lint_config.staged: - raise GitLintUsageError(u"The 'staged' option (--staged) can only be used when using '--msg-filename' or " - u"when piping data to gitlint via stdin.") + 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.") return GitContext.from_local_repository(lint_config.target, refspec) -class ContextObj(object): +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, refspec, msg_filename, gitcontext=None): @@ -187,7 +201,7 @@ class ContextObj(object): 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', type=click.Path(exists=True, dir_okay=False, readable=True, resolve_path=True), - help="Config file location [default: {0}]".format(DEFAULT_CONFIG_FILE)) + help=f"Config file location [default: {DEFAULT_CONFIG_FILE}]") @click.option('-c', multiple=True, help="Config flags in format .