1
0
Fork 0

Merging upstream version 0.16.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-13 06:05:35 +01:00
parent 40df5416c1
commit 72676ec535
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
56 changed files with 615 additions and 161 deletions

View file

@ -1,2 +1,6 @@
[report]
fail_under = 97
[run] [run]
omit=*dist-packages*,*site-packages*,gitlint/tests/*,.venv/*,*virtualenv* branch = true
omit=*dist-packages*,*site-packages*,gitlint/tests/*,.venv/*,*virtualenv*

14
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: docker
directory: /
schedule:
interval: daily
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
- package-ecosystem: pip
directory: /
schedule:
interval: daily

View file

@ -7,7 +7,7 @@ jobs:
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
strategy: strategy:
matrix: matrix:
python-version: [3.6, 3.7, 3.8, 3.9, pypy3] python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", pypy3]
os: ["macos-latest", "ubuntu-latest"] os: ["macos-latest", "ubuntu-latest"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -69,8 +69,11 @@ jobs:
- name: Re-add git version control to code - name: Re-add git version control to code
run: mv ._git .git run: mv ._git .git
# Run gitlint. Skip during PR runs, since PR commit messages are transient and usually full of gitlint violations.
# PRs get squashed and get a proper commit message during merge.
- name: Gitlint check - name: Gitlint check
run: ./run_tests.sh -g --debug run: ./run_tests.sh -g --debug
if: ${{ github.event_name != 'pull_request' }}
windows-checks: windows-checks:
runs-on: windows-latest runs-on: windows-latest
@ -133,5 +136,8 @@ jobs:
- name: Re-add git version control to code - name: Re-add git version control to code
run: Rename-Item ._git .git run: Rename-Item ._git .git
# Run gitlint. Skip during PR runs, since PR commit messages are transient and usually full of gitlint violations.
# PRs get squashed and get a proper commit message during merge.
- name: Gitlint check - name: Gitlint check
run: gitlint --debug run: gitlint --debug
if: ${{ github.event_name != 'pull_request' }}

View file

@ -1,5 +1,19 @@
# Changelog # # Changelog #
## v0.16.0 (2021-10-08) ##
Contributors:
Special thanks to all contributors for this release, in particular [sigmavirus24](https://github.com/sigmavirus24), [l0b0](https://github.com/l0b0) and [rafaelbubach](https://github.com/rafaelbubach).
- Python 3.10 support
- **New Rule**: [ignore-by-author-name](http://jorisroovers.github.io/gitlint/rules/#i4-ignore-by-author-name) allows users to skip linting commit messages made by specific authors
- `--commit <SHA>` flag to more easily lint a single commit message ([#141](https://github.com/jorisroovers/gitlint/issues/141))
- `--fail-without-commits` flag will force gitlint to fail ([exit code 253](https://jorisroovers.com/gitlint/#exit-codes)) when the target commit range is empty (typically when using `--commits`) ([#193](https://github.com/jorisroovers/gitlint/issues/193))
- Bugfixes:
- [contrib-title-conventional-commits (CT1)](https://jorisroovers.com/gitlint/contrib_rules/#ct1-contrib-title-conventional-commits) now properly enforces the commit type ([#185](https://github.com/jorisroovers/gitlint/issues/185))
- [contrib-title-conventional-commits (CT1)](https://jorisroovers.com/gitlint/contrib_rules/#ct1-contrib-title-conventional-commits) now supports the BREAKING CHANGE symbol "!" ([#186](https://github.com/jorisroovers/gitlint/issues/186))
- Heads-up: [Python 3.6 will become EOL at the end of 2021](https://endoflife.date/python). It's likely that future gitlint releases will stop supporting Python 3.6 as a result. We will continue to support Python 3.6 as long as its easily doable, which in practice usually means as long as our dependencies support it.
- Under-the-hood: dependencies updated, test and github action improvements.
## v0.15.1 (2021-04-16) ## ## v0.15.1 (2021-04-16) ##
Contributors: Contributors:

View file

@ -9,7 +9,7 @@
# NOTE: --ulimit is required to work around a limitation in Docker # NOTE: --ulimit is required to work around a limitation in Docker
# Details: https://github.com/jorisroovers/gitlint/issues/129 # Details: https://github.com/jorisroovers/gitlint/issues/129
FROM python:3.9-alpine FROM python:3.10-alpine
ARG GITLINT_VERSION ARG GITLINT_VERSION
RUN apk add git RUN apk add git

View file

@ -8,12 +8,14 @@ Git commit message linter written in python (for Linux and Mac, experimental on
**See [jorisroovers.github.io/gitlint](http://jorisroovers.github.io/gitlint/) for full documentation.** **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://asciinema.org/a/30477.png" width="640"/></a> <a href="http://jorisroovers.github.io/gitlint/" target="_blank">
<img src="docs/images/readme-gitlint.png" />
</a>
## Contributing ## ## Contributing ##
All contributions are welcome and very much appreciated! 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 open a PR if you're interested - Thanks!** **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 See [jorisroovers.github.io/gitlint/contributing](http://jorisroovers.github.io/gitlint/contributing) for details on
how to get started - it's easy! how to get started - it's easy!

View file

@ -1 +1 @@
mkdocs==1.1.2 mkdocs==1.2.2

View file

@ -52,6 +52,12 @@ ignore-stdin=true
# commit message to gitlint via stdin or --commit-msg. Disabled by default. # commit message to gitlint via stdin or --commit-msg. Disabled by default.
staged=true staged=true
# Hard fail when the target commit range is empty. Note that gitlint will
# already fail by default on invalid commit ranges. This option is specifically
# to tell gitlint to fail on *valid but empty* commit ranges.
# Disabled by default.
fail-without-commits=true
# Enable debug mode (prints more output). Disabled by default. # Enable debug mode (prints more output). Disabled by default.
debug=true debug=true
@ -128,7 +134,7 @@ ignore=T1,body-min-length
[ignore-by-body] [ignore-by-body]
# Ignore certain rules for commits of which the body has a line that matches a regex # Ignore certain rules for commits of which the body has a line that matches a regex
# E.g. Match bodies that have a line that that contain "release" # E.g. Match bodies that have a line that that contain "release"
# regex=(.*)release(.*) regex=(.*)release(.*)
# #
# Ignore certain rules, you can reference them by their id or by their full name # Ignore certain rules, you can reference them by their id or by their full name
# Use 'all' to ignore all rules # Use 'all' to ignore all rules
@ -139,6 +145,15 @@ ignore=T1,body-min-length
# E.g. Ignore all lines that start with 'Co-Authored-By' # E.g. Ignore all lines that start with 'Co-Authored-By'
regex=^Co-Authored-By regex=^Co-Authored-By
[ignore-by-author-name]
# Ignore certain rules for commits of which the author name matches a regex
# E.g. Match commits made by dependabot
regex=(.*)dependabot(.*)
# Ignore certain rules, you can reference them by their id or by their full name
# Use 'all' to ignore all rules
ignore=T1,body-min-length
# This is a contrib rule - a community contributed rule. These are disabled by default. # This is a contrib rule - a community contributed rule. These are disabled by default.
# You need to explicitly enable them one-by-one by adding them to the "contrib" option # You need to explicitly enable them one-by-one by adding them to the "contrib" option
# under [general] section above. # under [general] section above.
@ -363,6 +378,30 @@ GITLINT_STAGED=1 gitlint # using env variable
staged=true staged=true
``` ```
### fail-without-commits
Hard fail when the target commit range is empty. Note that gitlint will
already fail by default on invalid commit ranges. This option is specifically
to tell gitlint to fail on **valid but empty** commit ranges.
Default value | gitlint version | commandline flag | environment variable
---------------|------------------|---------------------------|-----------------------
false | >= 0.15.2 | `--fail-without-commits` | `GITLINT_FAIL_WITHOUT_COMMITS`
#### Examples
```sh
# CLI
# The following will cause gitlint to hard fail (i.e. exit code > 0)
# since HEAD..HEAD is a valid but empty commit range.
gitlint --fail-without-commits --commits HEAD..HEAD
GITLINT_FAIL_WITHOUT_COMMITS=1 gitlint # using env variable
```
```ini
#.gitlint
[general]
fail-without-commits=true
```
### ignore-stdin ### ignore-stdin
Ignore any stdin data. Sometimes useful when running gitlint in a CI server. Ignore any stdin data. Sometimes useful when running gitlint in a CI server.

View file

@ -44,7 +44,9 @@ vagrant ssh
Or you can choose to use your local environment: Or you can choose to use your local environment:
```sh ```sh
virtualenv .venv python -m venv .venv
. .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt -r test-requirements.txt -r doc-requirements.txt pip install -r requirements.txt -r test-requirements.txt -r doc-requirements.txt
python setup.py develop python setup.py develop
``` ```

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

View file

@ -38,12 +38,11 @@ useful throughout the years.
# Pip is recommended to install the latest version # Pip is recommended to install the latest version
pip install gitlint pip install gitlint
# macOS # Community maintained packages:
brew install gitlint brew install gitlint # Homebrew (macOS)
sudo port install gitlint # alternative using macports sudo port install gitlint # Macports (macOS)
apt-get install gitlint # Ubuntu
# Ubuntu # Other package managers, see https://repology.org/project/gitlint/versions
apt-get install gitlint
# Docker: https://hub.docker.com/r/jorisroovers/gitlint # Docker: https://hub.docker.com/r/jorisroovers/gitlint
docker run --ulimit nofile=1024 -v $(pwd):/repo jorisroovers/gitlint docker run --ulimit nofile=1024 -v $(pwd):/repo jorisroovers/gitlint
@ -134,8 +133,9 @@ Options:
current working directory] current working directory]
-C, --config FILE Config file location [default: .gitlint] -C, --config FILE Config file location [default: .gitlint]
-c TEXT Config flags in format <rule>.<option>=<value> -c TEXT Config flags in format <rule>.<option>=<value>
(e.g.: -c T1.line-length=80). Flag can be used (e.g.: -c T1.line-length=80). Flag can be
multiple times to set multiple config values. used multiple times to set multiple config values.
--commit TEXT Hash (SHA) of specific commit to lint.
--commits TEXT The range of commits to lint. [default: HEAD] --commits TEXT The range of commits to lint. [default: HEAD]
-e, --extra-path PATH Path to a directory or python module with extra -e, --extra-path PATH Path to a directory or python module with extra
user-defined rules user-defined rules
@ -147,10 +147,11 @@ Options:
server. server.
--staged Read staged commit meta-info from the local --staged Read staged commit meta-info from the local
repository. repository.
-v, --verbose Verbosity, more v's for more verbose output (e.g.: --fail-without-commits Hard fail when the target commit range is empty.
-v, -vv, -vvv). [default: -vvv] -v, --verbose Verbosity, more v's for more verbose output
-s, --silent Silent mode (no output). Takes precedence over -v, (e.g.: -v, -vv, -vvv). [default: -vvv]
-vv, -vvv. -s, --silent Silent mode (no output).
Takes precedence over -v, -vv, -vvv.
-d, --debug Enable debugging output. -d, --debug Enable debugging output.
--version Show the version and exit. --version Show the version and exit.
--help Show this message and exit. --help Show this message and exit.
@ -159,6 +160,7 @@ Commands:
generate-config Generates a sample gitlint config file. generate-config Generates a sample gitlint config file.
install-hook Install gitlint as a git commit-msg hook. install-hook Install gitlint as a git commit-msg hook.
lint Lints a git repository [default command] lint Lints a git repository [default command]
run-hook Runs the gitlint commit-msg hook.
uninstall-hook Uninstall gitlint commit-msg hook. uninstall-hook Uninstall gitlint commit-msg hook.
When no COMMAND is specified, gitlint defaults to 'gitlint lint'. When no COMMAND is specified, gitlint defaults to 'gitlint lint'.
@ -246,19 +248,21 @@ git log -1 --pretty=%B 62c0519 | gitlint
Note that gitlint requires that you specify `--pretty=%B` (=only print the log message, not the metadata), Note that gitlint requires that you specify `--pretty=%B` (=only print the log message, not the metadata),
future versions of gitlint might fix this and not require the `--pretty` argument. future versions of gitlint might fix this and not require the `--pretty` argument.
## Linting a range of commits ## Linting specific commits
_Introduced in gitlint v0.9.0 (experimental in v0.8.0)_ Gitlint allows users to lint a specific commit:
```sh
gitlint --commit 019cf40580a471a3958d3c346aa8bfd265fe5e16
gitlint --commit 019cf40 # short SHAs work too
```
Gitlint allows users to lint a number of commits at once like so: You can also lint multiple commits at once like so:
```sh ```sh
# Lint a specific commit range: # Lint a specific commit range:
gitlint --commits "019cf40...d6bc75a" gitlint --commits "019cf40...d6bc75a"
# You can also use git's special references: # You can also use git's special references:
gitlint --commits "origin..HEAD" gitlint --commits "origin..HEAD"
# Or specify a single specific commit in refspec format, like so:
gitlint --commits "019cf40^...019cf40"
``` ```
The `--commits` flag takes a **single** refspec argument or commit range. Basically, any range that is understood The `--commits` flag takes a **single** refspec argument or commit range. Basically, any range that is understood
@ -271,9 +275,8 @@ script to lint an arbitrary set of commits, like shown in the example below.
#!/bin/sh #!/bin/sh
for commit in $(git rev-list master); do for commit in $(git rev-list master); do
commit_msg=$(git log -1 --pretty=%B $commit) echo "Commit $commit"
echo "$commit" gitlint --commit $commit
echo "$commit_msg" | gitlint
echo "--------" echo "--------"
done done
``` ```
@ -309,7 +312,6 @@ general `ignore-merge-commits`, `ignore-revert-commits`, `ignore-fixup-commits`
[using one of the various ways to configure gitlint](configuration.md). [using one of the various ways to configure gitlint](configuration.md).
## Ignoring commits ## Ignoring commits
_Introduced in gitlint v0.10.0_
You can configure gitlint to ignore specific commits or parts of a commit. You can configure gitlint to ignore specific commits or parts of a commit.
@ -317,8 +319,7 @@ One way to do this, is to by [adding a gitline-ignore line to your commit messag
If you have a case where you want to ignore a certain type of commits all-together, you can If you have a case where you want to ignore a certain type of commits all-together, you can
use gitlint's *ignore* rules. use gitlint's *ignore* rules.
Here's an example gitlint file that configures gitlint to ignore rules `title-max-length` and `body-min-length` Here's a few examples snippets from a `.gitlint` file:
for all commits with a title starting with *"Release"*.
```ini ```ini
[ignore-by-title] [ignore-by-title]
@ -332,6 +333,11 @@ ignore=title-max-length,body-min-length
# Match commits message bodies that have a line that contains 'release' # Match commits message bodies that have a line that contains 'release'
regex=(.*)release(.*) regex=(.*)release(.*)
ignore=all ignore=all
[ignore-by-author-name]
# Match commits by author name (e.g. ignore all rules when a commit is made by dependabot)
regex=dependabot
ignore=all
``` ```
If you just want to ignore certain lines in a commit, you can do that using the If you just want to ignore certain lines in a commit, you can do that using the

View file

@ -30,6 +30,8 @@ M1 | author-valid-email | >= 0.9.0 | Author email address m
I1 | ignore-by-title | >= 0.10.0 | Ignore a commit based on matching its title I1 | ignore-by-title | >= 0.10.0 | Ignore a commit based on matching its title
I2 | ignore-by-body | >= 0.10.0 | Ignore a commit based on matching its body I2 | ignore-by-body | >= 0.10.0 | Ignore a commit based on matching its body
I3 | ignore-body-lines | >= 0.14.0 | Ignore certain lines in a commit body that match a regex I3 | ignore-body-lines | >= 0.14.0 | Ignore certain lines in a commit body that match a regex
I4 | ignore-by-author-name | >= 0.16.0 | Ignore a commit based on matching its author name
## T1: title-max-length ## T1: title-max-length
@ -405,3 +407,32 @@ regex=(^Co-Authored-By)|(^Signed-off-by)
[ignore-body-lines] [ignore-body-lines]
regex=(.*)foobar(.*) regex=(.*)foobar(.*)
``` ```
## I4: ignore-by-author-name
ID | Name | gitlint version | Description
------|---------------------------|-----------------|-------------------------------------------
I4 | ignore-by-author-name | >= 0.16.0 | Ignore a commit based on matching its author name.
### Options
Name | gitlint version | Default | Description
----------------------|-------------------|------------------------------|----------------------------------
regex | >= 0.16.0 | None | [Python regex](https://docs.python.org/library/re.html) to match against the commit author name. On match, the commit will be ignored.
ignore | >= 0.16.0 | all | Comma-separated list of rule names or ids to ignore when this rule is matched.
### Examples
#### .gitlint
```ini
# Ignore all commits authored by dependabot
[ignore-by-author-name]
regex=dependabot
# For commits made by anyone with "[bot]" in their name, ignore
# rules T1, body-min-length and B6
[ignore-by-author-name]
regex=(.*)\[bot\](.*)
ignore=T1,body-min-length,B6
```

View file

@ -1 +1 @@
__version__ = "0.15.1" __version__ = "0.16.0"

View file

@ -18,6 +18,7 @@ from gitlint.utils import LOG_FORMAT
from gitlint.exception import GitlintError from gitlint.exception import GitlintError
# Error codes # Error codes
GITLINT_SUCCESS = 0
MAX_VIOLATION_ERROR_CODE = 252 MAX_VIOLATION_ERROR_CODE = 252
USAGE_ERROR_CODE = 253 USAGE_ERROR_CODE = 253
GIT_CONTEXT_ERROR_CODE = 254 GIT_CONTEXT_ERROR_CODE = 254
@ -61,7 +62,8 @@ def log_system_info():
def build_config( # pylint: disable=too-many-arguments def build_config( # pylint: disable=too-many-arguments
target, config_path, c, extra_path, ignore, contrib, ignore_stdin, staged, verbose, silent, debug 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. """ """ Creates a LintConfig object based on a set of commandline parameters. """
config_builder = LintConfigBuilder() config_builder = LintConfigBuilder()
@ -102,6 +104,9 @@ def build_config( # pylint: disable=too-many-arguments
if staged: if staged:
config_builder.set_option('general', 'staged', 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() config = config_builder.build()
return config, config_builder return config, config_builder
@ -139,7 +144,7 @@ def get_stdin_data():
return False return False
def build_git_context(lint_config, msg_filename, refspec): def build_git_context(lint_config, msg_filename, commit_hash, refspec):
""" Builds a git context based on passed parameters and order of precedence """ """ Builds a git context based on passed parameters and order of precedence """
# Determine which GitContext method to use if a custom message is passed # Determine which GitContext method to use if a custom message is passed
@ -168,7 +173,11 @@ def build_git_context(lint_config, msg_filename, refspec):
# 3. Fallback to reading from local repository # 3. Fallback to reading from local repository
LOG.debug("No --msg-filename flag, no or empty data passed to stdin. Using the local repo.") 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)
if commit_hash and refspec:
raise GitLintUsageError("--commit and --commits are mutually exclusive, use one or the other.")
return GitContext.from_local_repository(lint_config.target, refspec=refspec, commit_hash=commit_hash)
def handle_gitlint_error(ctx, exc): def handle_gitlint_error(ctx, exc):
@ -187,9 +196,10 @@ def handle_gitlint_error(ctx, exc):
class ContextObj: class ContextObj:
""" Simple class to hold data that is passed between Click commands via the Click context. """ """ 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): def __init__(self, config, config_builder, commit_hash, refspec, msg_filename, gitcontext=None):
self.config = config self.config = config
self.config_builder = config_builder self.config_builder = config_builder
self.commit_hash = commit_hash
self.refspec = refspec self.refspec = refspec
self.msg_filename = msg_filename self.msg_filename = msg_filename
self.gitcontext = gitcontext self.gitcontext = gitcontext
@ -205,6 +215,7 @@ class ContextObj:
@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). " + 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 "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, help="The range of commits to lint. [default: HEAD]") @click.option('--commits', envvar='GITLINT_COMMITS', default=None, help="The range of commits 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", help="Path to a directory or python module with extra user-defined rules",
@ -217,6 +228,8 @@ class ContextObj:
help="Ignore any stdin data. Useful for running in CI server.") 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="Read staged commit meta-info from the local repository.") help="Read staged commit meta-info from the local repository.")
@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, @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]", ) 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('-s', '--silent', envvar='GITLINT_SILENT', is_flag=True,
@ -225,8 +238,9 @@ class ContextObj:
@click.version_option(version=gitlint.__version__) @click.version_option(version=gitlint.__version__)
@click.pass_context @click.pass_context
def cli( # pylint: disable=too-many-arguments def cli( # pylint: disable=too-many-arguments
ctx, target, config, c, commits, extra_path, ignore, contrib, ctx, target, config, c, commit, commits, extra_path, ignore, contrib,
msg_filename, ignore_stdin, staged, verbose, silent, debug, msg_filename, ignore_stdin, staged, fail_without_commits, verbose,
silent, debug,
): ):
""" Git lint tool, checks your git commit messages for styling issues """ Git lint tool, checks your git commit messages for styling issues
@ -242,11 +256,11 @@ def cli( # pylint: disable=too-many-arguments
# Get the lint config from the commandline parameters and # Get the lint config from the commandline parameters and
# store it in the context (click allows storing an arbitrary object in ctx.obj). # store it in the context (click allows storing an arbitrary object in ctx.obj).
config, config_builder = build_config(target, config, c, extra_path, ignore, contrib, config, config_builder = build_config(target, config, c, extra_path, ignore, contrib, ignore_stdin, staged,
ignore_stdin, staged, verbose, silent, debug) fail_without_commits, verbose, silent, debug)
LOG.debug("Configuration\n%s", config) LOG.debug("Configuration\n%s", config)
ctx.obj = ContextObj(config, config_builder, commits, msg_filename) ctx.obj = ContextObj(config, config_builder, commit, commits, msg_filename)
# If no subcommand is specified, then just lint # If no subcommand is specified, then just lint
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
@ -262,9 +276,10 @@ def lint(ctx):
""" Lints a git repository [default command] """ """ Lints a git repository [default command] """
lint_config = ctx.obj.config lint_config = ctx.obj.config
refspec = ctx.obj.refspec refspec = ctx.obj.refspec
commit_hash = ctx.obj.commit_hash
msg_filename = ctx.obj.msg_filename msg_filename = ctx.obj.msg_filename
gitcontext = build_git_context(lint_config, msg_filename, refspec) gitcontext = build_git_context(lint_config, msg_filename, commit_hash, refspec)
# Set gitcontext in the click context, so we can use it in command that are ran after this # Set gitcontext in the click context, so we can use it in command that are ran after this
# in particular, this is used by run-hook # in particular, this is used by run-hook
ctx.obj.gitcontext = gitcontext ctx.obj.gitcontext = gitcontext
@ -273,17 +288,20 @@ def lint(ctx):
# Exit if we don't have commits in the specified range. Use a 0 exit code, since a popular use-case is one # Exit if we don't have commits in the specified range. Use a 0 exit code, since a popular use-case is one
# where users are using --commits in a check job to check the commit messages inside a CI job. By returning 0, we # where users are using --commits in a check job to check the commit messages inside a CI job. By returning 0, we
# ensure that these jobs don't fail if for whatever reason the specified commit range is empty. # ensure that these jobs don't fail if for whatever reason the specified commit range is empty.
# This behavior can be overridden by using the --fail-without-commits flag.
if number_of_commits == 0: if number_of_commits == 0:
LOG.debug(u'No commits in range "%s"', refspec) LOG.debug('No commits in range "%s"', refspec)
ctx.exit(0) if lint_config.fail_without_commits:
raise GitLintUsageError(f'No commits in range "{refspec}"')
ctx.exit(GITLINT_SUCCESS)
LOG.debug(u'Linting %d commit(s)', number_of_commits) LOG.debug('Linting %d commit(s)', number_of_commits)
general_config_builder = ctx.obj.config_builder general_config_builder = ctx.obj.config_builder
last_commit = gitcontext.commits[-1] last_commit = gitcontext.commits[-1]
# Let's get linting! # Let's get linting!
first_violation = True first_violation = True
exit_code = 0 exit_code = GITLINT_SUCCESS
for commit in gitcontext.commits: for commit in gitcontext.commits:
# Build a config_builder taking into account the commit specific config (if any) # Build a config_builder taking into account the commit specific config (if any)
config_builder = general_config_builder.clone() config_builder = general_config_builder.clone()
@ -301,10 +319,8 @@ def lint(ctx):
if violations: if violations:
# Display the commit hash & new lines intelligently # Display the commit hash & new lines intelligently
if number_of_commits > 1 and commit.sha: if number_of_commits > 1 and commit.sha:
linter.display.e("{0}Commit {1}:".format( commit_separator = "\n" if not first_violation or commit is last_commit else ""
"\n" if not first_violation or commit is last_commit else "", linter.display.e(f"{commit_separator}Commit {commit.sha[:10]}:")
commit.sha[:10]
))
linter.print_violations(violations) linter.print_violations(violations)
first_violation = False first_violation = False
@ -323,7 +339,7 @@ def install_hook(ctx):
hooks.GitHookInstaller.install_commit_msg_hook(ctx.obj.config) hooks.GitHookInstaller.install_commit_msg_hook(ctx.obj.config)
hook_path = hooks.GitHookInstaller.commit_msg_hook_path(ctx.obj.config) hook_path = hooks.GitHookInstaller.commit_msg_hook_path(ctx.obj.config)
click.echo(f"Successfully installed gitlint commit-msg hook in {hook_path}") click.echo(f"Successfully installed gitlint commit-msg hook in {hook_path}")
ctx.exit(0) ctx.exit(GITLINT_SUCCESS)
except hooks.GitHookInstallerError as e: except hooks.GitHookInstallerError as e:
click.echo(e, err=True) click.echo(e, err=True)
ctx.exit(GIT_CONTEXT_ERROR_CODE) ctx.exit(GIT_CONTEXT_ERROR_CODE)
@ -337,7 +353,7 @@ def uninstall_hook(ctx):
hooks.GitHookInstaller.uninstall_commit_msg_hook(ctx.obj.config) hooks.GitHookInstaller.uninstall_commit_msg_hook(ctx.obj.config)
hook_path = hooks.GitHookInstaller.commit_msg_hook_path(ctx.obj.config) hook_path = hooks.GitHookInstaller.commit_msg_hook_path(ctx.obj.config)
click.echo(f"Successfully uninstalled gitlint commit-msg hook from {hook_path}") click.echo(f"Successfully uninstalled gitlint commit-msg hook from {hook_path}")
ctx.exit(0) ctx.exit(GITLINT_SUCCESS)
except hooks.GitHookInstallerError as e: except hooks.GitHookInstallerError as e:
click.echo(e, err=True) click.echo(e, err=True)
ctx.exit(GIT_CONTEXT_ERROR_CODE) ctx.exit(GIT_CONTEXT_ERROR_CODE)
@ -361,7 +377,7 @@ def run_hook(ctx):
sys.stdout.flush() sys.stdout.flush()
exit_code = e.exit_code exit_code = e.exit_code
if exit_code == 0: if exit_code == GITLINT_SUCCESS:
click.echo("gitlint: " + click.style("OK", fg='green') + " (no violations in commit message)") click.echo("gitlint: " + click.style("OK", fg='green') + " (no violations in commit message)")
continue continue
@ -387,7 +403,7 @@ def run_hook(ctx):
if value == "y": if value == "y":
LOG.debug("run-hook: commit message accepted") LOG.debug("run-hook: commit message accepted")
exit_code = 0 exit_code = GITLINT_SUCCESS
elif value == "e": elif value == "e":
LOG.debug("run-hook: editing commit message") LOG.debug("run-hook: editing commit message")
msg_filename = ctx.obj.msg_filename msg_filename = ctx.obj.msg_filename
@ -428,7 +444,7 @@ def generate_config(ctx):
LintConfigGenerator.generate_config(path) LintConfigGenerator.generate_config(path)
click.echo(f"Successfully generated {path}") click.echo(f"Successfully generated {path}")
ctx.exit(0) ctx.exit(GITLINT_SUCCESS)
# Let's Party! # Let's Party!

View file

@ -41,6 +41,7 @@ class LintConfig:
default_rule_classes = (rules.IgnoreByTitle, default_rule_classes = (rules.IgnoreByTitle,
rules.IgnoreByBody, rules.IgnoreByBody,
rules.IgnoreBodyLines, rules.IgnoreBodyLines,
rules.IgnoreByAuthorName,
rules.TitleMaxLength, rules.TitleMaxLength,
rules.TitleTrailingWhitespace, rules.TitleTrailingWhitespace,
rules.TitleLeadingWhitespace, rules.TitleLeadingWhitespace,
@ -76,6 +77,8 @@ class LintConfig:
ignore_stdin_description = "Ignore any stdin data. Useful for running in CI server." ignore_stdin_description = "Ignore any stdin data. Useful for running in CI server."
self._ignore_stdin = options.BoolOption('ignore-stdin', False, ignore_stdin_description) self._ignore_stdin = options.BoolOption('ignore-stdin', False, ignore_stdin_description)
self._staged = options.BoolOption('staged', False, "Read staged commit meta-info from the local repository.") self._staged = options.BoolOption('staged', False, "Read staged commit meta-info from the local repository.")
self._fail_without_commits = options.BoolOption('fail-without-commits', False,
"Hard fail when the target commit range is empty")
@property @property
def target(self): def target(self):
@ -170,6 +173,15 @@ class LintConfig:
def staged(self, value): def staged(self, value):
return self._staged.set(value) return self._staged.set(value)
@property
def fail_without_commits(self):
return self._fail_without_commits.value
@fail_without_commits.setter
@handle_option_error
def fail_without_commits(self, value):
return self._fail_without_commits.set(value)
@property @property
def extra_path(self): def extra_path(self):
return self._extra_path.value if self._extra_path else None return self._extra_path.value if self._extra_path else None
@ -275,6 +287,7 @@ class LintConfig:
self.ignore_revert_commits == other.ignore_revert_commits and \ self.ignore_revert_commits == other.ignore_revert_commits and \
self.ignore_stdin == other.ignore_stdin and \ self.ignore_stdin == other.ignore_stdin and \
self.staged == other.staged and \ self.staged == other.staged and \
self.fail_without_commits == other.fail_without_commits and \
self.debug == other.debug and \ self.debug == other.debug and \
self.ignore == other.ignore and \ self.ignore == other.ignore and \
self._config_path == other._config_path # noqa self._config_path == other._config_path # noqa
@ -292,6 +305,7 @@ class LintConfig:
f"ignore-revert-commits: {self.ignore_revert_commits}\n" f"ignore-revert-commits: {self.ignore_revert_commits}\n"
f"ignore-stdin: {self.ignore_stdin}\n" f"ignore-stdin: {self.ignore_stdin}\n"
f"staged: {self.staged}\n" f"staged: {self.staged}\n"
f"fail-without-commits: {self.fail_without_commits}\n"
f"verbosity: {self.verbosity}\n" f"verbosity: {self.verbosity}\n"
f"debug: {self.debug}\n" f"debug: {self.debug}\n"
f"target: {self.target}\n" f"target: {self.target}\n"

View file

@ -3,7 +3,7 @@ import re
from gitlint.options import ListOption from gitlint.options import ListOption
from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation
RULE_REGEX = re.compile(r"[^(]+?(\([^)]+?\))?: .+") RULE_REGEX = re.compile(r"([^(]+?)(\([^)]+?\))?!?: .+")
class ConventionalCommit(LineRule): class ConventionalCommit(LineRule):
@ -23,16 +23,15 @@ class ConventionalCommit(LineRule):
def validate(self, line, _commit): def validate(self, line, _commit):
violations = [] violations = []
match = RULE_REGEX.match(line)
for commit_type in self.options["types"].value: if not match:
if line.startswith(commit_type):
break
else:
msg = "Title does not start with one of {0}".format(', '.join(self.options['types'].value))
violations.append(RuleViolation(self.id, msg, line))
if not RULE_REGEX.match(line):
msg = "Title does not follow ConventionalCommits.org format 'type(optional-scope): description'" msg = "Title does not follow ConventionalCommits.org format 'type(optional-scope): description'"
violations.append(RuleViolation(self.id, msg, line)) violations.append(RuleViolation(self.id, msg, line))
else:
line_commit_type = match.group(1)
if line_commit_type not in self.options["types"].value:
opt_str = ', '.join(self.options['types'].value)
violations.append(RuleViolation(self.id, f"Title does not start with one of {opt_str}", line))
return violations return violations

View file

@ -27,6 +27,12 @@
# commit message to gitlint via stdin or --commit-msg. Disabled by default. # commit message to gitlint via stdin or --commit-msg. Disabled by default.
# staged=true # staged=true
# Hard fail when the target commit range is empty. Note that gitlint will
# already fail by default on invalid commit ranges. This option is specifically
# to tell gitlint to fail on *valid but empty* commit ranges.
# Disabled by default.
# fail-without-commits=true
# Enable debug mode (prints more output). Disabled by default. # Enable debug mode (prints more output). Disabled by default.
# debug=true # debug=true
@ -111,6 +117,15 @@
# E.g. Ignore all lines that start with 'Co-Authored-By' # E.g. Ignore all lines that start with 'Co-Authored-By'
# regex=^Co-Authored-By # regex=^Co-Authored-By
# [ignore-by-author-name]
# Ignore certain rules for commits of which the author name matches a regex
# E.g. Match commits made by dependabot
# regex=(.*)dependabot(.*)
#
# Ignore certain rules, you can reference them by their id or by their full name
# Use 'all' to ignore all rules
# ignore=T1,body-min-length
# This is a contrib rule - a community contributed rule. These are disabled by default. # This is a contrib rule - a community contributed rule. These are disabled by default.
# You need to explicitly enable them one-by-one by adding them to the "contrib" option # You need to explicitly enable them one-by-one by adding them to the "contrib" option
# under [general] section above. # under [general] section above.

View file

@ -364,22 +364,27 @@ class GitContext(PropertyCache):
return context return context
@staticmethod @staticmethod
def from_local_repository(repository_path, refspec=None): def from_local_repository(repository_path, refspec=None, commit_hash=None):
""" Retrieves the git context from a local git repository. """ Retrieves the git context from a local git repository.
:param repository_path: Path to the git repository to retrieve the context from :param repository_path: Path to the git repository to retrieve the context from
:param refspec: The commit(s) to retrieve :param refspec: The commit(s) to retrieve (mutually exclusive with `commit_sha`)
:param commit_hash: Hash of the commit to retrieve (mutually exclusive with `refspec`)
""" """
context = GitContext(repository_path=repository_path) context = GitContext(repository_path=repository_path)
# If no refspec is defined, fallback to the last commit on the current branch if refspec:
if refspec is None: sha_list = _git("rev-list", refspec, _cwd=repository_path).split()
elif commit_hash: # Single commit, just pass it to `git log -1`
# Even though we have already been passed the commit hash, we ask git to retrieve this hash and
# return it to us. This way we verify that the passed hash is a valid hash for the target repo and we
# also convert it to the full hash format (we might have been passed a short hash).
sha_list = [_git("log", "-1", commit_hash, "--pretty=%H", _cwd=repository_path).replace("\n", "")]
else: # If no refspec is defined, fallback to the last commit on the current branch
# We tried many things here e.g.: defaulting to e.g. HEAD or HEAD^... (incl. dealing with # We tried many things here e.g.: defaulting to e.g. HEAD or HEAD^... (incl. dealing with
# repos that only have a single commit - HEAD^... doesn't work there), but then we still get into # repos that only have a single commit - HEAD^... doesn't work there), but then we still get into
# problems with e.g. merge commits. Easiest solution is just taking the SHA from `git log -1`. # problems with e.g. merge commits. Easiest solution is just taking the SHA from `git log -1`.
sha_list = [_git("log", "-1", "--pretty=%H", _cwd=repository_path).replace("\n", "")] sha_list = [_git("log", "-1", "--pretty=%H", _cwd=repository_path).replace("\n", "")]
else:
sha_list = _git("rev-list", refspec, _cwd=repository_path).split()
for sha in sha_list: for sha in sha_list:
commit = LocalGitCommit(context, sha) commit = LocalGitCommit(context, sha)

View file

@ -141,7 +141,7 @@ class LineMustNotContainWord(LineRule):
strings = self.options['words'].value strings = self.options['words'].value
violations = [] violations = []
for string in strings: for string in strings:
regex = re.compile(r"\b%s\b" % string.lower(), re.IGNORECASE | re.UNICODE) regex = re.compile(rf"\b{string.lower()}\b", re.IGNORECASE | re.UNICODE)
match = regex.search(line.lower()) match = regex.search(line.lower())
if match: if match:
violations.append(RuleViolation(self.id, self.violation_message.format(string), line)) violations.append(RuleViolation(self.id, self.violation_message.format(string), line))
@ -416,3 +416,25 @@ class IgnoreBodyLines(ConfigurationRule):
commit.message.body = new_body 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):
name = "ignore-by-author-name"
id = "I4"
options_spec = [RegexOption('regex', None, "Regex matching the author name of commits this rule should apply to"),
StrOption('ignore', "all", "Comma-separated list of rules to ignore")]
def apply(self, config, commit):
# If no regex is specified, immediately return
if not self.options['regex'].value:
return
if self.options['regex'].value.match(commit.author_name):
config.ignore = self.options['ignore'].value
message = (f"Commit Author Name '{commit.author_name}' matches the regex "
f"'{self.options['regex'].value.pattern}', ignoring rules: {self.options['ignore'].value}")
self.log.debug("Ignoring commit because of rule '%s': %s", self.id, message)
# No need to check other lines if we found a match
return

View file

@ -11,8 +11,8 @@ from gitlint.utils import USE_SH_LIB, DEFAULT_ENCODING
def shell(cmd): def shell(cmd):
""" Convenience function that opens a given command in a shell. Does not use 'sh' library. """ """ Convenience function that opens a given command in a shell. Does not use 'sh' library. """
p = subprocess.Popen(cmd, shell=True) with subprocess.Popen(cmd, shell=True) as p:
p.communicate() p.communicate()
if USE_SH_LIB: if USE_SH_LIB:
@ -57,8 +57,8 @@ else:
popen_kwargs['cwd'] = kwargs['_cwd'] popen_kwargs['cwd'] = kwargs['_cwd']
try: try:
p = subprocess.Popen(args, **popen_kwargs) with subprocess.Popen(args, **popen_kwargs) as p:
result = p.communicate() result = p.communicate()
except FileNotFoundError as e: except FileNotFoundError as e:
raise CommandNotFound from e raise CommandNotFound from e

View file

@ -126,6 +126,10 @@ class BaseTestCase(unittest.TestCase):
""" """
return super().assertRaisesRegex(expected_exception, re.escape(expected_regex), *args, **kwargs) return super().assertRaisesRegex(expected_exception, re.escape(expected_regex), *args, **kwargs)
def clearlog(self):
""" Clears the log capture """
self.logcapture.clear()
@contextlib.contextmanager @contextlib.contextmanager
def assertRaisesMessage(self, expected_exception, expected_msg): # pylint: disable=invalid-name def assertRaisesMessage(self, expected_exception, expected_msg): # pylint: disable=invalid-name
""" Asserts an exception has occurred with a given error message """ """ Asserts an exception has occurred with a given error message """
@ -182,3 +186,6 @@ class LogCapture(logging.Handler):
def emit(self, record): def emit(self, record):
self.messages.append(self.format(record)) self.messages.append(self.format(record))
def clear(self):
self.messages = []

View file

@ -26,6 +26,7 @@ class CLITests(BaseTestCase):
USAGE_ERROR_CODE = 253 USAGE_ERROR_CODE = 253
GIT_CONTEXT_ERROR_CODE = 254 GIT_CONTEXT_ERROR_CODE = 254
CONFIG_ERROR_CODE = 255 CONFIG_ERROR_CODE = 255
GITLINT_SUCCESS_CODE = 0
def setUp(self): def setUp(self):
super(CLITests, self).setUp() super(CLITests, self).setUp()
@ -180,6 +181,39 @@ class CLITests(BaseTestCase):
self.assertEqual(stderr.getvalue(), expected) self.assertEqual(stderr.getvalue(), expected)
self.assertEqual(result.exit_code, 2) self.assertEqual(result.exit_code, 2)
@patch('gitlint.cli.get_stdin_data', return_value=False)
@patch('gitlint.git.sh')
def test_lint_commit(self, sh, _):
""" Test for --commit option """
sh.git.side_effect = [
"6f29bf81a8322a04071bb794666e48c443a90360\n", # git log -1 <SHA> --pretty=%H
# git log --pretty <FORMAT> <SHA>
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
"WIP: commït-title1\n\ncommït-body1",
"#", # git config --get core.commentchar
"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
]
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--commit", "foo"])
self.assertEqual(result.output, "")
self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_commit_1"))
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, _):
""" Negative test for --commit option """
# Try using --commit and --commits at the same time (not allowed)
result = self.cli.invoke(cli.cli, ["--commit", "foo", "--commits", "foo...bar"])
expected_output = "Error: --commit and --commits are mutually exclusive, use one or the other.\n"
self.assertEqual(result.output, expected_output)
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
@patch('gitlint.cli.get_stdin_data', return_value=u'WIP: tïtle \n') @patch('gitlint.cli.get_stdin_data', return_value=u'WIP: tïtle \n')
def test_input_stream(self, _): def test_input_stream(self, _):
""" Test for linting when a message is passed via stdin """ """ Test for linting when a message is passed via stdin """
@ -282,6 +316,30 @@ class CLITests(BaseTestCase):
self.assertEqual(result.output, ("Error: The 'staged' option (--staged) can only be used when using " self.assertEqual(result.output, ("Error: The 'staged' option (--staged) can only be used when using "
"'--msg-filename' or when piping data to gitlint via stdin.\n")) "'--msg-filename' or when piping data to gitlint via stdin.\n"))
@patch('gitlint.cli.get_stdin_data', return_value=False)
@patch('gitlint.git.sh')
def test_fail_without_commits(self, sh, _):
""" Test for --debug option """
sh.git.side_effect = [
"", # First invocation of git rev-list
"" # Second invocation of git rev-list
]
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
# By default, gitlint should silently exit with code GITLINT_SUCCESS when there are no commits
result = self.cli.invoke(cli.cli, ["--commits", "foo..bar"])
self.assertEqual(stderr.getvalue(), "")
self.assertEqual(result.exit_code, cli.GITLINT_SUCCESS)
self.assert_log_contains("DEBUG: gitlint.cli No commits in range \"foo..bar\"")
# When --fail-without-commits is set, gitlint should hard fail with code USAGE_ERROR_CODE
self.clearlog()
result = self.cli.invoke(cli.cli, ["--commits", "foo..bar", "--fail-without-commits"])
self.assertEqual(result.output, 'Error: No commits in range "foo..bar"\n')
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
self.assert_log_contains("DEBUG: gitlint.cli No commits in range \"foo..bar\"")
@patch('gitlint.cli.get_stdin_data', return_value=False) @patch('gitlint.cli.get_stdin_data', return_value=False)
def test_msg_filename(self, _): def test_msg_filename(self, _):
expected_output = "3: B6 Body message is missing\n" expected_output = "3: B6 Body message is missing\n"
@ -405,7 +463,7 @@ class CLITests(BaseTestCase):
result = self.cli.invoke(cli.cli, ["--contrib", "contrib-title-conventional-commits,CC1"]) result = self.cli.invoke(cli.cli, ["--contrib", "contrib-title-conventional-commits,CC1"])
expected_output = self.get_expected('cli/test_cli/test_contrib_1') expected_output = self.get_expected('cli/test_cli/test_contrib_1')
self.assertEqual(stderr.getvalue(), expected_output) self.assertEqual(stderr.getvalue(), expected_output)
self.assertEqual(result.exit_code, 3) self.assertEqual(result.exit_code, 2)
@patch('gitlint.cli.get_stdin_data', return_value="Test tïtle\n") @patch('gitlint.cli.get_stdin_data', return_value="Test tïtle\n")
def test_contrib_negative(self, _): def test_contrib_negative(self, _):
@ -475,7 +533,7 @@ class CLITests(BaseTestCase):
def test_generate_config(self, generate_config): def test_generate_config(self, generate_config):
""" Test for the generate-config subcommand """ """ Test for the generate-config subcommand """
result = self.cli.invoke(cli.cli, ["generate-config"], input="tëstfile\n") result = self.cli.invoke(cli.cli, ["generate-config"], input="tëstfile\n")
self.assertEqual(result.exit_code, 0) self.assertEqual(result.exit_code, self.GITLINT_SUCCESS_CODE)
expected_msg = "Please specify a location for the sample gitlint config file [.gitlint]: tëstfile\n" + \ expected_msg = "Please specify a location for the sample gitlint config file [.gitlint]: tëstfile\n" + \
f"Successfully generated {os.path.realpath('tëstfile')}\n" f"Successfully generated {os.path.realpath('tëstfile')}\n"
self.assertEqual(result.output, expected_msg) self.assertEqual(result.output, expected_msg)
@ -517,7 +575,7 @@ class CLITests(BaseTestCase):
result = self.cli.invoke(cli.cli, ["--commits", "master...HEAD"]) result = self.cli.invoke(cli.cli, ["--commits", "master...HEAD"])
self.assert_log_contains("DEBUG: gitlint.cli No commits in range \"master...HEAD\"") self.assert_log_contains("DEBUG: gitlint.cli No commits in range \"master...HEAD\"")
self.assertEqual(result.exit_code, 0) self.assertEqual(result.exit_code, self.GITLINT_SUCCESS_CODE)
@patch('gitlint.cli.get_stdin_data', return_value="WIP: tëst tïtle") @patch('gitlint.cli.get_stdin_data', return_value="WIP: tëst tïtle")
def test_named_rules(self, _): def test_named_rules(self, _):

View file

@ -50,6 +50,7 @@ class LintConfigTests(BaseTestCase):
self.assertFalse(config.ignore_stdin) self.assertFalse(config.ignore_stdin)
self.assertFalse(config.staged) self.assertFalse(config.staged)
self.assertFalse(config.fail_without_commits)
self.assertFalse(config.debug) self.assertFalse(config.debug)
self.assertEqual(config.verbosity, 3) self.assertEqual(config.verbosity, 3)
active_rule_classes = tuple(type(rule) for rule in config.rules) active_rule_classes = tuple(type(rule) for rule in config.rules)
@ -95,6 +96,10 @@ class LintConfigTests(BaseTestCase):
config.set_general_option("staged", "true") config.set_general_option("staged", "true")
self.assertTrue(config.staged) self.assertTrue(config.staged)
# fail-without-commits
config.set_general_option("fail-without-commits", "true")
self.assertTrue(config.fail_without_commits)
# target # target
config.set_general_option("target", self.SAMPLES_DIR) config.set_general_option("target", self.SAMPLES_DIR)
self.assertEqual(config.target, self.SAMPLES_DIR) self.assertEqual(config.target, self.SAMPLES_DIR)
@ -227,7 +232,7 @@ class LintConfigTests(BaseTestCase):
# splitting which means it it will accept just about everything # splitting which means it it will accept just about everything
# invalid boolean options # invalid boolean options
for attribute in ['debug', 'staged', 'ignore_stdin']: for attribute in ['debug', 'staged', 'ignore_stdin', 'fail_without_commits']:
option_name = attribute.replace("_", "-") option_name = attribute.replace("_", "-")
with self.assertRaisesMessage(LintConfigError, with self.assertRaisesMessage(LintConfigError,
f"Option '{option_name}' must be either 'true' or 'false'"): f"Option '{option_name}' must be either 'true' or 'false'"):

View file

@ -29,12 +29,34 @@ class ContribConventionalCommitTests(BaseTestCase):
violations = rule.validate("bår: foo", None) violations = rule.validate("bår: foo", None)
self.assertListEqual([expected_violation], violations) self.assertListEqual([expected_violation], violations)
# assert violation when use strange chars after correct type
expected_violation = RuleViolation("CT1", "Title does not start with one of fix, feat, chore, docs,"
" style, refactor, perf, test, revert, ci, build",
"feat_wrong_chars: föo")
violations = rule.validate("feat_wrong_chars: föo", None)
self.assertListEqual([expected_violation], violations)
# assert violation when use strange chars after correct type
expected_violation = RuleViolation("CT1", "Title does not start with one of fix, feat, chore, docs,"
" style, refactor, perf, test, revert, ci, build",
"feat_wrong_chars(scope): föo")
violations = rule.validate("feat_wrong_chars(scope): föo", None)
self.assertListEqual([expected_violation], violations)
# assert violation on wrong format # assert violation on wrong format
expected_violation = RuleViolation("CT1", "Title does not follow ConventionalCommits.org format " expected_violation = RuleViolation("CT1", "Title does not follow ConventionalCommits.org format "
"'type(optional-scope): description'", "fix föo") "'type(optional-scope): description'", "fix föo")
violations = rule.validate("fix föo", None) violations = rule.validate("fix föo", None)
self.assertListEqual([expected_violation], violations) self.assertListEqual([expected_violation], violations)
# assert no violation when use ! for breaking changes without scope
violations = rule.validate("feat!: föo", None)
self.assertListEqual([], violations)
# assert no violation when use ! for breaking changes with scope
violations = rule.validate("fix(scope)!: föo", None)
self.assertListEqual([], violations)
# assert no violation when adding new type # assert no violation when adding new type
rule = ConventionalCommit({'types': ["föo", "bär"]}) rule = ConventionalCommit({'types': ["föo", "bär"]})
for typ in ["föo", "bär"]: for typ in ["föo", "bär"]:
@ -45,3 +67,9 @@ class ContribConventionalCommitTests(BaseTestCase):
violations = rule.validate("fix: hür dur", None) violations = rule.validate("fix: hür dur", None)
expected_violation = RuleViolation("CT1", "Title does not start with one of föo, bär", "fix: hür dur") expected_violation = RuleViolation("CT1", "Title does not start with one of föo, bär", "fix: hür dur")
self.assertListEqual([expected_violation], violations) self.assertListEqual([expected_violation], violations)
# assert no violation when adding new type named with numbers
rule = ConventionalCommit({'types': ["föo123", "123bär"]})
for typ in ["föo123", "123bär"]:
violations = rule.validate(typ + ": hür dur", None)
self.assertListEqual([], violations)

View file

@ -1,3 +1,2 @@
1: CC1 Body does not contain a 'Signed-off-by' line 1: CC1 Body does not contain a 'Signed-off-by' line
1: CT1 Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert, ci, build: "Test tïtle"
1: CT1 Title does not follow ConventionalCommits.org format 'type(optional-scope): description': "Test tïtle" 1: CT1 Title does not follow ConventionalCommits.org format 'type(optional-scope): description': "Test tïtle"

View file

@ -17,6 +17,7 @@ ignore-squash-commits: True
ignore-revert-commits: True ignore-revert-commits: True
ignore-stdin: False ignore-stdin: False
staged: False staged: False
fail-without-commits: False
verbosity: 1 verbosity: 1
debug: True debug: True
target: {target} target: {target}
@ -29,6 +30,9 @@ target: {target}
regex=None regex=None
I3: ignore-body-lines I3: ignore-body-lines
regex=None regex=None
I4: ignore-by-author-name
ignore=all
regex=None
T1: title-max-length T1: title-max-length
line-length=20 line-length=20
T2: title-trailing-whitespace T2: title-trailing-whitespace

View file

@ -17,6 +17,7 @@ ignore-squash-commits: True
ignore-revert-commits: True ignore-revert-commits: True
ignore-stdin: False ignore-stdin: False
staged: False staged: False
fail-without-commits: False
verbosity: 3 verbosity: 3
debug: True debug: True
target: {target} target: {target}
@ -29,6 +30,9 @@ target: {target}
regex=None regex=None
I3: ignore-body-lines I3: ignore-body-lines
regex=None regex=None
I4: ignore-by-author-name
ignore=all
regex=None
T1: title-max-length T1: title-max-length
line-length=72 line-length=72
T2: title-trailing-whitespace T2: title-trailing-whitespace

View file

@ -0,0 +1,2 @@
1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: commït-title1"
3: B5 Body message is too short (12<20): "commït-body1"

View file

@ -17,6 +17,7 @@ ignore-squash-commits: True
ignore-revert-commits: True ignore-revert-commits: True
ignore-stdin: False ignore-stdin: False
staged: True staged: True
fail-without-commits: False
verbosity: 3 verbosity: 3
debug: True debug: True
target: {target} target: {target}
@ -29,6 +30,9 @@ target: {target}
regex=None regex=None
I3: ignore-body-lines I3: ignore-body-lines
regex=None regex=None
I4: ignore-by-author-name
ignore=all
regex=None
T1: title-max-length T1: title-max-length
line-length=72 line-length=72
T2: title-trailing-whitespace T2: title-trailing-whitespace

View file

@ -17,6 +17,7 @@ ignore-squash-commits: True
ignore-revert-commits: True ignore-revert-commits: True
ignore-stdin: False ignore-stdin: False
staged: True staged: True
fail-without-commits: False
verbosity: 3 verbosity: 3
debug: True debug: True
target: {target} target: {target}
@ -29,6 +30,9 @@ target: {target}
regex=None regex=None
I3: ignore-body-lines I3: ignore-body-lines
regex=None regex=None
I4: ignore-by-author-name
ignore=all
regex=None
T1: title-max-length T1: title-max-length
line-length=72 line-length=72
T2: title-trailing-whitespace T2: title-trailing-whitespace

View file

@ -17,6 +17,7 @@ ignore-squash-commits: True
ignore-revert-commits: True ignore-revert-commits: True
ignore-stdin: False ignore-stdin: False
staged: False staged: False
fail-without-commits: False
verbosity: 3 verbosity: 3
debug: True debug: True
target: {target} target: {target}
@ -29,6 +30,9 @@ target: {target}
regex=None regex=None
I3: ignore-body-lines I3: ignore-body-lines
regex=None regex=None
I4: ignore-by-author-name
ignore=all
regex=None
T1: title-max-length T1: title-max-length
line-length=72 line-length=72
T2: title-trailing-whitespace T2: title-trailing-whitespace

View file

@ -75,11 +75,12 @@ class GitCommitTests(BaseTestCase):
self.assertListEqual(sh.git.mock_calls, expected_calls) self.assertListEqual(sh.git.mock_calls, expected_calls)
@patch('gitlint.git.sh') @patch('gitlint.git.sh')
def test_from_local_repository_specific_ref(self, sh): def test_from_local_repository_specific_refspec(self, sh):
sample_sha = "myspecialref" sample_refspec = "åbc123..def456"
sample_sha = "åbc123"
sh.git.side_effect = [ sh.git.side_effect = [
sample_sha, sample_sha, # git rev-list <sample_refspec>
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n" "test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
"cömmit-title\n\ncömmit-body", "cömmit-title\n\ncömmit-body",
"#", # git config --get core.commentchar "#", # git config --get core.commentchar
@ -87,10 +88,10 @@ class GitCommitTests(BaseTestCase):
"foöbar\n* hürdur\n" "foöbar\n* hürdur\n"
] ]
context = GitContext.from_local_repository("fåke/path", sample_sha) context = GitContext.from_local_repository("fåke/path", refspec=sample_refspec)
# assert that commit info was read using git command # assert that commit info was read using git command
expected_calls = [ expected_calls = [
call("rev-list", sample_sha, **self.expected_sh_special_args), call("rev-list", sample_refspec, **self.expected_sh_special_args),
call("log", sample_sha, "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args), call("log", sample_sha, "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args),
call('config', '--get', 'core.commentchar', _ok_code=[0, 1], **self.expected_sh_special_args), call('config', '--get', 'core.commentchar', _ok_code=[0, 1], **self.expected_sh_special_args),
call('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', sample_sha, call('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', sample_sha,
@ -127,6 +128,59 @@ class GitCommitTests(BaseTestCase):
# All expected calls should've happened at this point # All expected calls should've happened at this point
self.assertListEqual(sh.git.mock_calls, expected_calls) self.assertListEqual(sh.git.mock_calls, expected_calls)
@patch('gitlint.git.sh')
def test_from_local_repository_specific_commit_hash(self, sh):
sample_hash = "åbc123"
sh.git.side_effect = [
sample_hash, # git log -1 <sample_hash>
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
"cömmit-title\n\ncömmit-body",
"#", # git config --get core.commentchar
"file1.txt\npåth/to/file2.txt\n",
"foöbar\n* hürdur\n"
]
context = GitContext.from_local_repository("fåke/path", commit_hash=sample_hash)
# assert that commit info was read using git command
expected_calls = [
call("log", "-1", sample_hash, "--pretty=%H", **self.expected_sh_special_args),
call("log", sample_hash, "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args),
call('config', '--get', 'core.commentchar', _ok_code=[0, 1], **self.expected_sh_special_args),
call('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', sample_hash,
**self.expected_sh_special_args),
call('branch', '--contains', sample_hash, **self.expected_sh_special_args)
]
# Only first 'git log' call should've happened at this point
self.assertEqual(sh.git.mock_calls, expected_calls[:1])
last_commit = context.commits[-1]
self.assertIsInstance(last_commit, LocalGitCommit)
self.assertEqual(last_commit.sha, sample_hash)
self.assertEqual(last_commit.message.title, "cömmit-title")
self.assertEqual(last_commit.message.body, ["", "cömmit-body"])
self.assertEqual(last_commit.author_name, "test åuthor")
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
self.assertEqual(last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15,
tzinfo=dateutil.tz.tzoffset("+0100", 3600)))
self.assertListEqual(last_commit.parents, ["åbc"])
self.assertFalse(last_commit.is_merge_commit)
self.assertFalse(last_commit.is_fixup_commit)
self.assertFalse(last_commit.is_squash_commit)
self.assertFalse(last_commit.is_revert_commit)
# First 2 'git log' calls should've happened at this point
self.assertListEqual(sh.git.mock_calls, expected_calls[:3])
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.txt"])
# 'git diff-tree' should have happened at this point
self.assertListEqual(sh.git.mock_calls, expected_calls[:4])
self.assertListEqual(last_commit.branches, ["foöbar", "hürdur"])
# All expected calls should've happened at this point
self.assertListEqual(sh.git.mock_calls, expected_calls)
@patch('gitlint.git.sh') @patch('gitlint.git.sh')
def test_get_latest_commit_merge_commit(self, sh): def test_get_latest_commit_merge_commit(self, sh):
sample_sha = "d8ac47e9f2923c7f22d8668e3a1ed04eb4cdbca9" sample_sha = "d8ac47e9f2923c7f22d8668e3a1ed04eb4cdbca9"

View file

@ -101,13 +101,13 @@ class BodyRuleTests(BaseTestCase):
expected_violation = rules.RuleViolation("B5", "Body message is too short (21<120)", "å" * 21, 3) expected_violation = rules.RuleViolation("B5", "Body message is too short (21<120)", "å" * 21, 3)
rule = rules.BodyMinLength({'min-length': 120}) rule = rules.BodyMinLength({'min-length': 120})
commit = self.gitcommit("Title\n\n%s\n" % ("å" * 21)) commit = self.gitcommit("Title\n\n{0}\n".format("å" * 21)) # pylint: disable=consider-using-f-string
violations = rule.validate(commit) violations = rule.validate(commit)
self.assertListEqual(violations, [expected_violation]) self.assertListEqual(violations, [expected_violation])
# Make sure we don't get the error if the body-length is exactly the min-length # Make sure we don't get the error if the body-length is exactly the min-length
rule = rules.BodyMinLength({'min-length': 8}) rule = rules.BodyMinLength({'min-length': 8})
commit = self.gitcommit("Tïtle\n\n%s\n" % ("å" * 8)) commit = self.gitcommit("Tïtle\n\n{0}\n".format("å" * 8)) # pylint: disable=consider-using-f-string
violations = rule.validate(commit) violations = rule.validate(commit)
self.assertIsNone(violations) self.assertIsNone(violations)
@ -182,7 +182,7 @@ class BodyRuleTests(BaseTestCase):
expected_violation = rules.RuleViolation("B7", "Body does not mention changed file 'föo/test.py'", None, 4) expected_violation = rules.RuleViolation("B7", "Body does not mention changed file 'föo/test.py'", None, 4)
self.assertEqual([expected_violation], violations) self.assertEqual([expected_violation], violations)
# assert multiple errors if multiple files habe changed and are not mentioned # assert multiple errors if multiple files have changed and are not mentioned
commit_msg = "This is å test\n\nHere is a mention of\nAnd here is a mention of" commit_msg = "This is å test\n\nHere is a mention of\nAnd here is a mention of"
commit = self.gitcommit(commit_msg, ["föo/test.py", "bar.txt"]) commit = self.gitcommit(commit_msg, ["föo/test.py", "bar.txt"])
violations = rule.validate(commit) violations = rule.validate(commit)

View file

@ -71,6 +71,39 @@ class ConfigurationRuleTests(BaseTestCase):
"Commit message line ' a relëase body' matches the regex '(.*)relëase(.*)', ignoring rules: T1,B2" "Commit message line ' a relëase body' matches the regex '(.*)relëase(.*)', ignoring rules: T1,B2"
self.assert_log_contains(expected_log_message) self.assert_log_contains(expected_log_message)
def test_ignore_by_author_name(self):
commit = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line", author_name="Tëst nåme")
# No regex specified -> Config shouldn't be changed
rule = rules.IgnoreByAuthorName()
config = LintConfig()
rule.apply(config, commit)
self.assertEqual(config, LintConfig())
self.assert_logged([]) # nothing logged -> nothing ignored
# Matching regex -> expect config to ignore all rules
rule = rules.IgnoreByAuthorName({"regex": "(.*)ëst(.*)"})
expected_config = LintConfig()
expected_config.ignore = "all"
rule.apply(config, commit)
self.assertEqual(config, expected_config)
expected_log_message = ("DEBUG: gitlint.rules Ignoring commit because of rule 'I4': "
"Commit Author Name 'Tëst nåme' matches the regex '(.*)ëst(.*)',"
" ignoring rules: all")
self.assert_log_contains(expected_log_message)
# Matching regex with specific ignore
rule = rules.IgnoreByAuthorName({"regex": "(.*)nåme", "ignore": "T1,B2"})
expected_config = LintConfig()
expected_config.ignore = "T1,B2"
rule.apply(config, commit)
self.assertEqual(config, expected_config)
expected_log_message = ("DEBUG: gitlint.rules Ignoring commit because of rule 'I4': "
"Commit Author Name 'Tëst nåme' matches the regex '(.*)nåme', ignoring rules: T1,B2")
self.assert_log_contains(expected_log_message)
def test_ignore_body_lines(self): def test_ignore_body_lines(self):
commit1 = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line") commit1 = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line")
commit2 = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line") commit2 = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line")

View file

@ -79,7 +79,7 @@ class TitleRuleTests(BaseTestCase):
violations = rule.validate("This is å test", None) violations = rule.validate("This is å test", None)
self.assertIsNone(violations) self.assertIsNone(violations)
# no violation if WIP occurs inside a wor # no violation if WIP occurs inside a word
violations = rule.validate("This is å wiping test", None) violations = rule.validate("This is å wiping test", None)
self.assertIsNone(violations) self.assertIsNone(violations)

View file

@ -97,7 +97,7 @@ class UserRuleTests(BaseTestCase):
def test_assert_valid_rule_class(self): def test_assert_valid_rule_class(self):
class MyLineRuleClass(rules.LineRule): class MyLineRuleClass(rules.LineRule):
id = 'UC1' id = 'UC1'
name = u'my-lïne-rule' name = 'my-lïne-rule'
target = rules.CommitMessageTitle target = rules.CommitMessageTitle
def validate(self): def validate(self):
@ -105,14 +105,14 @@ class UserRuleTests(BaseTestCase):
class MyCommitRuleClass(rules.CommitRule): class MyCommitRuleClass(rules.CommitRule):
id = 'UC2' id = 'UC2'
name = u'my-cömmit-rule' name = 'my-cömmit-rule'
def validate(self): def validate(self):
pass pass
class MyConfigurationRuleClass(rules.ConfigurationRule): class MyConfigurationRuleClass(rules.ConfigurationRule):
id = 'UC3' id = 'UC3'
name = u'my-cönfiguration-rule' name = 'my-cönfiguration-rule'
def apply(self): def apply(self):
pass pass

View file

@ -197,7 +197,7 @@ class RuleOptionTests(BaseTestCase):
self.assertEqual(option.value, self.get_sample_path()) self.assertEqual(option.value, self.get_sample_path())
# Expect exception if path type is invalid # Expect exception if path type is invalid
option.type = u'föo' option.type = 'föo'
expected = "Option tëst-directory type must be one of: 'file', 'dir', 'both' (current: 'föo')" expected = "Option tëst-directory type must be one of: 'file', 'dir', 'both' (current: 'föo')"
with self.assertRaisesMessage(RuleOptionError, expected): with self.assertRaisesMessage(RuleOptionError, expected):
option.set("haha") option.set("haha")

View file

@ -29,25 +29,20 @@ class BaseTestCase(TestCase):
GITLINT_USE_SH_LIB = os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]") GITLINT_USE_SH_LIB = os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]")
GIT_CONTEXT_ERROR_CODE = 254 GIT_CONTEXT_ERROR_CODE = 254
GITLINT_USAGE_ERROR = 253
@classmethod
def setUpClass(cls):
""" Sets up the integration tests by creating a new temporary git repository """
cls.tmp_git_repos = []
cls.tmp_git_repo = cls.create_tmp_git_repo()
@classmethod
def tearDownClass(cls):
""" Cleans up the temporary git repositories """
for repo in cls.tmp_git_repos:
shutil.rmtree(repo)
def setUp(self): def setUp(self):
""" Sets up the integration tests by creating a new temporary git repository """
self.tmpfiles = [] self.tmpfiles = []
self.tmp_git_repos = []
self.tmp_git_repo = self.create_tmp_git_repo()
def tearDown(self): def tearDown(self):
# Clean up temporary files and repos
for tmpfile in self.tmpfiles: for tmpfile in self.tmpfiles:
os.remove(tmpfile) os.remove(tmpfile)
for repo in self.tmp_git_repos:
shutil.rmtree(repo)
def assertEqualStdout(self, output, expected): # pylint: disable=invalid-name def assertEqualStdout(self, output, expected): # pylint: disable=invalid-name
self.assertIsInstance(output, RunningCommand) self.assertIsInstance(output, RunningCommand)
@ -55,16 +50,15 @@ class BaseTestCase(TestCase):
output = output.replace('\r', '') output = output.replace('\r', '')
self.assertMultiLineEqual(output, expected) self.assertMultiLineEqual(output, expected)
@classmethod @staticmethod
def generate_temp_path(cls): def generate_temp_path():
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f") timestamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f")
return os.path.realpath(f"/tmp/gitlint-test-{timestamp}") return os.path.realpath(f"/tmp/gitlint-test-{timestamp}")
@classmethod def create_tmp_git_repo(self):
def create_tmp_git_repo(cls):
""" Creates a temporary git repository and returns its directory path """ """ Creates a temporary git repository and returns its directory path """
tmp_git_repo = cls.generate_temp_path() tmp_git_repo = self.generate_temp_path()
cls.tmp_git_repos.append(tmp_git_repo) self.tmp_git_repos.append(tmp_git_repo)
git("init", tmp_git_repo) git("init", tmp_git_repo)
# configuring name and email is required in every git repot # configuring name and email is required in every git repot
@ -86,6 +80,7 @@ class BaseTestCase(TestCase):
def create_file(parent_dir): def create_file(parent_dir):
""" Creates a file inside a passed directory. Returns filename.""" """ Creates a file inside a passed directory. Returns filename."""
test_filename = "test-fïle-" + str(uuid4()) test_filename = "test-fïle-" + str(uuid4())
# pylint: disable=consider-using-with
io.open(os.path.join(parent_dir, test_filename), 'a', encoding=DEFAULT_ENCODING).close() io.open(os.path.join(parent_dir, test_filename), 'a', encoding=DEFAULT_ENCODING).close()
return test_filename return test_filename
@ -158,11 +153,12 @@ class BaseTestCase(TestCase):
specified by variable_dict. """ specified by variable_dict. """
expected_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "expected") expected_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "expected")
expected_path = os.path.join(expected_dir, filename) expected_path = os.path.join(expected_dir, filename)
expected = io.open(expected_path, encoding=DEFAULT_ENCODING).read() with io.open(expected_path, encoding=DEFAULT_ENCODING) as file:
expected = file.read()
if variable_dict: if variable_dict:
expected = expected.format(**variable_dict) expected = expected.format(**variable_dict)
return expected return expected
@staticmethod @staticmethod
def get_system_info_dict(): def get_system_info_dict():

View file

@ -18,6 +18,7 @@ ignore-squash-commits: True
ignore-revert-commits: True ignore-revert-commits: True
ignore-stdin: False ignore-stdin: False
staged: True staged: True
fail-without-commits: False
verbosity: 3 verbosity: 3
debug: True debug: True
target: {target} target: {target}
@ -30,6 +31,9 @@ target: {target}
regex=None regex=None
I3: ignore-body-lines I3: ignore-body-lines
regex=None regex=None
I4: ignore-by-author-name
ignore=all
regex=None
T1: title-max-length T1: title-max-length
line-length=72 line-length=72
T2: title-trailing-whitespace T2: title-trailing-whitespace

View file

@ -18,6 +18,7 @@ ignore-squash-commits: True
ignore-revert-commits: True ignore-revert-commits: True
ignore-stdin: False ignore-stdin: False
staged: True staged: True
fail-without-commits: False
verbosity: 3 verbosity: 3
debug: True debug: True
target: {target} target: {target}
@ -30,6 +31,9 @@ target: {target}
regex=None regex=None
I3: ignore-body-lines I3: ignore-body-lines
regex=None regex=None
I4: ignore-by-author-name
ignore=all
regex=None
T1: title-max-length T1: title-max-length
line-length=72 line-length=72
T2: title-trailing-whitespace T2: title-trailing-whitespace

View file

@ -18,6 +18,7 @@ ignore-squash-commits: True
ignore-revert-commits: True ignore-revert-commits: True
ignore-stdin: True ignore-stdin: True
staged: False staged: False
fail-without-commits: True
verbosity: 2 verbosity: 2
debug: True debug: True
target: {target} target: {target}
@ -30,6 +31,9 @@ target: {target}
regex=None regex=None
I3: ignore-body-lines I3: ignore-body-lines
regex=None regex=None
I4: ignore-by-author-name
ignore=all
regex=None
T1: title-max-length T1: title-max-length
line-length=72 line-length=72
T2: title-trailing-whitespace T2: title-trailing-whitespace

View file

@ -18,6 +18,7 @@ ignore-squash-commits: True
ignore-revert-commits: True ignore-revert-commits: True
ignore-stdin: False ignore-stdin: False
staged: True staged: True
fail-without-commits: False
verbosity: 0 verbosity: 0
debug: True debug: True
target: {target} target: {target}
@ -30,6 +31,9 @@ target: {target}
regex=None regex=None
I3: ignore-body-lines I3: ignore-body-lines
regex=None regex=None
I4: ignore-by-author-name
ignore=all
regex=None
T1: title-max-length T1: title-max-length
line-length=72 line-length=72
T2: title-trailing-whitespace T2: title-trailing-whitespace

View file

@ -18,6 +18,7 @@ ignore-squash-commits: True
ignore-revert-commits: True ignore-revert-commits: True
ignore-stdin: False ignore-stdin: False
staged: False staged: False
fail-without-commits: False
verbosity: 2 verbosity: 2
debug: True debug: True
target: {target} target: {target}
@ -30,6 +31,9 @@ target: {target}
regex=None regex=None
I3: ignore-body-lines I3: ignore-body-lines
regex=None regex=None
I4: ignore-by-author-name
ignore=all
regex=None
T1: title-max-length T1: title-max-length
line-length=20 line-length=20
T2: title-trailing-whitespace T2: title-trailing-whitespace

View file

@ -1,4 +1,3 @@
1: CC1 Body does not contain a 'Signed-off-by' line 1: CC1 Body does not contain a 'Signed-off-by' line
1: CT1 Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert, ci, build: "WIP Thi$ is å title"
1: CT1 Title does not follow ConventionalCommits.org format 'type(optional-scope): description': "WIP Thi$ is å title" 1: CT1 Title does not follow ConventionalCommits.org format 'type(optional-scope): description': "WIP Thi$ is å title"
1: T5 Title contains the word 'WIP' (case-insensitive): "WIP Thi$ is å title" 1: T5 Title contains the word 'WIP' (case-insensitive): "WIP Thi$ is å title"

View file

@ -1,4 +1,3 @@
1: CC1 Body does not contain a 'Signed-off-by' line 1: CC1 Body does not contain a 'Signed-off-by' line
1: CT1 Title does not start with one of föo, bår: "WIP Thi$ is å title"
1: CT1 Title does not follow ConventionalCommits.org format 'type(optional-scope): description': "WIP Thi$ is å title" 1: CT1 Title does not follow ConventionalCommits.org format 'type(optional-scope): description': "WIP Thi$ is å title"
1: T5 Title contains the word 'WIP' (case-insensitive): "WIP Thi$ is å title" 1: T5 Title contains the word 'WIP' (case-insensitive): "WIP Thi$ is å title"

View file

@ -1,4 +1,4 @@
sh==1.14.1 sh==1.14.2
pytest==6.2.3; pytest==6.2.5;
arrow==1.0.3; arrow==1.2.0;
gitlint # no version as you want to test the currently installed version gitlint # no version as you want to test the currently installed version

View file

@ -67,8 +67,8 @@ else:
popen_kwargs['env'] = kwargs['_env'] popen_kwargs['env'] = kwargs['_env']
try: try:
p = subprocess.Popen(args, **popen_kwargs) with subprocess.Popen(args, **popen_kwargs) as p:
result = p.communicate() result = p.communicate()
except FileNotFoundError as exc: except FileNotFoundError as exc:
raise CommandNotFound from exc raise CommandNotFound from exc

View file

@ -40,19 +40,60 @@ class CommitsTests(BaseTestCase):
expected_kwargs = {'commit_sha1': commit_sha1, 'commit_sha2': commit_sha2} expected_kwargs = {'commit_sha1': commit_sha1, 'commit_sha2': commit_sha2}
self.assertEqualStdout(output, self.get_expected("test_commits/test_violations_1", expected_kwargs)) self.assertEqualStdout(output, self.get_expected("test_commits/test_violations_1", expected_kwargs))
def test_lint_single_commit(self): def test_lint_empty_commit_range(self):
""" Tests `gitlint --commits <sha>` """ """ Tests `gitlint --commits <sha>^...<sha>` --fail-without-commits where the provided range is empty. """
self.create_simple_commit("Sïmple title.\n") self.create_simple_commit("Sïmple title.\n")
self.create_simple_commit("Sïmple title2.\n") self.create_simple_commit("Sïmple title2.\n")
commit_sha = self.get_last_commit_hash() commit_sha = self.get_last_commit_hash()
# git revspec -> 2 dots: <exclusive sha>..<inclusive sha> -> empty range when using same start and end sha
refspec = f"{commit_sha}..{commit_sha}"
# Regular gitlint invocation should run without issues
output = gitlint("--commits", refspec, _cwd=self.tmp_git_repo, _tty_in=True)
self.assertEqual(output.exit_code, 0)
self.assertEqualStdout(output, "")
# Gitlint should fail when --fail-without-commits is used
output = gitlint("--commits", refspec, "--fail-without-commits", _cwd=self.tmp_git_repo, _tty_in=True,
_ok_code=[self.GITLINT_USAGE_ERROR])
self.assertEqual(output.exit_code, self.GITLINT_USAGE_ERROR)
self.assertEqualStdout(output, f"Error: No commits in range \"{refspec}\"\n")
def test_lint_single_commit(self):
""" Tests `gitlint --commits <sha>^...<same sha>` """
self.create_simple_commit("Sïmple title.\n")
first_commit_sha = self.get_last_commit_hash()
self.create_simple_commit("Sïmple title2.\n")
commit_sha = self.get_last_commit_hash()
refspec = f"{commit_sha}^...{commit_sha}" refspec = f"{commit_sha}^...{commit_sha}"
self.create_simple_commit("Sïmple title3.\n") self.create_simple_commit("Sïmple title3.\n")
output = gitlint("--commits", refspec, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2])
expected = ("1: T3 Title has trailing punctuation (.): \"Sïmple title2.\"\n" + expected = ("1: T3 Title has trailing punctuation (.): \"Sïmple title2.\"\n" +
"3: B6 Body message is missing\n") "3: B6 Body message is missing\n")
# Lint using --commit <commit sha>
output = gitlint("--commit", commit_sha, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2])
self.assertEqual(output.exit_code, 2) self.assertEqual(output.exit_code, 2)
self.assertEqualStdout(output, expected) self.assertEqualStdout(output, expected)
# Lint a single commit using --commits <refspec> pointing to the single commit
output = gitlint("--commits", refspec, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2])
self.assertEqual(output.exit_code, 2)
self.assertEqualStdout(output, expected)
# Lint the first commit in the repository. This is a use-case that is not supported by --commits
# As <sha>^...<sha> is not correct refspec in case <sha> points to the initial commit (which has no parents)
expected = ("1: T3 Title has trailing punctuation (.): \"Sïmple title.\"\n" +
"3: B6 Body message is missing\n")
output = gitlint("--commit", first_commit_sha, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2])
self.assertEqual(output.exit_code, 2)
self.assertEqualStdout(output, expected)
# Assert that indeed --commits <refspec> is not supported when <refspec> points the the first commit
refspec = f"{first_commit_sha}^...{first_commit_sha}"
output = gitlint("--commits", refspec, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[254])
self.assertEqual(output.exit_code, 254)
def test_lint_staged_stdin(self): def test_lint_staged_stdin(self):
""" Tests linting a staged commit. Gitint should lint the passed commit message andfetch additional meta-data """ Tests linting a staged commit. Gitint should lint the passed commit message andfetch additional meta-data
from the underlying repository. The easiest way to test this is by inspecting `--debug` output. from the underlying repository. The easiest way to test this is by inspecting `--debug` output.
@ -139,7 +180,7 @@ class CommitsTests(BaseTestCase):
self.assertEqualStdout(output, self.get_expected("test_commits/test_lint_head_1", expected_kwargs)) self.assertEqualStdout(output, self.get_expected("test_commits/test_lint_head_1", expected_kwargs))
def test_ignore_commits(self): def test_ignore_commits(self):
""" Tests multiple commits of which some rules get igonored because of ignore-* rules """ """ Tests multiple commits of which some rules get ignored because of ignore-* rules """
# Create repo and some commits # Create repo and some commits
tmp_git_repo = self.create_tmp_git_repo() tmp_git_repo = self.create_tmp_git_repo()
self.create_simple_commit("Sïmple title.\n\nSimple bödy describing the commit", git_repo=tmp_git_repo) self.create_simple_commit("Sïmple title.\n\nSimple bödy describing the commit", git_repo=tmp_git_repo)

View file

@ -80,7 +80,8 @@ class ConfigTests(BaseTestCase):
filename = self.create_simple_commit(commit_msg, git_repo=target_repo) filename = self.create_simple_commit(commit_msg, git_repo=target_repo)
env = self.create_environment({"GITLINT_DEBUG": "1", "GITLINT_VERBOSITY": "2", env = self.create_environment({"GITLINT_DEBUG": "1", "GITLINT_VERBOSITY": "2",
"GITLINT_IGNORE": "T1,T2", "GITLINT_CONTRIB": "CC1,CT1", "GITLINT_IGNORE": "T1,T2", "GITLINT_CONTRIB": "CC1,CT1",
"GITLINT_IGNORE_STDIN": "1", "GITLINT_TARGET": target_repo, "GITLINT_FAIL_WITHOUT_COMMITS": "1", "GITLINT_IGNORE_STDIN": "1",
"GITLINT_TARGET": target_repo,
"GITLINT_COMMITS": self.get_last_commit_hash(git_repo=target_repo)}) "GITLINT_COMMITS": self.get_last_commit_hash(git_repo=target_repo)})
output = gitlint(_env=env, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5]) output = gitlint(_env=env, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5])
expected_kwargs = self.get_debug_vars_last_commit(git_repo=target_repo) expected_kwargs = self.get_debug_vars_last_commit(git_repo=target_repo)

View file

@ -10,14 +10,14 @@ class ContribRuleTests(BaseTestCase):
def test_contrib_rules(self): def test_contrib_rules(self):
self.create_simple_commit("WIP Thi$ is å title\n\nMy bödy that is a bit longer than 20 chars") self.create_simple_commit("WIP Thi$ is å title\n\nMy bödy that is a bit longer than 20 chars")
output = gitlint("--contrib", "contrib-title-conventional-commits,CC1", output = gitlint("--contrib", "contrib-title-conventional-commits,CC1",
_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[4]) _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3])
self.assertEqualStdout(output, self.get_expected("test_contrib/test_contrib_rules_1")) self.assertEqualStdout(output, self.get_expected("test_contrib/test_contrib_rules_1"))
def test_contrib_rules_with_config(self): def test_contrib_rules_with_config(self):
self.create_simple_commit("WIP Thi$ is å title\n\nMy bödy that is a bit longer than 20 chars") self.create_simple_commit("WIP Thi$ is å title\n\nMy bödy that is a bit longer than 20 chars")
output = gitlint("--contrib", "contrib-title-conventional-commits,CC1", output = gitlint("--contrib", "contrib-title-conventional-commits,CC1",
"-c", "contrib-title-conventional-commits.types=föo,bår", "-c", "contrib-title-conventional-commits.types=föo,bår",
_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[4]) _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3])
self.assertEqualStdout(output, self.get_expected("test_contrib/test_contrib_rules_with_config_1")) self.assertEqualStdout(output, self.get_expected("test_contrib/test_contrib_rules_with_config_1"))
def test_invalid_contrib_rules(self): def test_invalid_contrib_rules(self):

View file

@ -9,14 +9,15 @@ class HookTests(BaseTestCase):
""" Integration tests for gitlint commitmsg hooks""" """ Integration tests for gitlint commitmsg hooks"""
VIOLATIONS = ['gitlint: checking commit message...\n', VIOLATIONS = ['gitlint: checking commit message...\n',
u'1: T3 Title has trailing punctuation (.): "WIP: This ïs a title."\n', '1: T3 Title has trailing punctuation (.): "WIP: This ïs a title."\n',
u'1: T5 Title contains the word \'WIP\' (case-insensitive): "WIP: This ïs a title."\n', '1: T5 Title contains the word \'WIP\' (case-insensitive): "WIP: This ïs a title."\n',
u'2: B4 Second line is not empty: "Contënt on the second line"\n', '2: B4 Second line is not empty: "Contënt on the second line"\n',
'3: B6 Body message is missing\n', '3: B6 Body message is missing\n',
'-----------------------------------------------\n', '-----------------------------------------------\n',
'gitlint: \x1b[31mYour commit message contains violations.\x1b[0m\n'] 'gitlint: \x1b[31mYour commit message contains violations.\x1b[0m\n']
def setUp(self): def setUp(self):
super().setUp()
self.responses = [] self.responses = []
self.response_index = 0 self.response_index = 0
self.githook_output = [] self.githook_output = []
@ -28,16 +29,19 @@ class HookTests(BaseTestCase):
# install git commit-msg hook and assert output # install git commit-msg hook and assert output
output_installed = gitlint("install-hook", _cwd=self.tmp_git_repo) output_installed = gitlint("install-hook", _cwd=self.tmp_git_repo)
expected_installed = "Successfully installed gitlint commit-msg hook in %s/.git/hooks/commit-msg\n" % \ expected_installed = ("Successfully installed gitlint commit-msg hook in "
self.tmp_git_repo f"{self.tmp_git_repo}/.git/hooks/commit-msg\n")
self.assertEqualStdout(output_installed, expected_installed) self.assertEqualStdout(output_installed, expected_installed)
def tearDown(self): def tearDown(self):
# uninstall git commit-msg hook and assert output # uninstall git commit-msg hook and assert output
output_uninstalled = gitlint("uninstall-hook", _cwd=self.tmp_git_repo) output_uninstalled = gitlint("uninstall-hook", _cwd=self.tmp_git_repo)
expected_uninstalled = "Successfully uninstalled gitlint commit-msg hook from %s/.git/hooks/commit-msg\n" % \ expected_uninstalled = ("Successfully uninstalled gitlint commit-msg hook from "
self.tmp_git_repo f"{self.tmp_git_repo}/.git/hooks/commit-msg\n")
self.assertEqualStdout(output_uninstalled, expected_uninstalled) self.assertEqualStdout(output_uninstalled, expected_uninstalled)
super().tearDown()
def _violations(self): def _violations(self):
# Make a copy of the violations array so that we don't inadvertently edit it in the test (like I did :D) # Make a copy of the violations array so that we don't inadvertently edit it in the test (like I did :D)
@ -60,9 +64,9 @@ class HookTests(BaseTestCase):
short_hash = self.get_last_commit_short_hash() short_hash = self.get_last_commit_short_hash()
expected_output = ["gitlint: checking commit message...\n", expected_output = ["gitlint: checking commit message...\n",
"gitlint: \x1b[32mOK\x1b[0m (no violations in commit message)\n", "gitlint: \x1b[32mOK\x1b[0m (no violations in commit message)\n",
"[master %s] This ïs a title\n" % short_hash, f"[master {short_hash}] This ïs a title\n",
" 1 file changed, 0 insertions(+), 0 deletions(-)\n", " 1 file changed, 0 insertions(+), 0 deletions(-)\n",
" create mode 100644 %s\n" % test_filename] f" create mode 100644 {test_filename}\n"]
self.assertListEqual(expected_output, self.githook_output) self.assertListEqual(expected_output, self.githook_output)
def test_commit_hook_continue(self): def test_commit_hook_continue(self):
@ -76,10 +80,9 @@ class HookTests(BaseTestCase):
expected_output = self._violations() expected_output = self._violations()
expected_output += ["Continue with commit anyways (this keeps the current commit message)? " + expected_output += ["Continue with commit anyways (this keeps the current commit message)? " +
"[y(es)/n(no)/e(dit)] " + "[y(es)/n(no)/e(dit)] " +
"[master %s] WIP: This ïs a title. Contënt on the second line\n" f"[master {short_hash}] WIP: This ïs a title. Contënt on the second line\n",
% short_hash,
" 1 file changed, 0 insertions(+), 0 deletions(-)\n", " 1 file changed, 0 insertions(+), 0 deletions(-)\n",
" create mode 100644 %s\n" % test_filename] f" create mode 100644 {test_filename}\n"]
assert len(self.githook_output) == len(expected_output) assert len(self.githook_output) == len(expected_output)
for output, expected in zip(self.githook_output, expected_output): for output, expected in zip(self.githook_output, expected_output):
@ -124,9 +127,9 @@ class HookTests(BaseTestCase):
expected_output += self._violations()[1:] expected_output += self._violations()[1:]
expected_output += ['Continue with commit anyways (this keeps the current commit message)? ' + expected_output += ['Continue with commit anyways (this keeps the current commit message)? ' +
"[y(es)/n(no)/e(dit)] " + "[y(es)/n(no)/e(dit)] " +
"[master %s] WIP: This ïs a title. Contënt on the second line\n" % short_hash, f"[master {short_hash}] WIP: This ïs a title. Contënt on the second line\n",
" 1 file changed, 0 insertions(+), 0 deletions(-)\n", " 1 file changed, 0 insertions(+), 0 deletions(-)\n",
" create mode 100644 %s\n" % test_filename] f" create mode 100644 {test_filename}\n"]
assert len(self.githook_output) == len(expected_output) assert len(self.githook_output) == len(expected_output)
for output, expected in zip(self.githook_output, expected_output): for output, expected in zip(self.githook_output, expected_output):

View file

@ -50,7 +50,7 @@ class StdInTests(BaseTestCase):
# We need to use subprocess.Popen() here instead of sh because when passing a file_handle to sh, it will # We need to use subprocess.Popen() here instead of sh because when passing a file_handle to sh, it will
# deal with reading the file itself instead of passing it on to gitlint as a STDIN. Since we're trying to # deal with reading the file itself instead of passing it on to gitlint as a STDIN. Since we're trying to
# test for the condition where stat.S_ISREG == True that won't work for us here. # test for the condition where stat.S_ISREG == True that won't work for us here.
p = subprocess.Popen("gitlint", stdin=file_handle, cwd=self.tmp_git_repo, with subprocess.Popen("gitlint", stdin=file_handle, cwd=self.tmp_git_repo,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT) stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as p:
output, _ = p.communicate() output, _ = p.communicate()
self.assertEqual(output.decode(DEFAULT_ENCODING), self.get_expected("test_stdin/test_stdin_file_1")) self.assertEqual(output.decode(DEFAULT_ENCODING), self.get_expected("test_stdin/test_stdin_file_1"))

View file

@ -1,5 +1,5 @@
setuptools setuptools
wheel==0.36.2 wheel==0.37.0
Click==7.1.2 Click==8.0.1
sh==1.14.1; sys_platform != 'win32' # sh is not supported on windows sh==1.14.2; sys_platform != 'win32' # sh is not supported on windows
arrow==1.0.3 arrow==1.2.0

View file

@ -88,9 +88,8 @@ run_unit_tests(){
clean clean
# py.test -s => print standard output (i.e. show print statement output) # py.test -s => print standard output (i.e. show print statement output)
# -rw => print warnings # -rw => print warnings
OMIT="*pypy*,*venv*,*virtualenv*,*gitlint/tests/*"
target=${testargs:-"gitlint"} target=${testargs:-"gitlint"}
coverage run --omit=$OMIT -m pytest -rw -s $target coverage run -m pytest -rw -s $target
TEST_RESULT=$? TEST_RESULT=$?
if [ $include_coverage -eq 1 ]; then if [ $include_coverage -eq 1 ]; then
COVERAGE_REPORT=$(coverage report -m) COVERAGE_REPORT=$(coverage report -m)

View file

@ -49,6 +49,7 @@ setup(
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
"Environment :: Console", "Environment :: Console",
@ -59,12 +60,12 @@ setup(
], ],
python_requires=">=3.6", python_requires=">=3.6",
install_requires=[ install_requires=[
'Click==7.1.2', 'Click==8.0.1',
'arrow==1.0.3', 'arrow==1.2.0',
], ],
extras_require={ extras_require={
':sys_platform != "win32"': [ ':sys_platform != "win32"': [
'sh==1.14.1', 'sh==1.14.2',
], ],
}, },
keywords='gitlint git lint', keywords='gitlint git lint',

View file

@ -1,8 +1,8 @@
flake8==3.9.1 flake8==3.9.2
coverage==5.5 coverage==6.0
python-coveralls==2.9.3 python-coveralls==2.9.3
radon==4.5.0 radon==5.1.0
flake8-polyfill==1.0.2 # Required when installing both flake8 and radon>=4.3.1 flake8-polyfill==1.0.2 # Required when installing both flake8 and radon>=4.3.1
pytest==6.2.3; pytest==6.2.5;
pylint==2.7.4; pylint==2.11.1;
-e . -e .