Merging upstream version 0.16.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
40df5416c1
commit
72676ec535
56 changed files with 615 additions and 161 deletions
|
@ -1,2 +1,6 @@
|
|||
[report]
|
||||
fail_under = 97
|
||||
|
||||
[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
14
.github/dependabot.yml
vendored
Normal 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
|
8
.github/workflows/checks.yml
vendored
8
.github/workflows/checks.yml
vendored
|
@ -7,7 +7,7 @@ jobs:
|
|||
runs-on: "ubuntu-latest"
|
||||
strategy:
|
||||
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"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
@ -69,8 +69,11 @@ jobs:
|
|||
- name: Re-add git version control to code
|
||||
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
|
||||
run: ./run_tests.sh -g --debug
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
windows-checks:
|
||||
runs-on: windows-latest
|
||||
|
@ -133,5 +136,8 @@ jobs:
|
|||
- name: Re-add git version control to code
|
||||
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
|
||||
run: gitlint --debug
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
|
|
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,5 +1,19 @@
|
|||
# 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) ##
|
||||
|
||||
Contributors:
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
# NOTE: --ulimit is required to work around a limitation in Docker
|
||||
# Details: https://github.com/jorisroovers/gitlint/issues/129
|
||||
|
||||
FROM python:3.9-alpine
|
||||
FROM python:3.10-alpine
|
||||
ARG GITLINT_VERSION
|
||||
|
||||
RUN apk add git
|
||||
|
|
|
@ -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.**
|
||||
|
||||
<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 ##
|
||||
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
|
||||
how to get started - it's easy!
|
||||
|
|
|
@ -1 +1 @@
|
|||
mkdocs==1.1.2
|
||||
mkdocs==1.2.2
|
|
@ -52,6 +52,12 @@ ignore-stdin=true
|
|||
# commit message to gitlint via stdin or --commit-msg. Disabled by default.
|
||||
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.
|
||||
debug=true
|
||||
|
||||
|
@ -128,7 +134,7 @@ ignore=T1,body-min-length
|
|||
[ignore-by-body]
|
||||
# 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"
|
||||
# regex=(.*)release(.*)
|
||||
regex=(.*)release(.*)
|
||||
#
|
||||
# Ignore certain rules, you can reference them by their id or by their full name
|
||||
# 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'
|
||||
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.
|
||||
# You need to explicitly enable them one-by-one by adding them to the "contrib" option
|
||||
# under [general] section above.
|
||||
|
@ -363,6 +378,30 @@ GITLINT_STAGED=1 gitlint # using env variable
|
|||
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 any stdin data. Sometimes useful when running gitlint in a CI server.
|
||||
|
|
|
@ -44,7 +44,9 @@ vagrant ssh
|
|||
Or you can choose to use your local environment:
|
||||
|
||||
```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
|
||||
python setup.py develop
|
||||
```
|
||||
|
|
BIN
docs/images/readme-gitlint.png
Normal file
BIN
docs/images/readme-gitlint.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 340 KiB |
|
@ -38,12 +38,11 @@ useful throughout the years.
|
|||
# Pip is recommended to install the latest version
|
||||
pip install gitlint
|
||||
|
||||
# macOS
|
||||
brew install gitlint
|
||||
sudo port install gitlint # alternative using macports
|
||||
|
||||
# Ubuntu
|
||||
apt-get install gitlint
|
||||
# Community maintained packages:
|
||||
brew install gitlint # Homebrew (macOS)
|
||||
sudo port install gitlint # Macports (macOS)
|
||||
apt-get install gitlint # Ubuntu
|
||||
# Other package managers, see https://repology.org/project/gitlint/versions
|
||||
|
||||
# Docker: https://hub.docker.com/r/jorisroovers/gitlint
|
||||
docker run --ulimit nofile=1024 -v $(pwd):/repo jorisroovers/gitlint
|
||||
|
@ -134,8 +133,9 @@ Options:
|
|||
current working directory]
|
||||
-C, --config FILE Config file location [default: .gitlint]
|
||||
-c TEXT 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.
|
||||
(e.g.: -c T1.line-length=80). Flag can be
|
||||
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]
|
||||
-e, --extra-path PATH Path to a directory or python module with extra
|
||||
user-defined rules
|
||||
|
@ -147,10 +147,11 @@ Options:
|
|||
server.
|
||||
--staged Read staged commit meta-info from the local
|
||||
repository.
|
||||
-v, --verbose Verbosity, more v's for more verbose output (e.g.:
|
||||
-v, -vv, -vvv). [default: -vvv]
|
||||
-s, --silent Silent mode (no output). Takes precedence over -v,
|
||||
-vv, -vvv.
|
||||
--fail-without-commits Hard fail when the target commit range is empty.
|
||||
-v, --verbose Verbosity, more v's for more verbose output
|
||||
(e.g.: -v, -vv, -vvv). [default: -vvv]
|
||||
-s, --silent Silent mode (no output).
|
||||
Takes precedence over -v, -vv, -vvv.
|
||||
-d, --debug Enable debugging output.
|
||||
--version Show the version and exit.
|
||||
--help Show this message and exit.
|
||||
|
@ -159,6 +160,7 @@ Commands:
|
|||
generate-config Generates a sample gitlint config file.
|
||||
install-hook Install gitlint as a git commit-msg hook.
|
||||
lint Lints a git repository [default command]
|
||||
run-hook Runs the gitlint commit-msg hook.
|
||||
uninstall-hook Uninstall gitlint commit-msg hook.
|
||||
|
||||
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),
|
||||
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
|
||||
# Lint a specific commit range:
|
||||
gitlint --commits "019cf40...d6bc75a"
|
||||
# You can also use git's special references:
|
||||
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
|
||||
|
@ -271,9 +275,8 @@ script to lint an arbitrary set of commits, like shown in the example below.
|
|||
#!/bin/sh
|
||||
|
||||
for commit in $(git rev-list master); do
|
||||
commit_msg=$(git log -1 --pretty=%B $commit)
|
||||
echo "$commit"
|
||||
echo "$commit_msg" | gitlint
|
||||
echo "Commit $commit"
|
||||
gitlint --commit $commit
|
||||
echo "--------"
|
||||
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).
|
||||
|
||||
## Ignoring commits
|
||||
_Introduced in gitlint v0.10.0_
|
||||
|
||||
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
|
||||
use gitlint's *ignore* rules.
|
||||
Here's an example gitlint file that configures gitlint to ignore rules `title-max-length` and `body-min-length`
|
||||
for all commits with a title starting with *"Release"*.
|
||||
Here's a few examples snippets from a `.gitlint` file:
|
||||
|
||||
```ini
|
||||
[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'
|
||||
regex=(.*)release(.*)
|
||||
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
|
||||
|
|
|
@ -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
|
||||
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
|
||||
I4 | ignore-by-author-name | >= 0.16.0 | Ignore a commit based on matching its author name
|
||||
|
||||
|
||||
|
||||
## T1: title-max-length
|
||||
|
@ -405,3 +407,32 @@ regex=(^Co-Authored-By)|(^Signed-off-by)
|
|||
[ignore-body-lines]
|
||||
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
|
||||
```
|
|
@ -1 +1 @@
|
|||
__version__ = "0.15.1"
|
||||
__version__ = "0.16.0"
|
||||
|
|
|
@ -18,6 +18,7 @@ from gitlint.utils import LOG_FORMAT
|
|||
from gitlint.exception import GitlintError
|
||||
|
||||
# Error codes
|
||||
GITLINT_SUCCESS = 0
|
||||
MAX_VIOLATION_ERROR_CODE = 252
|
||||
USAGE_ERROR_CODE = 253
|
||||
GIT_CONTEXT_ERROR_CODE = 254
|
||||
|
@ -61,7 +62,8 @@ def log_system_info():
|
|||
|
||||
|
||||
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. """
|
||||
config_builder = LintConfigBuilder()
|
||||
|
@ -102,6 +104,9 @@ def build_config( # pylint: disable=too-many-arguments
|
|||
if staged:
|
||||
config_builder.set_option('general', 'staged', staged)
|
||||
|
||||
if fail_without_commits:
|
||||
config_builder.set_option('general', 'fail-without-commits', fail_without_commits)
|
||||
|
||||
config = config_builder.build()
|
||||
|
||||
return config, config_builder
|
||||
|
@ -139,7 +144,7 @@ def get_stdin_data():
|
|||
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 """
|
||||
|
||||
# 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
|
||||
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):
|
||||
|
@ -187,9 +196,10 @@ def handle_gitlint_error(ctx, exc):
|
|||
class ContextObj:
|
||||
""" Simple class to hold data that is passed between Click commands via the Click context. """
|
||||
|
||||
def __init__(self, config, config_builder, refspec, msg_filename, gitcontext=None):
|
||||
def __init__(self, config, config_builder, commit_hash, refspec, msg_filename, gitcontext=None):
|
||||
self.config = config
|
||||
self.config_builder = config_builder
|
||||
self.commit_hash = commit_hash
|
||||
self.refspec = refspec
|
||||
self.msg_filename = msg_filename
|
||||
self.gitcontext = gitcontext
|
||||
|
@ -205,6 +215,7 @@ class ContextObj:
|
|||
@click.option('-c', multiple=True,
|
||||
help="Config flags in format <rule>.<option>=<value> (e.g.: -c T1.line-length=80). " +
|
||||
"Flag can be used multiple times to set multiple config values.") # pylint: disable=bad-continuation
|
||||
@click.option('--commit', envvar='GITLINT_COMMIT', default=None, help="Hash (SHA) of specific commit to lint.")
|
||||
@click.option('--commits', envvar='GITLINT_COMMITS', default=None, help="The range of commits to lint. [default: HEAD]")
|
||||
@click.option('-e', '--extra-path', envvar='GITLINT_EXTRA_PATH',
|
||||
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.")
|
||||
@click.option('--staged', envvar='GITLINT_STAGED', is_flag=True,
|
||||
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,
|
||||
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,
|
||||
|
@ -225,8 +238,9 @@ class ContextObj:
|
|||
@click.version_option(version=gitlint.__version__)
|
||||
@click.pass_context
|
||||
def cli( # pylint: disable=too-many-arguments
|
||||
ctx, target, config, c, commits, extra_path, ignore, contrib,
|
||||
msg_filename, ignore_stdin, staged, verbose, silent, debug,
|
||||
ctx, target, config, c, commit, commits, extra_path, ignore, contrib,
|
||||
msg_filename, ignore_stdin, staged, fail_without_commits, verbose,
|
||||
silent, debug,
|
||||
):
|
||||
""" 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
|
||||
# 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,
|
||||
ignore_stdin, staged, verbose, silent, debug)
|
||||
config, config_builder = build_config(target, config, c, extra_path, ignore, contrib, ignore_stdin, staged,
|
||||
fail_without_commits, verbose, silent, debug)
|
||||
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 ctx.invoked_subcommand is None:
|
||||
|
@ -262,9 +276,10 @@ def lint(ctx):
|
|||
""" Lints a git repository [default command] """
|
||||
lint_config = ctx.obj.config
|
||||
refspec = ctx.obj.refspec
|
||||
commit_hash = ctx.obj.commit_hash
|
||||
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
|
||||
# in particular, this is used by run-hook
|
||||
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
|
||||
# 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.
|
||||
# This behavior can be overridden by using the --fail-without-commits flag.
|
||||
if number_of_commits == 0:
|
||||
LOG.debug(u'No commits in range "%s"', refspec)
|
||||
ctx.exit(0)
|
||||
LOG.debug('No commits in range "%s"', refspec)
|
||||
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
|
||||
last_commit = gitcontext.commits[-1]
|
||||
|
||||
# Let's get linting!
|
||||
first_violation = True
|
||||
exit_code = 0
|
||||
exit_code = GITLINT_SUCCESS
|
||||
for commit in gitcontext.commits:
|
||||
# Build a config_builder taking into account the commit specific config (if any)
|
||||
config_builder = general_config_builder.clone()
|
||||
|
@ -301,10 +319,8 @@ def lint(ctx):
|
|||
if violations:
|
||||
# Display the commit hash & new lines intelligently
|
||||
if number_of_commits > 1 and commit.sha:
|
||||
linter.display.e("{0}Commit {1}:".format(
|
||||
"\n" if not first_violation or commit is last_commit else "",
|
||||
commit.sha[:10]
|
||||
))
|
||||
commit_separator = "\n" if not first_violation or commit is last_commit else ""
|
||||
linter.display.e(f"{commit_separator}Commit {commit.sha[:10]}:")
|
||||
linter.print_violations(violations)
|
||||
first_violation = False
|
||||
|
||||
|
@ -323,7 +339,7 @@ def install_hook(ctx):
|
|||
hooks.GitHookInstaller.install_commit_msg_hook(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}")
|
||||
ctx.exit(0)
|
||||
ctx.exit(GITLINT_SUCCESS)
|
||||
except hooks.GitHookInstallerError as e:
|
||||
click.echo(e, err=True)
|
||||
ctx.exit(GIT_CONTEXT_ERROR_CODE)
|
||||
|
@ -337,7 +353,7 @@ def uninstall_hook(ctx):
|
|||
hooks.GitHookInstaller.uninstall_commit_msg_hook(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}")
|
||||
ctx.exit(0)
|
||||
ctx.exit(GITLINT_SUCCESS)
|
||||
except hooks.GitHookInstallerError as e:
|
||||
click.echo(e, err=True)
|
||||
ctx.exit(GIT_CONTEXT_ERROR_CODE)
|
||||
|
@ -361,7 +377,7 @@ def run_hook(ctx):
|
|||
sys.stdout.flush()
|
||||
|
||||
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)")
|
||||
continue
|
||||
|
||||
|
@ -387,7 +403,7 @@ def run_hook(ctx):
|
|||
|
||||
if value == "y":
|
||||
LOG.debug("run-hook: commit message accepted")
|
||||
exit_code = 0
|
||||
exit_code = GITLINT_SUCCESS
|
||||
elif value == "e":
|
||||
LOG.debug("run-hook: editing commit message")
|
||||
msg_filename = ctx.obj.msg_filename
|
||||
|
@ -428,7 +444,7 @@ def generate_config(ctx):
|
|||
|
||||
LintConfigGenerator.generate_config(path)
|
||||
click.echo(f"Successfully generated {path}")
|
||||
ctx.exit(0)
|
||||
ctx.exit(GITLINT_SUCCESS)
|
||||
|
||||
|
||||
# Let's Party!
|
||||
|
|
|
@ -41,6 +41,7 @@ class LintConfig:
|
|||
default_rule_classes = (rules.IgnoreByTitle,
|
||||
rules.IgnoreByBody,
|
||||
rules.IgnoreBodyLines,
|
||||
rules.IgnoreByAuthorName,
|
||||
rules.TitleMaxLength,
|
||||
rules.TitleTrailingWhitespace,
|
||||
rules.TitleLeadingWhitespace,
|
||||
|
@ -76,6 +77,8 @@ class LintConfig:
|
|||
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._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
|
||||
def target(self):
|
||||
|
@ -170,6 +173,15 @@ class LintConfig:
|
|||
def staged(self, 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
|
||||
def extra_path(self):
|
||||
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_stdin == other.ignore_stdin and \
|
||||
self.staged == other.staged and \
|
||||
self.fail_without_commits == other.fail_without_commits and \
|
||||
self.debug == other.debug and \
|
||||
self.ignore == other.ignore and \
|
||||
self._config_path == other._config_path # noqa
|
||||
|
@ -292,6 +305,7 @@ class LintConfig:
|
|||
f"ignore-revert-commits: {self.ignore_revert_commits}\n"
|
||||
f"ignore-stdin: {self.ignore_stdin}\n"
|
||||
f"staged: {self.staged}\n"
|
||||
f"fail-without-commits: {self.fail_without_commits}\n"
|
||||
f"verbosity: {self.verbosity}\n"
|
||||
f"debug: {self.debug}\n"
|
||||
f"target: {self.target}\n"
|
||||
|
|
|
@ -3,7 +3,7 @@ import re
|
|||
from gitlint.options import ListOption
|
||||
from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation
|
||||
|
||||
RULE_REGEX = re.compile(r"[^(]+?(\([^)]+?\))?: .+")
|
||||
RULE_REGEX = re.compile(r"([^(]+?)(\([^)]+?\))?!?: .+")
|
||||
|
||||
|
||||
class ConventionalCommit(LineRule):
|
||||
|
@ -23,16 +23,15 @@ class ConventionalCommit(LineRule):
|
|||
|
||||
def validate(self, line, _commit):
|
||||
violations = []
|
||||
match = RULE_REGEX.match(line)
|
||||
|
||||
for commit_type in self.options["types"].value:
|
||||
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):
|
||||
if not match:
|
||||
msg = "Title does not follow ConventionalCommits.org format 'type(optional-scope): description'"
|
||||
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
|
||||
|
|
|
@ -27,6 +27,12 @@
|
|||
# commit message to gitlint via stdin or --commit-msg. Disabled by default.
|
||||
# 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.
|
||||
# debug=true
|
||||
|
||||
|
@ -111,6 +117,15 @@
|
|||
# E.g. Ignore all lines that start with '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.
|
||||
# You need to explicitly enable them one-by-one by adding them to the "contrib" option
|
||||
# under [general] section above.
|
||||
|
|
|
@ -364,22 +364,27 @@ class GitContext(PropertyCache):
|
|||
return context
|
||||
|
||||
@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.
|
||||
: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)
|
||||
|
||||
# If no refspec is defined, fallback to the last commit on the current branch
|
||||
if refspec is None:
|
||||
if refspec:
|
||||
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
|
||||
# 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`.
|
||||
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:
|
||||
commit = LocalGitCommit(context, sha)
|
||||
|
|
|
@ -141,7 +141,7 @@ class LineMustNotContainWord(LineRule):
|
|||
strings = self.options['words'].value
|
||||
violations = []
|
||||
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())
|
||||
if match:
|
||||
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.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
|
||||
|
|
|
@ -11,8 +11,8 @@ from gitlint.utils import USE_SH_LIB, DEFAULT_ENCODING
|
|||
|
||||
def shell(cmd):
|
||||
""" Convenience function that opens a given command in a shell. Does not use 'sh' library. """
|
||||
p = subprocess.Popen(cmd, shell=True)
|
||||
p.communicate()
|
||||
with subprocess.Popen(cmd, shell=True) as p:
|
||||
p.communicate()
|
||||
|
||||
|
||||
if USE_SH_LIB:
|
||||
|
@ -57,8 +57,8 @@ else:
|
|||
popen_kwargs['cwd'] = kwargs['_cwd']
|
||||
|
||||
try:
|
||||
p = subprocess.Popen(args, **popen_kwargs)
|
||||
result = p.communicate()
|
||||
with subprocess.Popen(args, **popen_kwargs) as p:
|
||||
result = p.communicate()
|
||||
except FileNotFoundError as e:
|
||||
raise CommandNotFound from e
|
||||
|
||||
|
|
|
@ -126,6 +126,10 @@ class BaseTestCase(unittest.TestCase):
|
|||
"""
|
||||
return super().assertRaisesRegex(expected_exception, re.escape(expected_regex), *args, **kwargs)
|
||||
|
||||
def clearlog(self):
|
||||
""" Clears the log capture """
|
||||
self.logcapture.clear()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def assertRaisesMessage(self, expected_exception, expected_msg): # pylint: disable=invalid-name
|
||||
""" Asserts an exception has occurred with a given error message """
|
||||
|
@ -182,3 +186,6 @@ class LogCapture(logging.Handler):
|
|||
|
||||
def emit(self, record):
|
||||
self.messages.append(self.format(record))
|
||||
|
||||
def clear(self):
|
||||
self.messages = []
|
||||
|
|
|
@ -26,6 +26,7 @@ class CLITests(BaseTestCase):
|
|||
USAGE_ERROR_CODE = 253
|
||||
GIT_CONTEXT_ERROR_CODE = 254
|
||||
CONFIG_ERROR_CODE = 255
|
||||
GITLINT_SUCCESS_CODE = 0
|
||||
|
||||
def setUp(self):
|
||||
super(CLITests, self).setUp()
|
||||
|
@ -180,6 +181,39 @@ class CLITests(BaseTestCase):
|
|||
self.assertEqual(stderr.getvalue(), expected)
|
||||
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')
|
||||
def test_input_stream(self, _):
|
||||
""" 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 "
|
||||
"'--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)
|
||||
def test_msg_filename(self, _):
|
||||
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"])
|
||||
expected_output = self.get_expected('cli/test_cli/test_contrib_1')
|
||||
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")
|
||||
def test_contrib_negative(self, _):
|
||||
|
@ -475,7 +533,7 @@ class CLITests(BaseTestCase):
|
|||
def test_generate_config(self, generate_config):
|
||||
""" Test for the generate-config subcommand """
|
||||
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" + \
|
||||
f"Successfully generated {os.path.realpath('tëstfile')}\n"
|
||||
self.assertEqual(result.output, expected_msg)
|
||||
|
@ -517,7 +575,7 @@ class CLITests(BaseTestCase):
|
|||
result = self.cli.invoke(cli.cli, ["--commits", "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")
|
||||
def test_named_rules(self, _):
|
||||
|
|
|
@ -50,6 +50,7 @@ class LintConfigTests(BaseTestCase):
|
|||
|
||||
self.assertFalse(config.ignore_stdin)
|
||||
self.assertFalse(config.staged)
|
||||
self.assertFalse(config.fail_without_commits)
|
||||
self.assertFalse(config.debug)
|
||||
self.assertEqual(config.verbosity, 3)
|
||||
active_rule_classes = tuple(type(rule) for rule in config.rules)
|
||||
|
@ -95,6 +96,10 @@ class LintConfigTests(BaseTestCase):
|
|||
config.set_general_option("staged", "true")
|
||||
self.assertTrue(config.staged)
|
||||
|
||||
# fail-without-commits
|
||||
config.set_general_option("fail-without-commits", "true")
|
||||
self.assertTrue(config.fail_without_commits)
|
||||
|
||||
# target
|
||||
config.set_general_option("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
|
||||
|
||||
# invalid boolean options
|
||||
for attribute in ['debug', 'staged', 'ignore_stdin']:
|
||||
for attribute in ['debug', 'staged', 'ignore_stdin', 'fail_without_commits']:
|
||||
option_name = attribute.replace("_", "-")
|
||||
with self.assertRaisesMessage(LintConfigError,
|
||||
f"Option '{option_name}' must be either 'true' or 'false'"):
|
||||
|
|
|
@ -29,12 +29,34 @@ class ContribConventionalCommitTests(BaseTestCase):
|
|||
violations = rule.validate("bår: foo", 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: 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
|
||||
expected_violation = RuleViolation("CT1", "Title does not follow ConventionalCommits.org format "
|
||||
"'type(optional-scope): description'", "fix föo")
|
||||
violations = rule.validate("fix föo", None)
|
||||
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
|
||||
rule = ConventionalCommit({'types': ["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)
|
||||
expected_violation = RuleViolation("CT1", "Title does not start with one of föo, bär", "fix: hür dur")
|
||||
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)
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
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"
|
||||
|
|
|
@ -17,6 +17,7 @@ ignore-squash-commits: True
|
|||
ignore-revert-commits: True
|
||||
ignore-stdin: False
|
||||
staged: False
|
||||
fail-without-commits: False
|
||||
verbosity: 1
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -29,6 +30,9 @@ target: {target}
|
|||
regex=None
|
||||
I3: ignore-body-lines
|
||||
regex=None
|
||||
I4: ignore-by-author-name
|
||||
ignore=all
|
||||
regex=None
|
||||
T1: title-max-length
|
||||
line-length=20
|
||||
T2: title-trailing-whitespace
|
||||
|
|
|
@ -17,6 +17,7 @@ ignore-squash-commits: True
|
|||
ignore-revert-commits: True
|
||||
ignore-stdin: False
|
||||
staged: False
|
||||
fail-without-commits: False
|
||||
verbosity: 3
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -29,6 +30,9 @@ target: {target}
|
|||
regex=None
|
||||
I3: ignore-body-lines
|
||||
regex=None
|
||||
I4: ignore-by-author-name
|
||||
ignore=all
|
||||
regex=None
|
||||
T1: title-max-length
|
||||
line-length=72
|
||||
T2: title-trailing-whitespace
|
||||
|
|
2
gitlint/tests/expected/cli/test_cli/test_lint_commit_1
Normal file
2
gitlint/tests/expected/cli/test_cli/test_lint_commit_1
Normal 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"
|
|
@ -17,6 +17,7 @@ ignore-squash-commits: True
|
|||
ignore-revert-commits: True
|
||||
ignore-stdin: False
|
||||
staged: True
|
||||
fail-without-commits: False
|
||||
verbosity: 3
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -29,6 +30,9 @@ target: {target}
|
|||
regex=None
|
||||
I3: ignore-body-lines
|
||||
regex=None
|
||||
I4: ignore-by-author-name
|
||||
ignore=all
|
||||
regex=None
|
||||
T1: title-max-length
|
||||
line-length=72
|
||||
T2: title-trailing-whitespace
|
||||
|
|
|
@ -17,6 +17,7 @@ ignore-squash-commits: True
|
|||
ignore-revert-commits: True
|
||||
ignore-stdin: False
|
||||
staged: True
|
||||
fail-without-commits: False
|
||||
verbosity: 3
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -29,6 +30,9 @@ target: {target}
|
|||
regex=None
|
||||
I3: ignore-body-lines
|
||||
regex=None
|
||||
I4: ignore-by-author-name
|
||||
ignore=all
|
||||
regex=None
|
||||
T1: title-max-length
|
||||
line-length=72
|
||||
T2: title-trailing-whitespace
|
||||
|
|
|
@ -17,6 +17,7 @@ ignore-squash-commits: True
|
|||
ignore-revert-commits: True
|
||||
ignore-stdin: False
|
||||
staged: False
|
||||
fail-without-commits: False
|
||||
verbosity: 3
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -29,6 +30,9 @@ target: {target}
|
|||
regex=None
|
||||
I3: ignore-body-lines
|
||||
regex=None
|
||||
I4: ignore-by-author-name
|
||||
ignore=all
|
||||
regex=None
|
||||
T1: title-max-length
|
||||
line-length=72
|
||||
T2: title-trailing-whitespace
|
||||
|
|
|
@ -75,11 +75,12 @@ class GitCommitTests(BaseTestCase):
|
|||
self.assertListEqual(sh.git.mock_calls, expected_calls)
|
||||
|
||||
@patch('gitlint.git.sh')
|
||||
def test_from_local_repository_specific_ref(self, sh):
|
||||
sample_sha = "myspecialref"
|
||||
def test_from_local_repository_specific_refspec(self, sh):
|
||||
sample_refspec = "åbc123..def456"
|
||||
sample_sha = "åbc123"
|
||||
|
||||
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"
|
||||
"cömmit-title\n\ncömmit-body",
|
||||
"#", # git config --get core.commentchar
|
||||
|
@ -87,10 +88,10 @@ class GitCommitTests(BaseTestCase):
|
|||
"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
|
||||
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('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,
|
||||
|
@ -127,6 +128,59 @@ class GitCommitTests(BaseTestCase):
|
|||
# All expected calls should've happened at this point
|
||||
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')
|
||||
def test_get_latest_commit_merge_commit(self, sh):
|
||||
sample_sha = "d8ac47e9f2923c7f22d8668e3a1ed04eb4cdbca9"
|
||||
|
|
|
@ -101,13 +101,13 @@ class BodyRuleTests(BaseTestCase):
|
|||
expected_violation = rules.RuleViolation("B5", "Body message is too short (21<120)", "å" * 21, 3)
|
||||
|
||||
rule = rules.BodyMinLength({'min-length': 120})
|
||||
commit = self.gitcommit("Title\n\n%s\n" % ("å" * 21))
|
||||
commit = self.gitcommit("Title\n\n{0}\n".format("å" * 21)) # pylint: disable=consider-using-f-string
|
||||
violations = rule.validate(commit)
|
||||
self.assertListEqual(violations, [expected_violation])
|
||||
|
||||
# Make sure we don't get the error if the body-length is exactly the min-length
|
||||
rule = rules.BodyMinLength({'min-length': 8})
|
||||
commit = self.gitcommit("Tïtle\n\n%s\n" % ("å" * 8))
|
||||
commit = self.gitcommit("Tïtle\n\n{0}\n".format("å" * 8)) # pylint: disable=consider-using-f-string
|
||||
violations = rule.validate(commit)
|
||||
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)
|
||||
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 = self.gitcommit(commit_msg, ["föo/test.py", "bar.txt"])
|
||||
violations = rule.validate(commit)
|
||||
|
|
|
@ -71,6 +71,39 @@ class ConfigurationRuleTests(BaseTestCase):
|
|||
"Commit message line ' a relëase body' matches the regex '(.*)relëase(.*)', ignoring rules: T1,B2"
|
||||
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):
|
||||
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")
|
||||
|
|
|
@ -79,7 +79,7 @@ class TitleRuleTests(BaseTestCase):
|
|||
violations = rule.validate("This is å test", None)
|
||||
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)
|
||||
self.assertIsNone(violations)
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ class UserRuleTests(BaseTestCase):
|
|||
def test_assert_valid_rule_class(self):
|
||||
class MyLineRuleClass(rules.LineRule):
|
||||
id = 'UC1'
|
||||
name = u'my-lïne-rule'
|
||||
name = 'my-lïne-rule'
|
||||
target = rules.CommitMessageTitle
|
||||
|
||||
def validate(self):
|
||||
|
@ -105,14 +105,14 @@ class UserRuleTests(BaseTestCase):
|
|||
|
||||
class MyCommitRuleClass(rules.CommitRule):
|
||||
id = 'UC2'
|
||||
name = u'my-cömmit-rule'
|
||||
name = 'my-cömmit-rule'
|
||||
|
||||
def validate(self):
|
||||
pass
|
||||
|
||||
class MyConfigurationRuleClass(rules.ConfigurationRule):
|
||||
id = 'UC3'
|
||||
name = u'my-cönfiguration-rule'
|
||||
name = 'my-cönfiguration-rule'
|
||||
|
||||
def apply(self):
|
||||
pass
|
||||
|
|
|
@ -197,7 +197,7 @@ class RuleOptionTests(BaseTestCase):
|
|||
self.assertEqual(option.value, self.get_sample_path())
|
||||
|
||||
# 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')"
|
||||
with self.assertRaisesMessage(RuleOptionError, expected):
|
||||
option.set("haha")
|
||||
|
|
40
qa/base.py
40
qa/base.py
|
@ -29,25 +29,20 @@ class BaseTestCase(TestCase):
|
|||
|
||||
GITLINT_USE_SH_LIB = os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]")
|
||||
GIT_CONTEXT_ERROR_CODE = 254
|
||||
|
||||
@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)
|
||||
GITLINT_USAGE_ERROR = 253
|
||||
|
||||
def setUp(self):
|
||||
""" Sets up the integration tests by creating a new temporary git repository """
|
||||
self.tmpfiles = []
|
||||
self.tmp_git_repos = []
|
||||
self.tmp_git_repo = self.create_tmp_git_repo()
|
||||
|
||||
def tearDown(self):
|
||||
# Clean up temporary files and repos
|
||||
for tmpfile in self.tmpfiles:
|
||||
os.remove(tmpfile)
|
||||
for repo in self.tmp_git_repos:
|
||||
shutil.rmtree(repo)
|
||||
|
||||
def assertEqualStdout(self, output, expected): # pylint: disable=invalid-name
|
||||
self.assertIsInstance(output, RunningCommand)
|
||||
|
@ -55,16 +50,15 @@ class BaseTestCase(TestCase):
|
|||
output = output.replace('\r', '')
|
||||
self.assertMultiLineEqual(output, expected)
|
||||
|
||||
@classmethod
|
||||
def generate_temp_path(cls):
|
||||
@staticmethod
|
||||
def generate_temp_path():
|
||||
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f")
|
||||
return os.path.realpath(f"/tmp/gitlint-test-{timestamp}")
|
||||
|
||||
@classmethod
|
||||
def create_tmp_git_repo(cls):
|
||||
def create_tmp_git_repo(self):
|
||||
""" Creates a temporary git repository and returns its directory path """
|
||||
tmp_git_repo = cls.generate_temp_path()
|
||||
cls.tmp_git_repos.append(tmp_git_repo)
|
||||
tmp_git_repo = self.generate_temp_path()
|
||||
self.tmp_git_repos.append(tmp_git_repo)
|
||||
|
||||
git("init", tmp_git_repo)
|
||||
# configuring name and email is required in every git repot
|
||||
|
@ -86,6 +80,7 @@ class BaseTestCase(TestCase):
|
|||
def create_file(parent_dir):
|
||||
""" Creates a file inside a passed directory. Returns filename."""
|
||||
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()
|
||||
return test_filename
|
||||
|
||||
|
@ -158,11 +153,12 @@ class BaseTestCase(TestCase):
|
|||
specified by variable_dict. """
|
||||
expected_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "expected")
|
||||
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:
|
||||
expected = expected.format(**variable_dict)
|
||||
return expected
|
||||
if variable_dict:
|
||||
expected = expected.format(**variable_dict)
|
||||
return expected
|
||||
|
||||
@staticmethod
|
||||
def get_system_info_dict():
|
||||
|
|
|
@ -18,6 +18,7 @@ ignore-squash-commits: True
|
|||
ignore-revert-commits: True
|
||||
ignore-stdin: False
|
||||
staged: True
|
||||
fail-without-commits: False
|
||||
verbosity: 3
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -30,6 +31,9 @@ target: {target}
|
|||
regex=None
|
||||
I3: ignore-body-lines
|
||||
regex=None
|
||||
I4: ignore-by-author-name
|
||||
ignore=all
|
||||
regex=None
|
||||
T1: title-max-length
|
||||
line-length=72
|
||||
T2: title-trailing-whitespace
|
||||
|
|
|
@ -18,6 +18,7 @@ ignore-squash-commits: True
|
|||
ignore-revert-commits: True
|
||||
ignore-stdin: False
|
||||
staged: True
|
||||
fail-without-commits: False
|
||||
verbosity: 3
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -30,6 +31,9 @@ target: {target}
|
|||
regex=None
|
||||
I3: ignore-body-lines
|
||||
regex=None
|
||||
I4: ignore-by-author-name
|
||||
ignore=all
|
||||
regex=None
|
||||
T1: title-max-length
|
||||
line-length=72
|
||||
T2: title-trailing-whitespace
|
||||
|
|
|
@ -18,6 +18,7 @@ ignore-squash-commits: True
|
|||
ignore-revert-commits: True
|
||||
ignore-stdin: True
|
||||
staged: False
|
||||
fail-without-commits: True
|
||||
verbosity: 2
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -30,6 +31,9 @@ target: {target}
|
|||
regex=None
|
||||
I3: ignore-body-lines
|
||||
regex=None
|
||||
I4: ignore-by-author-name
|
||||
ignore=all
|
||||
regex=None
|
||||
T1: title-max-length
|
||||
line-length=72
|
||||
T2: title-trailing-whitespace
|
||||
|
|
|
@ -18,6 +18,7 @@ ignore-squash-commits: True
|
|||
ignore-revert-commits: True
|
||||
ignore-stdin: False
|
||||
staged: True
|
||||
fail-without-commits: False
|
||||
verbosity: 0
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -30,6 +31,9 @@ target: {target}
|
|||
regex=None
|
||||
I3: ignore-body-lines
|
||||
regex=None
|
||||
I4: ignore-by-author-name
|
||||
ignore=all
|
||||
regex=None
|
||||
T1: title-max-length
|
||||
line-length=72
|
||||
T2: title-trailing-whitespace
|
||||
|
|
|
@ -18,6 +18,7 @@ ignore-squash-commits: True
|
|||
ignore-revert-commits: True
|
||||
ignore-stdin: False
|
||||
staged: False
|
||||
fail-without-commits: False
|
||||
verbosity: 2
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -30,6 +31,9 @@ target: {target}
|
|||
regex=None
|
||||
I3: ignore-body-lines
|
||||
regex=None
|
||||
I4: ignore-by-author-name
|
||||
ignore=all
|
||||
regex=None
|
||||
T1: title-max-length
|
||||
line-length=20
|
||||
T2: title-trailing-whitespace
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
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: T5 Title contains the word 'WIP' (case-insensitive): "WIP Thi$ is å title"
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
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: T5 Title contains the word 'WIP' (case-insensitive): "WIP Thi$ is å title"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
sh==1.14.1
|
||||
pytest==6.2.3;
|
||||
arrow==1.0.3;
|
||||
sh==1.14.2
|
||||
pytest==6.2.5;
|
||||
arrow==1.2.0;
|
||||
gitlint # no version as you want to test the currently installed version
|
||||
|
|
|
@ -67,8 +67,8 @@ else:
|
|||
popen_kwargs['env'] = kwargs['_env']
|
||||
|
||||
try:
|
||||
p = subprocess.Popen(args, **popen_kwargs)
|
||||
result = p.communicate()
|
||||
with subprocess.Popen(args, **popen_kwargs) as p:
|
||||
result = p.communicate()
|
||||
except FileNotFoundError as exc:
|
||||
raise CommandNotFound from exc
|
||||
|
||||
|
|
|
@ -40,19 +40,60 @@ class CommitsTests(BaseTestCase):
|
|||
expected_kwargs = {'commit_sha1': commit_sha1, 'commit_sha2': commit_sha2}
|
||||
self.assertEqualStdout(output, self.get_expected("test_commits/test_violations_1", expected_kwargs))
|
||||
|
||||
def test_lint_single_commit(self):
|
||||
""" Tests `gitlint --commits <sha>` """
|
||||
def test_lint_empty_commit_range(self):
|
||||
""" 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 title2.\n")
|
||||
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}"
|
||||
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" +
|
||||
"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.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):
|
||||
""" 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.
|
||||
|
@ -139,7 +180,7 @@ class CommitsTests(BaseTestCase):
|
|||
self.assertEqualStdout(output, self.get_expected("test_commits/test_lint_head_1", expected_kwargs))
|
||||
|
||||
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
|
||||
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)
|
||||
|
|
|
@ -80,7 +80,8 @@ class ConfigTests(BaseTestCase):
|
|||
filename = self.create_simple_commit(commit_msg, git_repo=target_repo)
|
||||
env = self.create_environment({"GITLINT_DEBUG": "1", "GITLINT_VERBOSITY": "2",
|
||||
"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)})
|
||||
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)
|
||||
|
|
|
@ -10,14 +10,14 @@ class ContribRuleTests(BaseTestCase):
|
|||
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")
|
||||
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"))
|
||||
|
||||
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")
|
||||
output = gitlint("--contrib", "contrib-title-conventional-commits,CC1",
|
||||
"-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"))
|
||||
|
||||
def test_invalid_contrib_rules(self):
|
||||
|
|
|
@ -9,14 +9,15 @@ class HookTests(BaseTestCase):
|
|||
""" Integration tests for gitlint commitmsg hooks"""
|
||||
|
||||
VIOLATIONS = ['gitlint: checking commit message...\n',
|
||||
u'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',
|
||||
u'2: B4 Second line is not empty: "Contënt on the second line"\n',
|
||||
'1: T3 Title has trailing punctuation (.): "WIP: This ïs a title."\n',
|
||||
'1: T5 Title contains the word \'WIP\' (case-insensitive): "WIP: This ïs a title."\n',
|
||||
'2: B4 Second line is not empty: "Contënt on the second line"\n',
|
||||
'3: B6 Body message is missing\n',
|
||||
'-----------------------------------------------\n',
|
||||
'gitlint: \x1b[31mYour commit message contains violations.\x1b[0m\n']
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.responses = []
|
||||
self.response_index = 0
|
||||
self.githook_output = []
|
||||
|
@ -28,16 +29,19 @@ class HookTests(BaseTestCase):
|
|||
|
||||
# install git commit-msg hook and assert output
|
||||
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" % \
|
||||
self.tmp_git_repo
|
||||
expected_installed = ("Successfully installed gitlint commit-msg hook in "
|
||||
f"{self.tmp_git_repo}/.git/hooks/commit-msg\n")
|
||||
|
||||
self.assertEqualStdout(output_installed, expected_installed)
|
||||
|
||||
def tearDown(self):
|
||||
# uninstall git commit-msg hook and assert output
|
||||
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" % \
|
||||
self.tmp_git_repo
|
||||
expected_uninstalled = ("Successfully uninstalled gitlint commit-msg hook from "
|
||||
f"{self.tmp_git_repo}/.git/hooks/commit-msg\n")
|
||||
|
||||
self.assertEqualStdout(output_uninstalled, expected_uninstalled)
|
||||
super().tearDown()
|
||||
|
||||
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)
|
||||
|
@ -60,9 +64,9 @@ class HookTests(BaseTestCase):
|
|||
short_hash = self.get_last_commit_short_hash()
|
||||
expected_output = ["gitlint: checking 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",
|
||||
" create mode 100644 %s\n" % test_filename]
|
||||
f" create mode 100644 {test_filename}\n"]
|
||||
self.assertListEqual(expected_output, self.githook_output)
|
||||
|
||||
def test_commit_hook_continue(self):
|
||||
|
@ -76,10 +80,9 @@ class HookTests(BaseTestCase):
|
|||
expected_output = self._violations()
|
||||
expected_output += ["Continue with commit anyways (this keeps the current commit message)? " +
|
||||
"[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",
|
||||
" create mode 100644 %s\n" % test_filename]
|
||||
f" create mode 100644 {test_filename}\n"]
|
||||
|
||||
assert len(self.githook_output) == len(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 += ['Continue with commit anyways (this keeps the current commit message)? ' +
|
||||
"[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",
|
||||
" create mode 100644 %s\n" % test_filename]
|
||||
f" create mode 100644 {test_filename}\n"]
|
||||
|
||||
assert len(self.githook_output) == len(expected_output)
|
||||
for output, expected in zip(self.githook_output, expected_output):
|
||||
|
|
|
@ -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
|
||||
# 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.
|
||||
p = subprocess.Popen("gitlint", stdin=file_handle, cwd=self.tmp_git_repo,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
output, _ = p.communicate()
|
||||
self.assertEqual(output.decode(DEFAULT_ENCODING), self.get_expected("test_stdin/test_stdin_file_1"))
|
||||
with subprocess.Popen("gitlint", stdin=file_handle, cwd=self.tmp_git_repo,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as p:
|
||||
output, _ = p.communicate()
|
||||
self.assertEqual(output.decode(DEFAULT_ENCODING), self.get_expected("test_stdin/test_stdin_file_1"))
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
setuptools
|
||||
wheel==0.36.2
|
||||
Click==7.1.2
|
||||
sh==1.14.1; sys_platform != 'win32' # sh is not supported on windows
|
||||
arrow==1.0.3
|
||||
wheel==0.37.0
|
||||
Click==8.0.1
|
||||
sh==1.14.2; sys_platform != 'win32' # sh is not supported on windows
|
||||
arrow==1.2.0
|
||||
|
|
|
@ -88,9 +88,8 @@ run_unit_tests(){
|
|||
clean
|
||||
# py.test -s => print standard output (i.e. show print statement output)
|
||||
# -rw => print warnings
|
||||
OMIT="*pypy*,*venv*,*virtualenv*,*gitlint/tests/*"
|
||||
target=${testargs:-"gitlint"}
|
||||
coverage run --omit=$OMIT -m pytest -rw -s $target
|
||||
coverage run -m pytest -rw -s $target
|
||||
TEST_RESULT=$?
|
||||
if [ $include_coverage -eq 1 ]; then
|
||||
COVERAGE_REPORT=$(coverage report -m)
|
||||
|
|
7
setup.py
7
setup.py
|
@ -49,6 +49,7 @@ setup(
|
|||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Environment :: Console",
|
||||
|
@ -59,12 +60,12 @@ setup(
|
|||
],
|
||||
python_requires=">=3.6",
|
||||
install_requires=[
|
||||
'Click==7.1.2',
|
||||
'arrow==1.0.3',
|
||||
'Click==8.0.1',
|
||||
'arrow==1.2.0',
|
||||
],
|
||||
extras_require={
|
||||
':sys_platform != "win32"': [
|
||||
'sh==1.14.1',
|
||||
'sh==1.14.2',
|
||||
],
|
||||
},
|
||||
keywords='gitlint git lint',
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
flake8==3.9.1
|
||||
coverage==5.5
|
||||
flake8==3.9.2
|
||||
coverage==6.0
|
||||
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
|
||||
pytest==6.2.3;
|
||||
pylint==2.7.4;
|
||||
pytest==6.2.5;
|
||||
pylint==2.11.1;
|
||||
-e .
|
||||
|
|
Loading…
Add table
Reference in a new issue