1
0
Fork 0

Merging upstream version 0.15.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-13 06:03:13 +01:00
parent 470a4841cc
commit 3213982697
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
75 changed files with 1281 additions and 1555 deletions

View file

@ -7,7 +7,7 @@ jobs:
runs-on: "ubuntu-latest"
strategy:
matrix:
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, pypy2, pypy3]
python-version: [3.6, 3.7, 3.8, 3.9, pypy3]
os: ["macos-latest", "ubuntu-latest"]
steps:
- uses: actions/checkout@v2
@ -76,7 +76,7 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
python-version: [2.7, 3.6]
python-version: [3.6]
steps:
- uses: actions/checkout@v2
with:

View file

@ -1,5 +1,6 @@
- id: gitlint
name: gitlint
language: python
entry: gitlint --staged --msg-filename
entry: gitlint
args: [--staged, --msg-filename]
stages: [commit-msg]

View file

@ -1,9 +1,22 @@
# Changelog #
## v0.15.0 (2020-11-27) ##
Contributors:
Special thanks to [BrunIF](https://github.com/BrunIF), [lukech](https://github.com/lukech), [Cielquan](https://github.com/Cielquan), [harens](https://github.com/harens) and [sigmavirus24](https://github.com/sigmavirus24).
**This release drops support for Python 2.7 and Python 3.5 ([both are EOL](https://endoflife.date/python)). Other than a few minor fixes, there are no functional differences from the 0.14.0 release.**
Other call-outs:
- **Mac users**: Gitlint can now be installed using both homebrew (upgraded to latest) and macports. Special thanks to [@harens](https://github.com/harens) for maintaining these packages (best-effort).
- Bugfix: Gitlint now properly handles exceptions when using its built-in commit-msg hook ([#166](https://github.com/jorisroovers/gitlint/issues/166)).
- All dependencies have been upgraded to the latest available versions (`Click==7.1.2`, `arrow==0.17.0`, `sh==1.14.1`).
- Much under-the-hood refactoring as a result of dropping Python 2.7
## v0.14.0 (2020-10-24) ##
Contributors:
Special thanks to all contributors for this release, in particular [@mrshu](https://github.com/mrshu), [@glasserc](https://github.com/glasserc), [@strk](https://github.com/strk), [@chgl](https://github.com/chgl), [@melg8](https://github.com/melg8) and [@sigmavirus24](https://github.com/sigmavirus24).
Special thanks to all contributors for this release, in particular [mrshu](https://github.com/mrshu), [glasserc](https://github.com/glasserc), [strk](https://github.com/strk), [chgl](https://github.com/chgl), [melg8](https://github.com/melg8) and [sigmavirus24](https://github.com/sigmavirus24).
- **IMPORTANT: Gitlint 0.14.x will be the last gitlint release to support Python 2.7 and Python 3.5, as [both are EOL](https://endoflife.date/python) which makes it difficult to keep supporting them.**
@ -13,7 +26,7 @@ Special thanks to all contributors for this release, in particular [@mrshu](http
- **New Rule**: [ignore-body-lines](http://jorisroovers.github.io/gitlint/rules/#i3-ignore-body-lines) allows users to
[ignore parts of a commit](http://jorisroovers.github.io/gitlint/gitlint/#ignoring-commits) by matching a regex against
the lines in a commit message body ([#126](https://github.com/jorisroovers/gitlint/issues/126))
- [Named Rules](http://jorisroovers.github.io/gitlint/#named-rules) allow users to have multiple instances of the same rule active at the same time. This is useful when you want to enforce the same rule multiple times but with different options ([#113](https://github.com/jorisroovers/gitlint/issues/130), [#66](https://github.com/jorisroovers/gitlint/issues/130))
- [Named Rules](http://jorisroovers.github.io/gitlint/#named-rules) allow users to have multiple instances of the same rule active at the same time. This is useful when you want to enforce the same rule multiple times but with different options ([#113](https://github.com/jorisroovers/gitlint/issues/113), [#66](https://github.com/jorisroovers/gitlint/issues/66))
- [User-defined Configuration Rules](http://jorisroovers.github.io/gitlint/user_defined_rules/#configuration-rules) allow users to dynamically change gitlint's configuration and/or the commit *before* any other rules are applied.
- The `commit-msg` hook has been re-written in Python (it contained a lot of Bash before), fixing a number of platform specific issues. Existing users will need to reinstall their hooks (`gitlint uninstall-hook; gitlint install-hook`) to make use of this.
- Most general options can now be set through environment variables (e.g. set the `general.ignore` option via `GITLINT_IGNORE=T1,T2`). The list of available environment variables can be found in the [configuration documentation](http://jorisroovers.github.io/gitlint/configuration).

13
Vagrantfile vendored
View file

@ -7,18 +7,19 @@ INSTALL_DEPS=<<EOF
cd /vagrant
sudo add-apt-repository -y ppa:deadsnakes/ppa
sudo apt-get update
sudo apt-get install -y --allow-unauthenticated python2.7-dev python3.5-dev python3.6-dev python3.7-dev python3.8-dev python3.9-dev
sudo apt-get install -y --allow-unauthenticated python3.6-dev python3.7-dev python3.8-dev python3.9-dev
sudo apt-get install -y --allow-unauthenticated python3.8-distutils python3.9-distutils # Needed to work around python3.8/9+virtualenv issue
sudo apt-get install -y python-virtualenv git ipython python-pip python3-pip silversearcher-ag jq
sudo apt-get install -y git python3-pip ripgrep jq
sudo apt-get install -y build-essential libssl-dev libffi-dev # for rebuilding cryptography (required for pypy2)
sudo apt-get purge -y python3-virtualenv
sudo pip3 install virtualenv
sudo apt-get install -y python3-pip
pip3 install -U pip
pip3 install 'virtualenv!=20.1.0'
./run_tests.sh --uninstall --envs all
./run_tests.sh --install --envs all
grep 'cd /vagrant' /home/vagrant/.bashrc || echo 'cd /vagrant' >> /home/vagrant/.bashrc
grep 'source .venv27/bin/activate' /home/vagrant/.bashrc || echo 'source .venv27/bin/activate' >> /home/vagrant/.bashrc
grep 'source .venv36/bin/activate' /home/vagrant/.bashrc || echo 'source .venv36/bin/activate' >> /home/vagrant/.bashrc
EOF
INSTALL_JENKINS=<<EOF
@ -31,7 +32,7 @@ EOF
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "ubuntu/xenial64"
config.vm.box = "ubuntu/focal64"
config.vm.define "dev" do |dev|
dev.vm.provision "gitlint", type: "shell", inline: "#{INSTALL_DEPS}"

View file

@ -80,7 +80,7 @@ min-length=5
words=wip
[title-match-regex]
# python like regex (https://docs.python.org/2/library/re.html) that the
# python like regex (https://docs.python.org/3/library/re.html) that the
# commit-msg title must be matched to.
# Note that the regex can contradict with other rules if not used correctly
# (e.g. title-must-not-contain-word).
@ -100,8 +100,8 @@ ignore-merge-commits=false
[body-changed-file-mention]
# List of files that need to be explicitly mentioned in the body when they are changed
# This is useful for when developers often erroneously edit certain files or git submodules.
# By specifying this rule, developers can only change the file when they explicitly reference
# it in the commit message.
# By specifying this rule, developers can only change the file when they explicitly
# reference it in the commit message.
files=gitlint/rules.py,README.md
[body-match-regex]
@ -110,9 +110,10 @@ files=gitlint/rules.py,README.md
regex=My-Commit-Tag: foo$
[author-valid-email]
# python like regex (https://docs.python.org/2/library/re.html) that the
# python like regex (https://docs.python.org/3/library/re.html) that the
# commit author email address should be matched to
# E.g.: For example, use the following regex if you only want to allow email addresses from foo.com
# E.g.: For example, use the following regex if you only want to allow email
# addresses from foo.com
regex=[^@]+@foo.com
[ignore-by-title]
@ -330,8 +331,10 @@ Default value | gitlint version | commandline flag | environment
```sh
# CLI
gitlint --contrib=contrib-title-conventional-commits,CC1
gitlint -c general.contrib=contrib-title-conventional-commits,CC1 # different way of doing the same
GITLINT_CONTRIB=contrib-title-conventional-commits,CC1 gitlint # using env variable
# different way of doing the same
gitlint -c general.contrib=contrib-title-conventional-commits,CC1
# using env variable
GITLINT_CONTRIB=contrib-title-conventional-commits,CC1 gitlint
```
```ini
#.gitlint
@ -341,7 +344,7 @@ contrib=contrib-title-conventional-commits,CC1
### staged
Fetch additional meta-data from the local `repository when manually passing a commit message to gitlint via stdin or `--commit-msg`.
Fetch additional meta-data from the local repository when manually passing a commit message to gitlint via stdin or `--commit-msg`.
Default value | gitlint version | commandline flag | environment variable
---------------|------------------|-------------------|-----------------------

View file

@ -34,7 +34,8 @@ and it's likely that your PR will be merged and released a lot sooner. Thanks!
## Development
There is a Vagrantfile in this repository that can be used for development.
There is a Vagrantfile (Ubuntu) in this repository that can be used for development.
It comes pre-installed with all Python versions that gitlint supports.
```sh
vagrant up
vagrant ssh
@ -51,7 +52,7 @@ python setup.py develop
To run tests:
```sh
./run_tests.sh # run unit tests and print test coverage
./run_test.sh gitlint/tests/test_body_rules.py::BodyRuleTests::test_body_missing # run a single test
./run_tests.sh gitlint/tests/rules/test_body_rules.py::BodyRuleTests::test_body_missing # run a single test
./run_tests.sh --no-coverage # run unit tests without test coverage
./run_tests.sh --collect-only --no-coverage # Only collect, don't run unit tests
./run_tests.sh --integration # Run integration tests (requires that you have gitlint installed)
@ -63,12 +64,12 @@ To run tests:
./run_tests.sh --all # Run unit, integration, pep8 and gitlint checks
```
The `Vagrantfile` comes with `virtualenv`s for python 2.7, 3.5, 3.6, 3.7, 3.8, 3.9 and pypy2.
The `Vagrantfile` comes with `virtualenv`s for python 3.6, 3.7, 3.8, 3.9 and pypy3.6.
You can easily run tests against specific python environments by using the following commands *inside* of the Vagrant VM:
```sh
./run_tests.sh --envs 27 # Run the unit tests against Python 2.7
./run_tests.sh --envs 27,37,pypy2 # Run the unit tests against Python 2.7, Python 3.7 and Pypy2
./run_tests.sh --envs 27,37 --pep8 # Run pep8 checks against Python 2.7 and Python 3.7 (also works for --git, --integration, --pep8, --stats and --lint.
./run_tests.sh --envs 36 # Run the unit tests against Python 3.6
./run_tests.sh --envs 36,37,pypy36 # Run the unit tests against Python 3.6, Python 3.7 and Pypy3.6
./run_tests.sh --envs 36,37 --pep8 # Run pep8 checks against Python 3.6 and Python 3.7 (also works for --git, --integration, --pep8, --stats and --lint.
./run_tests.sh --envs all --all # Run all tests against all environments
./run_tests.sh --all-env --all # Idem: Run all tests against all environments
```

View file

@ -15,7 +15,7 @@ Great for use as a [commit-msg git hook](#using-gitlint-as-a-commit-msg-hook) or
!!! important
**Gitlint will soon be dropping support for Python 2.7 and Python 3.5 as they [have reached End-Of-Life](https://endoflife.date/python)**.
**Gitlint no longer supports Python 2.7 and Python 3.5 as they [have reached End-Of-Life](https://endoflife.date/python). The last gitlint version to support Python 2.7 and Python 3.5 is `0.14.0` (released on October 24th, 2020).**
## Features
- **Commit message hook**: [Auto-trigger validations against new commit message right when you're committing](#using-gitlint-as-a-commit-msg-hook). Also [works with pre-commit](#using-gitlint-through-pre-commit).
@ -39,8 +39,8 @@ useful throughout the years.
pip install gitlint
# macOS
brew tap rockyluke/devops
brew install gitlint
sudo port install gitlint # alternative using macports
# Ubuntu
apt-get install gitlint
@ -219,11 +219,14 @@ your `.pre-commit-config.yaml` file like so:
rev: # Fill in a tag / sha here
hooks:
- id: gitlint
stages: [commit-msg]
entry: gitlint
args: [--contrib=CT1, --msg-filename]
```
!!! important
You need to add `--msg-filename` at the end of your custom `args` list as the gitlint-hook will fail otherwise.
## Using gitlint in a CI environment
By default, when just running `gitlint` without additional parameters, gitlint lints the last commit in the current
working directory.
@ -426,4 +429,4 @@ Exit Code | Description
-----------|------------------------------------------------------------
253 | Wrong invocation of the `gitlint` command.
254 | Something went wrong when invoking git.
255 | Invalid gitlint configuration
255 | Invalid gitlint configuration

View file

@ -255,8 +255,8 @@ files | >= 0.4 | (empty) | Comma-separated list o
#### .gitlint
```ini
# Prevent that certain sensitive files are committed by mistake by forcing users to mention them explicitly if they're
# deliberately changing them
# Prevent that certain sensitive files are committed by mistake by forcing
# users to mention them explicitly if they're deliberately changing them
[body-changed-file-mention]
files=generated.xml,secrets.txt,private-key.pem
```

View file

@ -152,7 +152,7 @@ class SpecialChars(LineRule):
# options can be accessed by looking them up by their name in self.options
for char in self.options['special-chars'].value:
if char in line:
msg = "Title contains the special character '{0}'".format(char)
msg = f"Title contains the special character '{char}'"
violation = RuleViolation(self.id, msg, line)
violations.append(violation)
@ -262,8 +262,7 @@ class BodyMaxLineCount(CommitRule):
line_count = len(commit.message.body)
max_line_count = self.options['max-line-count'].value
if line_count > max_line_count:
message = "Body contains too many lines ({0} > {1})".format(line_count,
max_line_count)
message = f"Body contains too many lines ({line_count} > {max_line_count})"
return [RuleViolation(self.id, message, line_nr=1)]
```
@ -371,7 +370,7 @@ class ReleaseConfigurationRule(ConfigurationRule):
# You can add any extra properties you want to the commit object,
# these will be available later on in all rules.
commit.my_property = u"This is my property"
commit.my_property = "This is my property"
```
For all available properties and methods on the `config` object, have a look at the

View file

@ -2,8 +2,6 @@
from gitlint.rules import CommitRule, RuleViolation
from gitlint.options import IntOption, ListOption
from gitlint import utils
"""
Full details on user-defined rules: https://jorisroovers.com/gitlint/user_defined_rules
@ -37,7 +35,7 @@ class BodyMaxLineCount(CommitRule):
line_count = len(commit.message.body)
max_line_count = self.options['max-line-count'].value
if line_count > max_line_count:
message = "Body contains too many lines ({0} > {1})".format(line_count, max_line_count)
message = f"Body contains too many lines ({line_count} > {max_line_count})"
return [RuleViolation(self.id, message, line_nr=1)]
@ -90,8 +88,7 @@ class BranchNamingConventions(CommitRule):
break
if not valid_branch_name:
msg = "Branch name '{0}' does not start with one of {1}".format(branch,
utils.sstr(allowed_branch_prefixes))
msg = f"Branch name '{branch}' does not start with one of {allowed_branch_prefixes}"
violations.append(RuleViolation(self.id, msg, line_nr=1))
return violations

View file

@ -69,4 +69,4 @@ class ReleaseConfigurationRule(ConfigurationRule):
# You can add any extra properties you want to the commit object, these will be available later on
# in all rules.
commit.my_property = u"This is my property"
commit.my_property = "This is my property"

View file

@ -45,7 +45,7 @@ class SpecialChars(LineRule):
# options can be accessed by looking them up by their name in self.options
for char in self.options['special-chars'].value:
if char in line:
msg = "Title contains the special character '{0}'".format(char)
msg = f"Title contains the special character '{char}'"
violation = RuleViolation(self.id, msg, line)
violations.append(violation)

View file

@ -1 +1 @@
__version__ = "0.14.0"
__version__ = "0.15.0"

View file

@ -1,4 +1,4 @@
class PropertyCache(object):
class PropertyCache:
""" Mixin class providing a simple cache. """
def __init__(self):
@ -13,7 +13,7 @@ class PropertyCache(object):
return self._cache[cache_key]
def cache(original_func=None, cachekey=None):
def cache(original_func=None, cachekey=None): # pylint: disable=unused-argument
""" Cache decorator. Caches function return values.
Requires the parent class to extend and initialize PropertyCache.
Usage:
@ -28,27 +28,23 @@ def cache(original_func=None, cachekey=None):
...
"""
# Decorators with optional arguments are a bit convoluted in python, especially if you want to support both
# Python 2 and 3. See some of the links below for details.
# Decorators with optional arguments are a bit convoluted in python, see some of the links below for details.
def cache_decorator(func):
# If no specific cache key is given, use the function name as cache key
if not cache_decorator.cachekey:
cache_decorator.cachekey = func.__name__
# Use 'nonlocal' keyword to access parent function variable:
# https://stackoverflow.com/a/14678445/381010
nonlocal cachekey
if not cachekey:
cachekey = func.__name__
def wrapped(*args):
def cache_func_result():
# Call decorated function and store its result in the cache
args[0]._cache[cache_decorator.cachekey] = func(*args)
return args[0]._try_cache(cache_decorator.cachekey, cache_func_result)
args[0]._cache[cachekey] = func(*args)
return args[0]._try_cache(cachekey, cache_func_result)
return wrapped
# Passing parent function variables to child functions requires special voodoo in python2:
# https://stackoverflow.com/a/14678445/381010
cache_decorator.cachekey = cachekey # attribute on the function
# To support optional kwargs for decorators, we need to check if a function is passed as first argument or not.
# https://stackoverflow.com/a/24617244/381010
if original_func:

View file

@ -8,19 +8,20 @@ import stat
import sys
import click
# Error codes
MAX_VIOLATION_ERROR_CODE = 252 # noqa
USAGE_ERROR_CODE = 253 # noqa
GIT_CONTEXT_ERROR_CODE = 254 # noqa
CONFIG_ERROR_CODE = 255 # noqa
import gitlint
from gitlint.lint import GitLinter
from gitlint.config import LintConfigBuilder, LintConfigError, LintConfigGenerator
from gitlint.git import GitContext, GitContextError, git_version
from gitlint import hooks
from gitlint.shell import shell
from gitlint.utils import ustr, LOG_FORMAT, IS_PY2
from gitlint.utils import LOG_FORMAT
from gitlint.exception import GitlintError
# Error codes
MAX_VIOLATION_ERROR_CODE = 252
USAGE_ERROR_CODE = 253
GIT_CONTEXT_ERROR_CODE = 254
CONFIG_ERROR_CODE = 255
DEFAULT_CONFIG_FILE = ".gitlint"
# -n: disable swap files. This fixes a vim error on windows (E303: Unable to open swap file for <path>)
@ -34,7 +35,7 @@ click.UsageError.exit_code = USAGE_ERROR_CODE
LOG = logging.getLogger("gitlint.cli")
class GitLintUsageError(Exception):
class GitLintUsageError(GitlintError):
""" Exception indicating there is an issue with how gitlint is used. """
pass
@ -134,7 +135,7 @@ def get_stdin_data():
# Only return the input data if there's actually something passed
# i.e. don't consider empty piped data
if input_data:
return ustr(input_data)
return str(input_data)
return False
@ -151,7 +152,7 @@ def build_git_context(lint_config, msg_filename, refspec):
# 1. Any data specified via --msg-filename
if msg_filename:
LOG.debug("Using --msg-filename.")
return from_commit_msg(ustr(msg_filename.read()))
return from_commit_msg(str(msg_filename.read()))
# 2. Any data sent to stdin (unless stdin is being ignored)
if not lint_config.ignore_stdin:
@ -162,15 +163,28 @@ def build_git_context(lint_config, msg_filename, refspec):
return from_commit_msg(stdin_input)
if lint_config.staged:
raise GitLintUsageError(u"The 'staged' option (--staged) can only be used when using '--msg-filename' or "
u"when piping data to gitlint via stdin.")
raise GitLintUsageError("The 'staged' option (--staged) can only be used when using '--msg-filename' or "
"when piping data to gitlint via stdin.")
# 3. Fallback to reading from local repository
LOG.debug("No --msg-filename flag, no or empty data passed to stdin. Using the local repo.")
return GitContext.from_local_repository(lint_config.target, refspec)
class ContextObj(object):
def handle_gitlint_error(ctx, exc):
""" Helper function to handle exceptions """
if isinstance(exc, GitContextError):
click.echo(exc)
ctx.exit(GIT_CONTEXT_ERROR_CODE)
elif isinstance(exc, GitLintUsageError):
click.echo(f"Error: {exc}")
ctx.exit(USAGE_ERROR_CODE)
elif isinstance(exc, LintConfigError):
click.echo(f"Config Error: {exc}")
ctx.exit(CONFIG_ERROR_CODE)
class ContextObj:
""" Simple class to hold data that is passed between Click commands via the Click context. """
def __init__(self, config, config_builder, refspec, msg_filename, gitcontext=None):
@ -187,7 +201,7 @@ class ContextObj(object):
type=click.Path(exists=True, resolve_path=True, file_okay=False, readable=True),
help="Path of the target git repository. [default: current working directory]")
@click.option('-C', '--config', type=click.Path(exists=True, dir_okay=False, readable=True, resolve_path=True),
help="Config file location [default: {0}]".format(DEFAULT_CONFIG_FILE))
help=f"Config file location [default: {DEFAULT_CONFIG_FILE}]")
@click.option('-c', multiple=True,
help="Config flags in format <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
@ -230,7 +244,7 @@ def cli( # pylint: disable=too-many-arguments
# 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)
LOG.debug(u"Configuration\n%s", ustr(config))
LOG.debug("Configuration\n%s", config)
ctx.obj = ContextObj(config, config_builder, commits, msg_filename)
@ -238,15 +252,8 @@ def cli( # pylint: disable=too-many-arguments
if ctx.invoked_subcommand is None:
ctx.invoke(lint)
except GitContextError as e:
click.echo(ustr(e))
ctx.exit(GIT_CONTEXT_ERROR_CODE)
except GitLintUsageError as e:
click.echo(u"Error: {0}".format(ustr(e)))
ctx.exit(USAGE_ERROR_CODE)
except LintConfigError as e:
click.echo(u"Config Error: {0}".format(ustr(e)))
ctx.exit(CONFIG_ERROR_CODE)
except GitlintError as e:
handle_gitlint_error(ctx, e)
@cli.command("lint")
@ -294,7 +301,7 @@ def lint(ctx):
if violations:
# Display the commit hash & new lines intelligently
if number_of_commits > 1 and commit.sha:
linter.display.e(u"{0}Commit {1}:".format(
linter.display.e("{0}Commit {1}:".format(
"\n" if not first_violation or commit is last_commit else "",
commit.sha[:10]
))
@ -315,10 +322,10 @@ def install_hook(ctx):
try:
hooks.GitHookInstaller.install_commit_msg_hook(ctx.obj.config)
hook_path = hooks.GitHookInstaller.commit_msg_hook_path(ctx.obj.config)
click.echo(u"Successfully installed gitlint commit-msg hook in {0}".format(hook_path))
click.echo(f"Successfully installed gitlint commit-msg hook in {hook_path}")
ctx.exit(0)
except hooks.GitHookInstallerError as e:
click.echo(ustr(e), err=True)
click.echo(e, err=True)
ctx.exit(GIT_CONTEXT_ERROR_CODE)
@ -329,10 +336,10 @@ def uninstall_hook(ctx):
try:
hooks.GitHookInstaller.uninstall_commit_msg_hook(ctx.obj.config)
hook_path = hooks.GitHookInstaller.commit_msg_hook_path(ctx.obj.config)
click.echo(u"Successfully uninstalled gitlint commit-msg hook from {0}".format(hook_path))
click.echo(f"Successfully uninstalled gitlint commit-msg hook from {hook_path}")
ctx.exit(0)
except hooks.GitHookInstallerError as e:
click.echo(ustr(e), err=True)
click.echo(e, err=True)
ctx.exit(GIT_CONTEXT_ERROR_CODE)
@ -344,8 +351,10 @@ def run_hook(ctx):
exit_code = 1
while exit_code > 0:
try:
click.echo(u"gitlint: checking commit message...")
click.echo("gitlint: checking commit message...")
ctx.invoke(lint)
except GitlintError as e:
handle_gitlint_error(ctx, e)
except click.exceptions.Exit as e:
# Flush stderr andstdout, this resolves an issue with output ordering in Cygwin
sys.stderr.flush()
@ -353,11 +362,11 @@ def run_hook(ctx):
exit_code = e.exit_code
if exit_code == 0:
click.echo(u"gitlint: " + click.style("OK", fg='green') + u" (no violations in commit message)")
click.echo("gitlint: " + click.style("OK", fg='green') + " (no violations in commit message)")
continue
click.echo(u"-----------------------------------------------")
click.echo(u"gitlint: " + click.style("Your commit message contains the above violations.", fg='red'))
click.echo("-----------------------------------------------")
click.echo("gitlint: " + click.style("Your commit message contains the above violations.", fg='red'))
value = None
while value not in ["y", "n", "e"]:
@ -374,14 +383,7 @@ def run_hook(ctx):
# - https://github.com/pallets/click/pull/1372
# - From https://click.palletsprojects.com/en/7.x/utils/#getting-characters-from-terminal
# Note that this function will always read from the terminal, even if stdin is instead a pipe.
#
# We also need a to use raw_input() in Python2 as input() is unsafe (and raw_input() doesn't exist in
# Python3). See https://stackoverflow.com/a/4960216/381010
input_func = input
if IS_PY2:
input_func = raw_input # noqa pylint: disable=undefined-variable
value = input_func()
value = input()
if value == "y":
LOG.debug("run-hook: commit message accepted")
@ -396,15 +398,15 @@ def run_hook(ctx):
LOG.debug("run-hook: %s %s", editor, msg_filename_path)
shell(editor + " " + msg_filename_path)
else:
click.echo(u"Editing only possible when --msg-filename is specified.")
click.echo("Editing only possible when --msg-filename is specified.")
ctx.exit(exit_code)
elif value == "n":
LOG.debug("run-hook: commit message declined")
click.echo(u"Commit aborted.")
click.echo(u"Your commit message: ")
click.echo(u"-----------------------------------------------")
click.echo("Commit aborted.")
click.echo("Your commit message: ")
click.echo("-----------------------------------------------")
click.echo(ctx.obj.gitcontext.commits[0].message.full)
click.echo(u"-----------------------------------------------")
click.echo("-----------------------------------------------")
ctx.exit(exit_code)
ctx.exit(exit_code)
@ -418,14 +420,14 @@ def generate_config(ctx):
path = os.path.realpath(path)
dir_name = os.path.dirname(path)
if not os.path.exists(dir_name):
click.echo(u"Error: Directory '{0}' does not exist.".format(dir_name), err=True)
click.echo(f"Error: Directory '{dir_name}' does not exist.", err=True)
ctx.exit(USAGE_ERROR_CODE)
elif os.path.exists(path):
click.echo(u"Error: File \"{0}\" already exists.".format(path), err=True)
click.echo(f"Error: File \"{path}\" already exists.", err=True)
ctx.exit(USAGE_ERROR_CODE)
LintConfigGenerator.generate_config(path)
click.echo(u"Successfully generated {0}".format(path))
click.echo(f"Successfully generated {path}")
ctx.exit(0)

View file

@ -1,9 +1,4 @@
try:
# python 2.x
from ConfigParser import ConfigParser, Error as ConfigParserError
except ImportError: # pragma: no cover
# python 3.x
from configparser import ConfigParser, Error as ConfigParserError # pragma: no cover, pylint: disable=import-error
from configparser import ConfigParser, Error as ConfigParserError
import copy
import io
@ -12,11 +7,12 @@ import os
import shutil
from collections import OrderedDict
from gitlint.utils import ustr, sstr, DEFAULT_ENCODING
from gitlint.utils import DEFAULT_ENCODING
from gitlint import rules # For some weird reason pylint complains about this, pylint: disable=unused-import
from gitlint import options
from gitlint import rule_finder
from gitlint.contrib import rules as contrib_rules
from gitlint.exception import GitlintError
def handle_option_error(func):
@ -27,16 +23,16 @@ def handle_option_error(func):
try:
return func(*args)
except options.RuleOptionError as e:
raise LintConfigError(ustr(e))
raise LintConfigError(str(e)) from e
return wrapped
class LintConfigError(Exception):
class LintConfigError(GitlintError):
pass
class LintConfig(object):
class LintConfig:
""" Class representing gitlint configuration.
Contains active config as well as number of methods to easily get/set the config.
"""
@ -198,7 +194,7 @@ class LintConfig(object):
self.rules.add_rules(rule_classes, {'is_user_defined': True})
except (options.RuleOptionError, rules.UserRuleError) as e:
raise LintConfigError(ustr(e))
raise LintConfigError(str(e)) from e
@property
def contrib(self):
@ -219,27 +215,25 @@ class LintConfig(object):
# For each specified contrib rule, check whether it exists among the contrib classes
for rule_id_or_name in self.contrib:
rule_class = next((rc for rc in rule_classes if
rc.id == ustr(rule_id_or_name) or rc.name == ustr(rule_id_or_name)), False)
rule_id_or_name in (rc.id, rc.name)), False)
# If contrib rule exists, instantiate it and add it to the rules list
if rule_class:
self.rules.add_rule(rule_class, rule_class.id, {'is_contrib': True})
else:
raise LintConfigError(u"No contrib rule with id or name '{0}' found.".format(ustr(rule_id_or_name)))
raise LintConfigError(f"No contrib rule with id or name '{rule_id_or_name}' found.")
except (options.RuleOptionError, rules.UserRuleError) as e:
raise LintConfigError(ustr(e))
raise LintConfigError(str(e)) from e
def _get_option(self, rule_name_or_id, option_name):
rule_name_or_id = ustr(rule_name_or_id) # convert to unicode first
option_name = ustr(option_name)
rule = self.rules.find_rule(rule_name_or_id)
if not rule:
raise LintConfigError(u"No such rule '{0}'".format(rule_name_or_id))
raise LintConfigError(f"No such rule '{rule_name_or_id}'")
option = rule.options.get(option_name)
if not option:
raise LintConfigError(u"Rule '{0}' has no option '{1}'".format(rule_name_or_id, option_name))
raise LintConfigError(f"Rule '{rule_name_or_id}' has no option '{option_name}'")
return option
@ -256,14 +250,14 @@ class LintConfig(object):
try:
option.set(option_value)
except options.RuleOptionError as e:
msg = u"'{0}' is not a valid value for option '{1}.{2}'. {3}."
raise LintConfigError(msg.format(option_value, rule_name_or_id, option_name, ustr(e)))
msg = f"'{option_value}' is not a valid value for option '{rule_name_or_id}.{option_name}'. {e}."
raise LintConfigError(msg) from e
def set_general_option(self, option_name, option_value):
attr_name = option_name.replace("-", "_")
# only allow setting general options that exist and don't start with an underscore
if not hasattr(self, attr_name) or attr_name[0] == "_":
raise LintConfigError(u"'{0}' is not a valid gitlint option".format(option_name))
raise LintConfigError(f"'{option_name}' is not a valid gitlint option")
# else:
setattr(self, attr_name, option_value)
@ -285,30 +279,26 @@ class LintConfig(object):
self.ignore == other.ignore and \
self._config_path == other._config_path # noqa
def __ne__(self, other):
return not self.__eq__(other) # required for py2
def __str__(self):
# config-path is not a user exposed variable, so don't print it under the general section
return_str = u"config-path: {0}\n".format(self._config_path)
return_str += u"[GENERAL]\n"
return_str += u"extra-path: {0}\n".format(self.extra_path)
return_str += u"contrib: {0}\n".format(sstr(self.contrib))
return_str += u"ignore: {0}\n".format(",".join(self.ignore))
return_str += u"ignore-merge-commits: {0}\n".format(self.ignore_merge_commits)
return_str += u"ignore-fixup-commits: {0}\n".format(self.ignore_fixup_commits)
return_str += u"ignore-squash-commits: {0}\n".format(self.ignore_squash_commits)
return_str += u"ignore-revert-commits: {0}\n".format(self.ignore_revert_commits)
return_str += u"ignore-stdin: {0}\n".format(self.ignore_stdin)
return_str += u"staged: {0}\n".format(self.staged)
return_str += u"verbosity: {0}\n".format(self.verbosity)
return_str += u"debug: {0}\n".format(self.debug)
return_str += u"target: {0}\n".format(self.target)
return_str += u"[RULES]\n{0}".format(self.rules)
return return_str
return (f"config-path: {self._config_path}\n"
f"[GENERAL]\n"
f"extra-path: {self.extra_path}\n"
f"contrib: {self.contrib}\n"
f"ignore: {','.join(self.ignore)}\n"
f"ignore-merge-commits: {self.ignore_merge_commits}\n"
f"ignore-fixup-commits: {self.ignore_fixup_commits}\n"
f"ignore-squash-commits: {self.ignore_squash_commits}\n"
f"ignore-revert-commits: {self.ignore_revert_commits}\n"
f"ignore-stdin: {self.ignore_stdin}\n"
f"staged: {self.staged}\n"
f"verbosity: {self.verbosity}\n"
f"debug: {self.debug}\n"
f"target: {self.target}\n"
f"[RULES]\n{self.rules}")
class RuleCollection(object):
class RuleCollection:
""" Class representing an ordered list of rules. Methods are provided to easily retrieve, add or delete rules. """
def __init__(self, rule_classes=None, rule_attrs=None):
@ -318,8 +308,6 @@ class RuleCollection(object):
self.add_rules(rule_classes, rule_attrs)
def find_rule(self, rule_id_or_name):
# try finding rule by id
rule_id_or_name = ustr(rule_id_or_name) # convert to unicode first
rule = self._rules.get(rule_id_or_name)
# if not found, try finding rule by name
if not rule:
@ -351,7 +339,7 @@ class RuleCollection(object):
""" Deletes all rules from the collection that match a given attribute name and value """
# Create a new list based on _rules.values() because in python 3, values() is a ValuesView as opposed to a list
# This means you can't modify the ValueView while iterating over it.
for rule in [r for r in self._rules.values()]:
for rule in [r for r in self._rules.values()]: # pylint: disable=unnecessary-comprehension
if hasattr(rule, attr_name) and (getattr(rule, attr_name) == attr_val):
del self._rules[rule.id]
@ -362,19 +350,13 @@ class RuleCollection(object):
def __eq__(self, other):
return isinstance(other, RuleCollection) and self._rules == other._rules
def __ne__(self, other):
return not self.__eq__(other) # required for py2
def __len__(self):
return len(self._rules)
def __repr__(self):
return self.__unicode__() # pragma: no cover
def __unicode__(self):
def __str__(self):
return_str = ""
for rule in self._rules.values():
return_str += u" {0}: {1}\n".format(rule.id, rule.name)
return_str += f" {rule.id}: {rule.name}\n"
for option_name, option_value in sorted(rule.options.items()):
if option_value.value is None:
option_val_repr = None
@ -384,11 +366,11 @@ class RuleCollection(object):
option_val_repr = option_value.value.pattern
else:
option_val_repr = option_value.value
return_str += u" {0}={1}\n".format(option_name, option_val_repr)
return_str += f" {option_name}={option_val_repr}\n"
return return_str
class LintConfigBuilder(object):
class LintConfigBuilder:
""" Factory class that can build gitlint config.
This is primarily useful to deal with complex configuration scenarios where configuration can be set and overridden
from various sources (typically according to certain precedence rules) before the actual config should be
@ -427,28 +409,27 @@ class LintConfigBuilder(object):
raise ValueError()
rule_name, option_name = config_name.split(".", 1)
self.set_option(rule_name, option_name, option_value)
except ValueError: # raised if the config string is invalid
except ValueError as e: # raised if the config string is invalid
raise LintConfigError(
u"'{0}' is an invalid configuration option. Use '<rule>.<option>=<value>'".format(config_option))
f"'{config_option}' is an invalid configuration option. Use '<rule>.<option>=<value>'") from e
def set_from_config_file(self, filename):
""" Loads lint config from a ini-style config file """
if not os.path.exists(filename):
raise LintConfigError(u"Invalid file path: {0}".format(filename))
raise LintConfigError(f"Invalid file path: {filename}")
self._config_path = os.path.realpath(filename)
try:
parser = ConfigParser()
with io.open(filename, encoding=DEFAULT_ENCODING) as config_file:
# readfp() is deprecated in python 3.2+, but compatible with 2.7
parser.readfp(config_file, filename) # pylint: disable=deprecated-method
parser.read_file(config_file, filename)
for section_name in parser.sections():
for option_name, option_value in parser.items(section_name):
self.set_option(section_name, option_name, ustr(option_value))
self.set_option(section_name, option_name, str(option_value))
except ConfigParserError as e:
raise LintConfigError(ustr(e))
raise LintConfigError(str(e)) from e
def _add_named_rule(self, config, qualified_rule_name):
""" Adds a Named Rule to a given LintConfig object.
@ -465,14 +446,14 @@ class LintConfigBuilder(object):
# - not empty
# - no whitespace or colons
if rule_name == "" or bool(re.search("\\s|:", rule_name, re.UNICODE)):
msg = u"The rule-name part in '{0}' cannot contain whitespace, colons or be empty"
raise LintConfigError(msg.format(qualified_rule_name))
msg = f"The rule-name part in '{qualified_rule_name}' cannot contain whitespace, colons or be empty"
raise LintConfigError(msg)
# find parent rule
parent_rule = config.rules.find_rule(parent_rule_specifier)
if not parent_rule:
msg = u"No such rule '{0}' (named rule: '{1}')"
raise LintConfigError(msg.format(parent_rule_specifier, qualified_rule_name))
msg = f"No such rule '{parent_rule_specifier}' (named rule: '{qualified_rule_name}')"
raise LintConfigError(msg)
# Determine canonical id and name by recombining the parent id/name and instance name parts.
canonical_id = parent_rule.__class__.id + self.RULE_QUALIFIER_SYMBOL + rule_name
@ -525,7 +506,7 @@ class LintConfigBuilder(object):
GITLINT_CONFIG_TEMPLATE_SRC_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "files/gitlint")
class LintConfigGenerator(object):
class LintConfigGenerator:
@staticmethod
def generate_config(dest):
""" Generates a gitlint config file at the given destination location.

View file

@ -2,7 +2,6 @@ import re
from gitlint.options import ListOption
from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation
from gitlint.utils import ustr
RULE_REGEX = re.compile(r"[^(]+?(\([^)]+?\))?: .+")
@ -26,14 +25,14 @@ class ConventionalCommit(LineRule):
violations = []
for commit_type in self.options["types"].value:
if line.startswith(ustr(commit_type)):
if line.startswith(commit_type):
break
else:
msg = u"Title does not start with one of {0}".format(', '.join(self.options['types'].value))
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 = u"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))
return violations

View file

@ -1,18 +1,7 @@
import codecs
import locale
from sys import stdout, stderr
from gitlint.utils import IS_PY2
# For some reason, python 2.x sometimes messes up with printing unicode chars to stdout/stderr
# This is mostly when there is a mismatch between the terminal encoding and the python encoding.
# This use-case is primarily triggered when piping input between commands, in particular our integration tests
# tend to trip over this.
if IS_PY2:
stdout = codecs.getwriter(locale.getpreferredencoding())(stdout) # pylint: disable=invalid-name
stderr = codecs.getwriter(locale.getpreferredencoding())(stderr) # pylint: disable=invalid-name
class Display(object):
class Display:
""" Utility class to print stuff to an output stream (stdout by default) based on the config's verbosity """
def __init__(self, lint_config):

4
gitlint/exception.py Normal file
View file

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

View file

@ -8,7 +8,7 @@ from gitlint import shell as sh
from gitlint.shell import CommandNotFound, ErrorReturnCode
from gitlint.cache import PropertyCache, cache
from gitlint.utils import ustr, sstr
from gitlint.exception import GitlintError
# For now, the git date format we use is fixed, but technically this format is determined by `git config log.date`
# We should fix this at some point :-)
@ -17,24 +17,23 @@ GIT_TIMEFORMAT = "YYYY-MM-DD HH:mm:ss Z"
LOG = logging.getLogger(__name__)
class GitContextError(Exception):
class GitContextError(GitlintError):
""" Exception indicating there is an issue with the git context """
pass
class GitNotInstalledError(GitContextError):
def __init__(self):
super(GitNotInstalledError, self).__init__(
u"'git' command not found. You need to install git to use gitlint on a local repository. " +
u"See https://git-scm.com/book/en/v2/Getting-Started-Installing-Git on how to install git.")
super().__init__(
"'git' command not found. You need to install git to use gitlint on a local repository. " +
"See https://git-scm.com/book/en/v2/Getting-Started-Installing-Git on how to install git.")
class GitExitCodeError(GitContextError):
def __init__(self, command, stderr):
self.command = command
self.stderr = stderr
super(GitExitCodeError, self).__init__(
u"An error occurred while executing '{0}': {1}".format(command, stderr))
super().__init__(f"An error occurred while executing '{command}': {stderr}")
def _git(*command_parts, **kwargs):
@ -42,33 +41,33 @@ def _git(*command_parts, **kwargs):
git_kwargs = {'_tty_out': False}
git_kwargs.update(kwargs)
try:
LOG.debug(sstr(command_parts))
LOG.debug(command_parts)
result = sh.git(*command_parts, **git_kwargs) # pylint: disable=unexpected-keyword-arg
# If we reach this point and the result has an exit_code that is larger than 0, this means that we didn't
# get an exception (which is the default sh behavior for non-zero exit codes) and so the user is expecting
# a non-zero exit code -> just return the entire result
if hasattr(result, 'exit_code') and result.exit_code > 0:
return result
return ustr(result)
except CommandNotFound:
raise GitNotInstalledError()
return str(result)
except CommandNotFound as e:
raise GitNotInstalledError from e
except ErrorReturnCode as e: # Something went wrong while executing the git command
error_msg = e.stderr.strip()
error_msg_lower = error_msg.lower()
if '_cwd' in git_kwargs and b"not a git repository" in error_msg_lower:
error_msg = u"{0} is not a git repository.".format(git_kwargs['_cwd'])
raise GitContextError(error_msg)
raise GitContextError(f"{git_kwargs['_cwd']} is not a git repository.") from e
if (b"does not have any commits yet" in error_msg_lower or
b"ambiguous argument 'head': unknown revision" in error_msg_lower):
raise GitContextError(u"Current branch has no commits. Gitlint requires at least one commit to function.")
msg = "Current branch has no commits. Gitlint requires at least one commit to function."
raise GitContextError(msg) from e
raise GitExitCodeError(e.full_cmd, error_msg)
raise GitExitCodeError(e.full_cmd, error_msg) from e
def git_version():
""" Determine the git version installed on this host by calling git --version"""
return _git("--version").replace(u"\n", u"")
return _git("--version").replace("\n", "")
def git_commentchar(repository_path=None):
@ -77,17 +76,17 @@ def git_commentchar(repository_path=None):
# git will return an exit code of 1 if it can't find a config value, in this case we fall-back to # as commentchar
if hasattr(commentchar, 'exit_code') and commentchar.exit_code == 1: # pylint: disable=no-member
commentchar = "#"
return ustr(commentchar).replace(u"\n", u"")
return commentchar.replace("\n", "")
def git_hooks_dir(repository_path):
""" Determine hooks directory for a given target dir """
hooks_dir = _git("rev-parse", "--git-path", "hooks", _cwd=repository_path)
hooks_dir = ustr(hooks_dir).replace(u"\n", u"")
hooks_dir = hooks_dir.replace("\n", "")
return os.path.realpath(os.path.join(repository_path, hooks_dir))
class GitCommitMessage(object):
class GitCommitMessage:
""" Class representing a git commit message. A commit message consists of the following:
- context: The `GitContext` this commit message is part of
- original: The actual commit message as returned by `git log`
@ -106,35 +105,26 @@ class GitCommitMessage(object):
def from_full_message(context, commit_msg_str):
""" Parses a full git commit message by parsing a given string into the different parts of a commit message """
all_lines = commit_msg_str.splitlines()
cutline = u"{0} ------------------------ >8 ------------------------".format(context.commentchar)
cutline = f"{context.commentchar} ------------------------ >8 ------------------------"
try:
cutline_index = all_lines.index(cutline)
except ValueError:
cutline_index = None
lines = [ustr(line) for line in all_lines[:cutline_index] if not line.startswith(context.commentchar)]
lines = [line for line in all_lines[:cutline_index] if not line.startswith(context.commentchar)]
full = "\n".join(lines)
title = lines[0] if lines else ""
body = lines[1:] if len(lines) > 1 else []
return GitCommitMessage(context=context, original=commit_msg_str, full=full, title=title, body=body)
def __unicode__(self):
return self.full # pragma: no cover
def __str__(self):
return sstr(self.__unicode__()) # pragma: no cover
def __repr__(self):
return self.__str__() # pragma: no cover
return self.full
def __eq__(self, other):
return (isinstance(other, GitCommitMessage) and self.original == other.original
and self.full == other.full and self.title == other.title and self.body == other.body) # noqa
def __ne__(self, other):
return not self.__eq__(other) # required for py2
class GitCommit(object):
class GitCommit:
""" Class representing a git commit.
A commit consists of: context, message, author name, author email, date, list of parent commit shas,
list of changed files, list of branch names.
@ -155,39 +145,33 @@ class GitCommit(object):
@property
def is_merge_commit(self):
return self.message.title.startswith(u"Merge")
return self.message.title.startswith("Merge")
@property
def is_fixup_commit(self):
return self.message.title.startswith(u"fixup!")
return self.message.title.startswith("fixup!")
@property
def is_squash_commit(self):
return self.message.title.startswith(u"squash!")
return self.message.title.startswith("squash!")
@property
def is_revert_commit(self):
return self.message.title.startswith(u"Revert")
def __unicode__(self):
format_str = (u"--- Commit Message ----\n%s\n"
u"--- Meta info ---------\n"
u"Author: %s <%s>\nDate: %s\n"
u"is-merge-commit: %s\nis-fixup-commit: %s\n"
u"is-squash-commit: %s\nis-revert-commit: %s\n"
u"Branches: %s\n"
u"Changed Files: %s\n"
u"-----------------------") # pragma: no cover
date_str = arrow.get(self.date).format(GIT_TIMEFORMAT) if self.date else None
return format_str % (ustr(self.message), self.author_name, self.author_email, date_str,
self.is_merge_commit, self.is_fixup_commit, self.is_squash_commit,
self.is_revert_commit, sstr(self.branches), sstr(self.changed_files)) # pragma: no cover
return self.message.title.startswith("Revert")
def __str__(self):
return sstr(self.__unicode__()) # pragma: no cover
def __repr__(self):
return self.__str__() # pragma: no cover
date_str = arrow.get(self.date).format(GIT_TIMEFORMAT) if self.date else None
return (f"--- Commit Message ----\n{self.message}\n"
"--- Meta info ---------\n"
f"Author: {self.author_name} <{self.author_email}>\n"
f"Date: {date_str}\n"
f"is-merge-commit: {self.is_merge_commit}\n"
f"is-fixup-commit: {self.is_fixup_commit}\n"
f"is-squash-commit: {self.is_squash_commit}\n"
f"is-revert-commit: {self.is_revert_commit}\n"
f"Branches: {self.branches}\n"
f"Changed Files: {self.changed_files}\n"
"-----------------------")
def __eq__(self, other):
# skip checking the context as context refers back to this obj, this will trigger a cyclic dependency
@ -199,9 +183,6 @@ class GitCommit(object):
and self.is_squash_commit == other.is_squash_commit and self.is_revert_commit == other.is_revert_commit
and self.changed_files == other.changed_files and self.branches == other.branches) # noqa
def __ne__(self, other):
return not self.__eq__(other) # required for py2
class LocalGitCommit(GitCommit, PropertyCache):
""" Class representing a git commit that exists in the local git repository.
@ -230,7 +211,7 @@ class LocalGitCommit(GitCommit, PropertyCache):
# "YYYY-MM-DD HH:mm:ss Z" -> ISO 8601-like format
# Use arrow for datetime parsing, because apparently python is quirky around ISO-8601 dates:
# http://stackoverflow.com/a/30696682/381010
commit_date = arrow.get(ustr(date), GIT_TIMEFORMAT).datetime
commit_date = arrow.get(date, GIT_TIMEFORMAT).datetime
# Create Git commit object with the retrieved info
commit_msg_obj = GitCommitMessage.from_full_message(self.context, commit_msg)
@ -270,7 +251,7 @@ class LocalGitCommit(GitCommit, PropertyCache):
# safely do this since git branches cannot contain '*' anywhere, so if we find an '*' we know it's output
# from the git CLI and not part of the branch name. See https://git-scm.com/docs/git-check-ref-format
# We also drop the last empty line from the output.
self._cache['branches'] = [ustr(branch.replace("*", "").strip()) for branch in branches[:-1]]
self._cache['branches'] = [branch.replace("*", "").strip() for branch in branches[:-1]]
return self._try_cache("branches", cache_branches)
@ -306,17 +287,17 @@ class StagedLocalGitCommit(GitCommit, PropertyCache):
@cache
def author_name(self):
try:
return ustr(_git("config", "--get", "user.name", _cwd=self.context.repository_path)).strip()
except GitExitCodeError:
raise GitContextError("Missing git configuration: please set user.name")
return _git("config", "--get", "user.name", _cwd=self.context.repository_path).strip()
except GitExitCodeError as e:
raise GitContextError("Missing git configuration: please set user.name") from e
@property
@cache
def author_email(self):
try:
return ustr(_git("config", "--get", "user.email", _cwd=self.context.repository_path)).strip()
except GitExitCodeError:
raise GitContextError("Missing git configuration: please set user.email")
return _git("config", "--get", "user.email", _cwd=self.context.repository_path).strip()
except GitExitCodeError as e:
raise GitContextError("Missing git configuration: please set user.email") from e
@property
@cache
@ -356,7 +337,7 @@ class GitContext(PropertyCache):
@property
@cache
def current_branch(self):
current_branch = ustr(_git("rev-parse", "--abbrev-ref", "HEAD", _cwd=self.repository_path)).strip()
current_branch = _git("rev-parse", "--abbrev-ref", "HEAD", _cwd=self.repository_path).strip()
return current_branch
@staticmethod
@ -396,7 +377,7 @@ class GitContext(PropertyCache):
# 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(u"\n", u"")]
sha_list = [_git("log", "-1", "--pretty=%H", _cwd=repository_path).replace("\n", "")]
else:
sha_list = _git("rev-list", refspec, _cwd=repository_path).split()
@ -410,6 +391,3 @@ class GitContext(PropertyCache):
return (isinstance(other, GitContext) and self.commits == other.commits
and self.repository_path == other.repository_path
and self.commentchar == other.commentchar and self.current_branch == other.current_branch) # noqa
def __ne__(self, other):
return not self.__eq__(other) # required for py2

View file

@ -5,17 +5,18 @@ import stat
from gitlint.utils import DEFAULT_ENCODING
from gitlint.git import git_hooks_dir
from gitlint.exception import GitlintError
COMMIT_MSG_HOOK_SRC_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "files", "commit-msg")
COMMIT_MSG_HOOK_DST_PATH = "commit-msg"
GITLINT_HOOK_IDENTIFIER = "### gitlint commit-msg hook start ###\n"
class GitHookInstallerError(Exception):
class GitHookInstallerError(GitlintError):
pass
class GitHookInstaller(object):
class GitHookInstaller:
""" Utility class that provides methods for installing and uninstalling the gitlint commitmsg hook. """
@staticmethod
@ -27,7 +28,7 @@ class GitHookInstaller(object):
""" Asserts that a given target directory is a git repository """
hooks_dir = git_hooks_dir(target)
if not os.path.isdir(hooks_dir):
raise GitHookInstallerError(u"{0} is not a git repository.".format(target))
raise GitHookInstallerError(f"{target} is not a git repository.")
@staticmethod
def install_commit_msg_hook(lint_config):
@ -35,8 +36,8 @@ class GitHookInstaller(object):
dest_path = GitHookInstaller.commit_msg_hook_path(lint_config)
if os.path.exists(dest_path):
raise GitHookInstallerError(
u"There is already a commit-msg hook file present in {0}.\n".format(dest_path) +
u"gitlint currently does not support appending to an existing commit-msg file.")
f"There is already a commit-msg hook file present in {dest_path}.\n" +
"gitlint currently does not support appending to an existing commit-msg file.")
# copy hook file
shutil.copy(COMMIT_MSG_HOOK_SRC_PATH, dest_path)
@ -49,14 +50,14 @@ class GitHookInstaller(object):
GitHookInstaller._assert_git_repo(lint_config.target)
dest_path = GitHookInstaller.commit_msg_hook_path(lint_config)
if not os.path.exists(dest_path):
raise GitHookInstallerError(u"There is no commit-msg hook present in {0}.".format(dest_path))
raise GitHookInstallerError(f"There is no commit-msg hook present in {dest_path}.")
with io.open(dest_path, encoding=DEFAULT_ENCODING) as fp:
lines = fp.readlines()
if len(lines) < 2 or lines[1] != GITLINT_HOOK_IDENTIFIER:
msg = u"The commit-msg hook in {0} was not installed by gitlint (or it was modified).\n" + \
u"Uninstallation of 3th party or modified gitlint hooks is not supported."
raise GitHookInstallerError(msg.format(dest_path))
msg = f"The commit-msg hook in {dest_path} was not installed by gitlint (or it was modified).\n" + \
"Uninstallation of 3th party or modified gitlint hooks is not supported."
raise GitHookInstallerError(msg)
# If we are sure it's a gitlint hook, go ahead and remove it
os.remove(dest_path)

View file

@ -2,13 +2,12 @@
import logging
from gitlint import rules as gitlint_rules
from gitlint import display
from gitlint.utils import ustr
LOG = logging.getLogger(__name__)
logging.basicConfig()
class GitLinter(object):
class GitLinter:
""" Main linter class. This is where rules actually get applied. See the lint() method. """
def __init__(self, config):
@ -70,7 +69,7 @@ class GitLinter(object):
def lint(self, commit):
""" Lint the last commit in a given git context by applying all ignore, title, body and commit rules. """
LOG.debug("Linting commit %s", commit.sha or "[SHA UNKNOWN]")
LOG.debug("Commit Object\n" + ustr(commit))
LOG.debug("Commit Object\n" + str(commit))
# Apply config rules
for rule in self.configuration_rules:
@ -79,8 +78,8 @@ class GitLinter(object):
# Skip linting if this is a special commit type that is configured to be ignored
ignore_commit_types = ["merge", "squash", "fixup", "revert"]
for commit_type in ignore_commit_types:
if getattr(commit, "is_{0}_commit".format(commit_type)) and \
getattr(self.config, "ignore_{0}_commits".format(commit_type)):
if getattr(commit, f"is_{commit_type}_commit") and \
getattr(self.config, f"ignore_{commit_type}_commits"):
return []
violations = []
@ -99,10 +98,9 @@ class GitLinter(object):
""" Print a given set of violations to the standard error output """
for v in violations:
line_nr = v.line_nr if v.line_nr else "-"
self.display.e(u"{0}: {1}".format(line_nr, v.rule_id), exact=True)
self.display.ee(u"{0}: {1} {2}".format(line_nr, v.rule_id, v.message), exact=True)
self.display.e(f"{line_nr}: {v.rule_id}", exact=True)
self.display.ee(f"{line_nr}: {v.rule_id} {v.message}", exact=True)
if v.content:
self.display.eee(u"{0}: {1} {2}: \"{3}\"".format(line_nr, v.rule_id, v.message, v.content),
exact=True)
self.display.eee(f"{line_nr}: {v.rule_id} {v.message}: \"{v.content}\"", exact=True)
else:
self.display.eee(u"{0}: {1} {2}".format(line_nr, v.rule_id, v.message), exact=True)
self.display.eee(f"{line_nr}: {v.rule_id} {v.message}", exact=True)

View file

@ -2,7 +2,7 @@ from abc import abstractmethod
import os
import re
from gitlint.utils import ustr, sstr
from gitlint.exception import GitlintError
def allow_none(func):
@ -17,11 +17,11 @@ def allow_none(func):
return wrapped
class RuleOptionError(Exception):
class RuleOptionError(GitlintError):
pass
class RuleOption(object):
class RuleOption:
""" Base class representing a configurable part (i.e. option) of a rule (e.g. the max-length of the title-max-line
rule).
This class should not be used directly. Instead, use on the derived classes like StrOption, IntOption to set
@ -29,8 +29,8 @@ class RuleOption(object):
"""
def __init__(self, name, value, description):
self.name = ustr(name)
self.description = ustr(description)
self.name = name
self.description = description
self.value = None
self.set(value)
@ -40,37 +40,28 @@ class RuleOption(object):
pass # pragma: no cover
def __str__(self):
return sstr(self) # pragma: no cover
def __unicode__(self):
return u"({0}: {1} ({2}))".format(self.name, self.value, self.description) # pragma: no cover
def __repr__(self):
return self.__str__() # pragma: no cover
return f"({self.name}: {self.value} ({self.description}))"
def __eq__(self, other):
return self.name == other.name and self.description == other.description and self.value == other.value
def __ne__(self, other):
return not self.__eq__(other) # required for py2
class StrOption(RuleOption):
@allow_none
def set(self, value):
self.value = ustr(value)
self.value = str(value)
class IntOption(RuleOption):
def __init__(self, name, value, description, allow_negative=False):
self.allow_negative = allow_negative
super(IntOption, self).__init__(name, value, description)
super().__init__(name, value, description)
def _raise_exception(self, value):
if self.allow_negative:
error_msg = u"Option '{0}' must be an integer (current value: '{1}')".format(self.name, value)
error_msg = f"Option '{self.name}' must be an integer (current value: '{value}')"
else:
error_msg = u"Option '{0}' must be a positive integer (current value: '{1}')".format(self.name, value)
error_msg = f"Option '{self.name}' must be a positive integer (current value: '{value}')"
raise RuleOptionError(error_msg)
@allow_none
@ -88,9 +79,9 @@ class BoolOption(RuleOption):
# explicit choice to not annotate with @allow_none: Booleans must be False or True, they cannot be unset.
def set(self, value):
value = ustr(value).strip().lower()
value = str(value).strip().lower()
if value not in ['true', 'false']:
raise RuleOptionError(u"Option '{0}' must be either 'true' or 'false'".format(self.name))
raise RuleOptionError(f"Option '{self.name}' must be either 'true' or 'false'")
self.value = value == 'true'
@ -103,37 +94,36 @@ class ListOption(RuleOption):
if isinstance(value, list):
the_list = value
else:
the_list = ustr(value).split(",")
the_list = str(value).split(",")
self.value = [ustr(item.strip()) for item in the_list if item.strip() != ""]
self.value = [str(item.strip()) for item in the_list if item.strip() != ""]
class PathOption(RuleOption):
""" Option that accepts either a directory or both a directory and a file. """
def __init__(self, name, value, description, type=u"dir"):
def __init__(self, name, value, description, type="dir"):
self.type = type
super(PathOption, self).__init__(name, value, description)
super().__init__(name, value, description)
@allow_none
def set(self, value):
value = ustr(value)
value = str(value)
error_msg = u""
error_msg = ""
if self.type == 'dir':
if not os.path.isdir(value):
error_msg = u"Option {0} must be an existing directory (current value: '{1}')".format(self.name, value)
error_msg = f"Option {self.name} must be an existing directory (current value: '{value}')"
elif self.type == 'file':
if not os.path.isfile(value):
error_msg = u"Option {0} must be an existing file (current value: '{1}')".format(self.name, value)
error_msg = f"Option {self.name} must be an existing file (current value: '{value}')"
elif self.type == 'both':
if not os.path.isdir(value) and not os.path.isfile(value):
error_msg = (u"Option {0} must be either an existing directory or file "
u"(current value: '{1}')").format(self.name, value)
error_msg = (f"Option {self.name} must be either an existing directory or file "
f"(current value: '{value}')")
else:
error_msg = u"Option {0} type must be one of: 'file', 'dir', 'both' (current: '{1}')".format(self.name,
self.type)
error_msg = f"Option {self.name} type must be one of: 'file', 'dir', 'both' (current: '{self.type}')"
if error_msg:
raise RuleOptionError(error_msg)
@ -148,7 +138,7 @@ class RegexOption(RuleOption):
try:
self.value = re.compile(value, re.UNICODE)
except (re.error, TypeError) as exc:
raise RuleOptionError("Invalid regular expression: '{0}'".format(exc))
raise RuleOptionError(f"Invalid regular expression: '{exc}'") from exc
def __deepcopy__(self, _):
# copy.deepcopy() - used in rules.py - doesn't support copying regex objects prior to Python 3.7

View file

@ -5,7 +5,6 @@ import sys
import importlib
from gitlint import rules, options
from gitlint.utils import ustr
def find_rule_classes(extra_path):
@ -28,7 +27,7 @@ def find_rule_classes(extra_path):
files = os.listdir(extra_path)
directory = extra_path
else:
raise rules.UserRuleError(u"Invalid extra-path: {0}".format(extra_path))
raise rules.UserRuleError(f"Invalid extra-path: {extra_path}")
# Filter out files that are not python modules
for filename in files:
@ -56,7 +55,7 @@ def find_rule_classes(extra_path):
importlib.import_module(module)
except Exception as e:
raise rules.UserRuleError(u"Error while importing extra-path module '{0}': {1}".format(module, ustr(e)))
raise rules.UserRuleError(f"Error while importing extra-path module '{module}': {e}")
# Find all rule classes in the module. We do this my inspecting all members of the module and checking
# 1) is it a class, if not, skip
@ -94,55 +93,53 @@ def assert_valid_rule_class(clazz, rule_type="User-defined"): # pylint: disable
# Rules must extend from LineRule, CommitRule or ConfigurationRule
if not (issubclass(clazz, rules.LineRule) or issubclass(clazz, rules.CommitRule)
or issubclass(clazz, rules.ConfigurationRule)):
msg = u"{0} rule class '{1}' must extend from {2}.{3}, {2}.{4} or {2}.{5}"
raise rules.UserRuleError(msg.format(rule_type, clazz.__name__, rules.CommitRule.__module__,
rules.LineRule.__name__, rules.CommitRule.__name__,
rules.ConfigurationRule.__name__))
msg = f"{rule_type} rule class '{clazz.__name__}' " + \
f"must extend from {rules.CommitRule.__module__}.{rules.LineRule.__name__}, " + \
f"{rules.CommitRule.__module__}.{rules.CommitRule.__name__} or " + \
f"{rules.CommitRule.__module__}.{rules.ConfigurationRule.__name__}"
raise rules.UserRuleError(msg)
# Rules must have an id attribute
if not hasattr(clazz, 'id') or clazz.id is None or not clazz.id:
msg = u"{0} rule class '{1}' must have an 'id' attribute"
raise rules.UserRuleError(msg.format(rule_type, clazz.__name__))
raise rules.UserRuleError(f"{rule_type} rule class '{clazz.__name__}' must have an 'id' attribute")
# Rule id's cannot start with gitlint reserved letters
if clazz.id[0].upper() in ['R', 'T', 'B', 'M', 'I']:
msg = u"The id '{1}' of '{0}' is invalid. Gitlint reserves ids starting with R,T,B,M,I"
raise rules.UserRuleError(msg.format(clazz.__name__, clazz.id[0]))
msg = f"The id '{clazz.id[0]}' of '{clazz.__name__}' is invalid. Gitlint reserves ids starting with R,T,B,M,I"
raise rules.UserRuleError(msg)
# Rules must have a name attribute
if not hasattr(clazz, 'name') or clazz.name is None or not clazz.name:
msg = u"{0} rule class '{1}' must have a 'name' attribute"
raise rules.UserRuleError(msg.format(rule_type, clazz.__name__))
raise rules.UserRuleError(f"{rule_type} rule class '{clazz.__name__}' must have a 'name' attribute")
# if set, options_spec must be a list of RuleOption
if not isinstance(clazz.options_spec, list):
msg = u"The options_spec attribute of {0} rule class '{1}' must be a list of {2}.{3}"
raise rules.UserRuleError(msg.format(rule_type.lower(), clazz.__name__,
options.RuleOption.__module__, options.RuleOption.__name__))
msg = f"The options_spec attribute of {rule_type.lower()} rule class '{clazz.__name__}' " + \
f"must be a list of {options.RuleOption.__module__}.{options.RuleOption.__name__}"
raise rules.UserRuleError(msg)
# check that all items in options_spec are actual gitlint options
for option in clazz.options_spec:
if not isinstance(option, options.RuleOption):
msg = u"The options_spec attribute of {0} rule class '{1}' must be a list of {2}.{3}"
raise rules.UserRuleError(msg.format(rule_type.lower(), clazz.__name__,
options.RuleOption.__module__, options.RuleOption.__name__))
msg = f"The options_spec attribute of {rule_type.lower()} rule class '{clazz.__name__}' " + \
f"must be a list of {options.RuleOption.__module__}.{options.RuleOption.__name__}"
raise rules.UserRuleError(msg)
# Line/Commit rules must have a `validate` method
# We use isroutine() as it's both python 2 and 3 compatible. Details: http://stackoverflow.com/a/17019998/381010
if (issubclass(clazz, rules.LineRule) or issubclass(clazz, rules.CommitRule)):
if not hasattr(clazz, 'validate') or not inspect.isroutine(clazz.validate):
msg = u"{0} rule class '{1}' must have a 'validate' method"
raise rules.UserRuleError(msg.format(rule_type, clazz.__name__))
raise rules.UserRuleError(f"{rule_type} rule class '{clazz.__name__}' must have a 'validate' method")
# Configuration rules must have an `apply` method
elif issubclass(clazz, rules.ConfigurationRule):
if not hasattr(clazz, 'apply') or not inspect.isroutine(clazz.apply):
msg = u"{0} Configuration rule class '{1}' must have an 'apply' method"
raise rules.UserRuleError(msg.format(rule_type, clazz.__name__))
msg = f"{rule_type} Configuration rule class '{clazz.__name__}' must have an 'apply' method"
raise rules.UserRuleError(msg)
# LineRules must have a valid target: rules.CommitMessageTitle or rules.CommitMessageBody
if issubclass(clazz, rules.LineRule):
if clazz.target not in [rules.CommitMessageTitle, rules.CommitMessageBody]:
msg = u"The target attribute of the {0} LineRule class '{1}' must be either {2}.{3} or {2}.{4}"
msg = msg.format(rule_type.lower(), clazz.__name__, rules.CommitMessageTitle.__module__,
rules.CommitMessageTitle.__name__, rules.CommitMessageBody.__name__)
msg = f"The target attribute of the {rule_type.lower()} LineRule class '{clazz.__name__}' " + \
f"must be either {rules.CommitMessageTitle.__module__}.{rules.CommitMessageTitle.__name__} " + \
f"or {rules.CommitMessageTitle.__module__}.{rules.CommitMessageBody.__name__}"
raise rules.UserRuleError(msg)

View file

@ -4,10 +4,10 @@ import logging
import re
from gitlint.options import IntOption, BoolOption, StrOption, ListOption, RegexOption
from gitlint.utils import sstr
from gitlint.exception import GitlintError
class Rule(object):
class Rule:
""" Class representing gitlint rules. """
options_spec = []
id = None
@ -36,17 +36,8 @@ class Rule(object):
return self.id == other.id and self.name == other.name and \
self.options == other.options and self.target == other.target # noqa
def __ne__(self, other):
return not self.__eq__(other) # required for py2
def __str__(self):
return sstr(self) # pragma: no cover
def __unicode__(self):
return u"{0} {1}".format(self.id, self.name) # pragma: no cover
def __repr__(self):
return self.__str__() # pragma: no cover
return f"{self.id} {self.name}" # pragma: no cover
class ConfigurationRule(Rule):
@ -64,7 +55,7 @@ class LineRule(Rule):
pass
class LineRuleTarget(object):
class LineRuleTarget:
""" Base class for LineRule targets. A LineRuleTarget specifies where a given rule will be applied
(e.g. commit message title, commit message body).
Each LineRule MUST have a target specified. """
@ -81,7 +72,7 @@ class CommitMessageBody(LineRuleTarget):
pass
class RuleViolation(object):
class RuleViolation:
""" Class representing a violation of a rule. I.e.: When a rule is broken, the rule will instantiate this class
to indicate how and where the rule was broken. """
@ -96,21 +87,11 @@ class RuleViolation(object):
equal = equal and self.content == other.content and self.line_nr == other.line_nr
return equal
def __ne__(self, other):
return not self.__eq__(other) # required for py2
def __str__(self):
return sstr(self) # pragma: no cover
def __unicode__(self):
return u"{0}: {1} {2}: \"{3}\"".format(self.line_nr, self.rule_id, self.message,
self.content) # pragma: no cover
def __repr__(self):
return self.__unicode__() # pragma: no cover
return f"{self.line_nr}: {self.rule_id} {self.message}: \"{self.content}\""
class UserRuleError(Exception):
class UserRuleError(GitlintError):
""" Error used to indicate that an error occurred while trying to load a user rule """
pass
@ -154,7 +135,7 @@ class LineMustNotContainWord(LineRule):
name = "line-must-not-contain"
id = "R5"
options_spec = [ListOption('words', [], "Comma separated list of words that should not be found")]
violation_message = u"Line contains {0}"
violation_message = "Line contains {0}"
def validate(self, line, _commit):
strings = self.options['words'].value
@ -202,7 +183,7 @@ class TitleTrailingPunctuation(LineRule):
punctuation_marks = '?:!.,;'
for punctuation_mark in punctuation_marks:
if title.endswith(punctuation_mark):
return [RuleViolation(self.id, u"Title has trailing punctuation ({0})".format(punctuation_mark), title)]
return [RuleViolation(self.id, f"Title has trailing punctuation ({punctuation_mark})", title)]
class TitleHardTab(HardTab):
@ -217,7 +198,7 @@ class TitleMustNotContainWord(LineMustNotContainWord):
id = "T5"
target = CommitMessageTitle
options_spec = [ListOption('words', ["WIP"], "Must not contain word")]
violation_message = u"Title contains the word '{0}' (case-insensitive)"
violation_message = "Title contains the word '{0}' (case-insensitive)"
class TitleLeadingWhitespace(LeadingWhiteSpace):
@ -239,7 +220,7 @@ class TitleRegexMatches(LineRule):
return
if not self.options['regex'].value.search(title):
violation_msg = u"Title does not match regex ({0})".format(self.options['regex'].value.pattern)
violation_msg = f"Title does not match regex ({self.options['regex'].value.pattern})"
return [RuleViolation(self.id, violation_msg, title)]
@ -253,7 +234,7 @@ class TitleMinLength(LineRule):
min_length = self.options['min-length'].value
actual_length = len(title)
if actual_length < min_length:
violation_message = "Title is too short ({0}<{1})".format(actual_length, min_length)
violation_message = f"Title is too short ({actual_length}<{min_length})"
return [RuleViolation(self.id, violation_message, title, 1)]
@ -296,7 +277,7 @@ class BodyMinLength(CommitRule):
body_message_no_newline = "".join([line for line in commit.message.body if line is not None])
actual_length = len(body_message_no_newline)
if 0 < actual_length < min_length:
violation_message = "Body message is too short ({0}<{1})".format(actual_length, min_length)
violation_message = f"Body message is too short ({actual_length}<{min_length})"
return [RuleViolation(self.id, violation_message, body_message_no_newline, 3)]
@ -325,7 +306,7 @@ class BodyChangedFileMention(CommitRule):
# in the commit msg body
if needs_mentioned_file in commit.changed_files:
if needs_mentioned_file not in " ".join(commit.message.body):
violation_message = u"Body does not mention changed file '{0}'".format(needs_mentioned_file)
violation_message = f"Body does not mention changed file '{needs_mentioned_file}'"
violations.append(RuleViolation(self.id, violation_message, None, len(commit.message.body) + 1))
return violations if violations else None
@ -354,7 +335,7 @@ class BodyRegexMatches(CommitRule):
full_body = "\n".join(body_lines)
if not self.options['regex'].value.search(full_body):
violation_msg = u"Body does not match regex ({0})".format(self.options['regex'].value.pattern)
violation_msg = f"Body does not match regex ({self.options['regex'].value.pattern})"
return [RuleViolation(self.id, violation_msg, None, len(commit.message.body) + 1)]
@ -386,9 +367,8 @@ class IgnoreByTitle(ConfigurationRule):
if self.options['regex'].value.match(commit.message.title):
config.ignore = self.options['ignore'].value
message = u"Commit title '{0}' matches the regex '{1}', ignoring rules: {2}"
message = message.format(commit.message.title, self.options['regex'].value.pattern,
self.options['ignore'].value)
message = f"Commit title '{commit.message.title}' 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)
@ -408,8 +388,8 @@ class IgnoreByBody(ConfigurationRule):
if self.options['regex'].value.match(line):
config.ignore = self.options['ignore'].value
message = u"Commit message line '{0}' matches the regex '{1}', ignoring rules: {2}"
message = message.format(line, self.options['regex'].value.pattern, self.options['ignore'].value)
message = f"Commit message line '{line}' matches the regex '{self.options['regex'].value.pattern}'," + \
f" 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
@ -429,10 +409,10 @@ class IgnoreBodyLines(ConfigurationRule):
new_body = []
for line in commit.message.body:
if self.options['regex'].value.match(line):
debug_msg = u"Ignoring line '%s' because it matches '%s'"
debug_msg = "Ignoring line '%s' because it matches '%s'"
self.log.debug(debug_msg, line, self.options['regex'].value.pattern)
else:
new_body.append(line)
commit.message.body = new_body
commit.message.full = u"\n".join([commit.message.title] + new_body)
commit.message.full = "\n".join([commit.message.title] + new_body)

View file

@ -6,7 +6,7 @@ capabilities wrt dealing with more edge-case environments on *nix systems that a
"""
import subprocess
from gitlint.utils import ustr, IS_PY2, USE_SH_LIB
from gitlint.utils import USE_SH_LIB, DEFAULT_ENCODING
def shell(cmd):
@ -25,7 +25,7 @@ else:
""" Exception indicating a command was not found during execution """
pass
class ShResult(object):
class ShResult:
""" Result wrapper class. We use this to more easily migrate from using https://amoffat.github.io/sh/ to using
the builtin subprocess module """
@ -51,11 +51,6 @@ else:
return _exec(*args, **kwargs)
def _exec(*args, **kwargs):
if IS_PY2:
no_command_error = OSError # noqa pylint: disable=undefined-variable,invalid-name
else:
no_command_error = FileNotFoundError # noqa pylint: disable=undefined-variable
pipe = subprocess.PIPE
popen_kwargs = {'stdout': pipe, 'stderr': pipe, 'shell': kwargs.get('_tty_out', False)}
if '_cwd' in kwargs:
@ -64,11 +59,11 @@ else:
try:
p = subprocess.Popen(args, **popen_kwargs)
result = p.communicate()
except no_command_error:
raise CommandNotFound
except FileNotFoundError as e:
raise CommandNotFound from e
exit_code = p.returncode
stdout = ustr(result[0])
stdout = result[0].decode(DEFAULT_ENCODING)
stderr = result[1] # 'sh' does not decode the stderr bytes to unicode
full_cmd = '' if args is None else ' '.join(args)

View file

@ -9,33 +9,12 @@ import re
import shutil
import tempfile
try:
# python 2.x
import unittest2 as unittest
except ImportError:
# python 3.x
import unittest
import unittest
try:
# python 2.x
from mock import patch
except ImportError:
# python 3.x
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
from unittest.mock import patch
from gitlint.git import GitContext
from gitlint.utils import ustr, IS_PY2, LOG_FORMAT, DEFAULT_ENCODING
# unittest2's assertRaisesRegex doesn't do unicode comparison.
# Let's monkeypatch the str() function to point to unicode() so that it does :)
# For reference, this is where this patch is required:
# https://hg.python.org/unittest2/file/tip/unittest2/case.py#l227
try:
# python 2.x
unittest.case.str = unicode
except (AttributeError, NameError):
pass # python 3.x
from gitlint.utils import LOG_FORMAT, DEFAULT_ENCODING
class BaseTestCase(unittest.TestCase):
@ -72,24 +51,22 @@ class BaseTestCase(unittest.TestCase):
def get_sample_path(filename=""):
# Don't join up empty files names because this will add a trailing slash
if filename == "":
return ustr(BaseTestCase.SAMPLES_DIR)
return BaseTestCase.SAMPLES_DIR
return ustr(os.path.join(BaseTestCase.SAMPLES_DIR, filename))
return os.path.join(BaseTestCase.SAMPLES_DIR, filename)
@staticmethod
def get_sample(filename=""):
""" Read and return the contents of a file in gitlint/tests/samples """
sample_path = BaseTestCase.get_sample_path(filename)
with io.open(sample_path, encoding=DEFAULT_ENCODING) as content:
sample = ustr(content.read())
sample = content.read()
return sample
@staticmethod
def patch_input(side_effect):
""" Patches the built-in input() with a provided side-effect """
module_path = "builtins.input"
if IS_PY2:
module_path = "__builtin__.raw_input"
patched_module = patch(module_path, side_effect=side_effect)
return patched_module
@ -99,7 +76,7 @@ class BaseTestCase(unittest.TestCase):
Optionally replace template variables specified by variable_dict. """
expected_path = os.path.join(BaseTestCase.EXPECTED_DIR, filename)
with io.open(expected_path, encoding=DEFAULT_ENCODING) as content:
expected = ustr(content.read())
expected = content.read()
if variable_dict:
expected = expected.format(**variable_dict)
@ -114,7 +91,7 @@ class BaseTestCase(unittest.TestCase):
""" Utility method to easily create gitcontext objects based on a given commit msg string and an optional set of
changed files"""
with patch("gitlint.git.git_commentchar") as comment_char:
comment_char.return_value = u"#"
comment_char.return_value = "#"
gitcontext = GitContext.from_commit_msg(commit_msg_str)
commit = gitcontext.commits[-1]
if changed_files:
@ -147,8 +124,7 @@ class BaseTestCase(unittest.TestCase):
""" Pass-through method to unittest.TestCase.assertRaisesRegex that applies re.escape() to the passed
`expected_regex`. This is useful to automatically escape all file paths that might be present in the regex.
"""
return super(BaseTestCase, self).assertRaisesRegex(expected_exception, re.escape(expected_regex),
*args, **kwargs)
return super().assertRaisesRegex(expected_exception, re.escape(expected_regex), *args, **kwargs)
@contextlib.contextmanager
def assertRaisesMessage(self, expected_exception, expected_msg): # pylint: disable=invalid-name
@ -156,17 +132,17 @@ class BaseTestCase(unittest.TestCase):
try:
yield
except expected_exception as exc:
exception_msg = ustr(exc)
exception_msg = str(exc)
if exception_msg != expected_msg:
error = u"Right exception, wrong message:\n got: {0}\n expected: {1}"
raise self.fail(error.format(exception_msg, expected_msg))
error = f"Right exception, wrong message:\n got: {exception_msg}\n expected: {expected_msg}"
raise self.fail(error)
# else: everything is fine, just return
return
except Exception as exc:
raise self.fail(u"Expected '{0}' got '{1}'".format(expected_exception.__name__, exc.__class__.__name__))
raise self.fail(f"Expected '{expected_exception.__name__}' got '{exc.__class__.__name__}'")
# No exception raised while we expected one
raise self.fail("Expected to raise {0}, didn't get an exception at all".format(expected_exception.__name__))
raise self.fail(f"Expected to raise {expected_exception.__name__}, didn't get an exception at all")
def object_equality_test(self, obj, attr_list, ctor_kwargs=None):
""" Helper function to easily implement object equality tests.
@ -190,9 +166,9 @@ class BaseTestCase(unittest.TestCase):
self.assertEqual(obj, clone)
# Change attribute and assert objects are different (via both attribute set and ctor)
setattr(clone, attr, u"föo")
setattr(clone, attr, "föo")
self.assertNotEqual(obj, clone)
attr_kwargs_copy[attr] = u"föo"
attr_kwargs_copy[attr] = "föo"
self.assertNotEqual(obj, obj.__class__(**attr_kwargs_copy))
@ -205,4 +181,4 @@ class LogCapture(logging.Handler):
self.messages = []
def emit(self, record):
self.messages.append(ustr(self.format(record)))
self.messages.append(self.format(record))

View file

@ -8,21 +8,11 @@ import platform
import arrow
try:
# python 2.x
from StringIO import StringIO
except ImportError:
# python 3.x
from io import StringIO # pylint: disable=ungrouped-imports
from io import StringIO
from click.testing import CliRunner
try:
# python 2.x
from mock import patch
except ImportError:
# python 3.x
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
from unittest.mock import patch
from gitlint.shell import CommandNotFound
@ -59,7 +49,7 @@ class CLITests(BaseTestCase):
def test_version(self):
""" Test for --version option """
result = self.cli.invoke(cli.cli, ["--version"])
self.assertEqual(result.output.split("\n")[0], "cli, version {0}".format(__version__))
self.assertEqual(result.output.split("\n")[0], f"cli, version {__version__}")
@patch('gitlint.cli.get_stdin_data', return_value=False)
@patch('gitlint.git.sh')
@ -67,11 +57,11 @@ class CLITests(BaseTestCase):
""" Test for basic simple linting functionality """
sh.git.side_effect = [
"6f29bf81a8322a04071bb794666e48c443a90360",
u"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
u"commït-title\n\ncommït-body",
u"#", # git config --get core.commentchar
u"commit-1-branch-1\ncommit-1-branch-2\n",
u"file1.txt\npåth/to/file2.txt\n"
"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
"commït-title\n\ncommït-body",
"#", # git config --get core.commentchar
"commit-1-branch-1\ncommit-1-branch-2\n",
"file1.txt\npåth/to/file2.txt\n"
]
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
@ -89,21 +79,21 @@ class CLITests(BaseTestCase):
"25053ccec5e28e1bb8f7551fdbb5ab213ada2401\n" +
"4da2656b0dadc76c7ee3fd0243a96cb64007f125\n",
# git log --pretty <FORMAT> <SHA>
u"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
u"commït-title1\n\ncommït-body1",
u"#", # git config --get core.commentchar
u"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
u"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
"commït-title1\n\ncommït-body1",
"#", # git config --get core.commentchar
"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
# git log --pretty <FORMAT> <SHA>
u"test åuthor2\x00test-email3@föo.com\x002016-12-04 15:28:15 +0100\x00åbc\n"
u"commït-title2\n\ncommït-body2",
u"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
u"commit-2/file-1\ncommit-2/file-2\n", # git diff-tree
"test åuthor2\x00test-email3@föo.com\x002016-12-04 15:28:15 +0100\x00åbc\n"
"commït-title2\n\ncommït-body2",
"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
"commit-2/file-1\ncommit-2/file-2\n", # git diff-tree
# git log --pretty <FORMAT> <SHA>
u"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00åbc\n"
u"commït-title3\n\ncommït-body3",
u"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
u"commit-3/file-1\ncommit-3/file-2\n", # git diff-tree
"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00åbc\n"
"commït-title3\n\ncommït-body3",
"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
"commit-3/file-1\ncommit-3/file-2\n", # git diff-tree
]
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
@ -122,21 +112,21 @@ class CLITests(BaseTestCase):
"25053ccec5e28e1bb8f7551fdbb5ab213ada2401\n" +
"4da2656b0dadc76c7ee3fd0243a96cb64007f125\n",
# git log --pretty <FORMAT> <SHA>
u"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
u"commït-title1\n\ncommït-body1",
u"#", # git config --get core.commentchar
u"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
u"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
"commït-title1\n\ncommït-body1",
"#", # git config --get core.commentchar
"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
# git log --pretty <FORMAT> <SHA>
u"test åuthor2\x00test-email2@föo.com\x002016-12-04 15:28:15 +0100\x00åbc\n"
u"commït-title2.\n\ncommït-body2\ngitlint-ignore: T3\n",
u"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
u"commit-2/file-1\ncommit-2/file-2\n", # git diff-tree
"test åuthor2\x00test-email2@föo.com\x002016-12-04 15:28:15 +0100\x00åbc\n"
"commït-title2.\n\ncommït-body2\ngitlint-ignore: T3\n",
"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
"commit-2/file-1\ncommit-2/file-2\n", # git diff-tree
# git log --pretty <FORMAT> <SHA>
u"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00åbc\n"
u"commït-title3.\n\ncommït-body3",
u"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
u"commit-3/file-1\ncommit-3/file-2\n", # git diff-tree
"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00åbc\n"
"commït-title3.\n\ncommït-body3",
"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
"commit-3/file-1\ncommit-3/file-2\n", # git diff-tree
]
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
@ -157,24 +147,24 @@ class CLITests(BaseTestCase):
"25053ccec5e28e1bb8f7551fdbb5ab213ada2401\n" +
"4da2656b0dadc76c7ee3fd0243a96cb64007f125\n",
# git log --pretty <FORMAT> <SHA>
u"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
u"commït-title1\n\ncommït-body1",
u"#", # git config --get core.commentchar
u"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
u"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
"commït-title1\n\ncommït-body1",
"#", # git config --get core.commentchar
"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
# git log --pretty <FORMAT> <SHA>
u"test åuthor2\x00test-email3@föo.com\x002016-12-04 15:28:15 +0100\x00åbc\n"
"test åuthor2\x00test-email3@föo.com\x002016-12-04 15:28:15 +0100\x00åbc\n"
# Normally T3 violation (trailing punctuation), but this commit is ignored because of
# config below
u"commït-title2.\n\ncommït-body2\n",
u"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
u"commit-2/file-1\ncommit-2/file-2\n", # git diff-tree
"commït-title2.\n\ncommït-body2\n",
"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
"commit-2/file-1\ncommit-2/file-2\n", # git diff-tree
# git log --pretty <FORMAT> <SHA>
u"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00åbc\n"
"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00åbc\n"
# Normally T1 and B5 violations, now only T1 because we're ignoring B5 in config below
u"commït-title3.\n\ncommït-body3 foo",
u"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
u"commit-3/file-1\ncommit-3/file-2\n", # git diff-tree
"commït-title3.\n\ncommït-body3 foo",
"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
"commit-3/file-1\ncommit-3/file-2\n", # git diff-tree
]
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
@ -183,9 +173,9 @@ class CLITests(BaseTestCase):
# We expect that the second commit has no failures because of it matching against I1.regex
# Because we do test for the 3th commit to return violations, this test also ensures that a unique
# config object is passed to each commit lint call
expected = (u"Commit 6f29bf81a8:\n"
expected = ("Commit 6f29bf81a8:\n"
u'3: B5 Body message is too short (12<20): "commït-body1"\n\n'
u"Commit 4da2656b0d:\n"
"Commit 4da2656b0d:\n"
u'1: T3 Title has trailing punctuation (.): "commït-title3."\n')
self.assertEqual(stderr.getvalue(), expected)
self.assertEqual(result.exit_code, 2)
@ -218,11 +208,11 @@ class CLITests(BaseTestCase):
""" Test for ignoring stdin when --ignore-stdin flag is enabled"""
sh.git.side_effect = [
"6f29bf81a8322a04071bb794666e48c443a90360",
u"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
u"commït-title\n\ncommït-body",
u"#", # git config --get core.commentchar
u"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
u"file1.txt\npåth/to/file2.txt\n" # git diff-tree
"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
"commït-title\n\ncommït-body",
"#", # git config --get core.commentchar
"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
"file1.txt\npåth/to/file2.txt\n" # git diff-tree
]
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
@ -240,11 +230,11 @@ class CLITests(BaseTestCase):
""" Test for ignoring stdin when --ignore-stdin flag is enabled"""
sh.git.side_effect = [
u"#", # git config --get core.commentchar
u"föo user\n", # git config --get user.name
u"föo@bar.com\n", # git config --get user.email
u"my-branch\n", # git rev-parse --abbrev-ref HEAD (=current branch)
u"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
"#", # git config --get core.commentchar
"föo user\n", # git config --get user.name
"föo@bar.com\n", # git config --get user.email
"my-branch\n", # git rev-parse --abbrev-ref HEAD (=current branch)
"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
]
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
@ -263,17 +253,17 @@ class CLITests(BaseTestCase):
""" Test for ignoring stdin when --ignore-stdin flag is enabled"""
sh.git.side_effect = [
u"#", # git config --get core.commentchar
u"föo user\n", # git config --get user.name
u"föo@bar.com\n", # git config --get user.email
u"my-branch\n", # git rev-parse --abbrev-ref HEAD (=current branch)
u"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
"#", # git config --get core.commentchar
"föo user\n", # git config --get user.name
"föo@bar.com\n", # git config --get user.email
"my-branch\n", # git rev-parse --abbrev-ref HEAD (=current branch)
"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
]
with self.tempdir() as tmpdir:
msg_filename = os.path.join(tmpdir, "msg")
with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
f.write(u"WIP: msg-filename tïtle\n")
f.write("WIP: msg-filename tïtle\n")
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--debug", "--staged", "--msg-filename", msg_filename])
@ -289,17 +279,17 @@ class CLITests(BaseTestCase):
def test_lint_staged_negative(self, _):
result = self.cli.invoke(cli.cli, ["--staged"])
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
self.assertEqual(result.output, (u"Error: The 'staged' option (--staged) can only be used when using "
u"'--msg-filename' or when piping data to gitlint via stdin.\n"))
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)
def test_msg_filename(self, _):
expected_output = u"3: B6 Body message is missing\n"
expected_output = "3: B6 Body message is missing\n"
with self.tempdir() as tmpdir:
msg_filename = os.path.join(tmpdir, "msg")
with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
f.write(u"Commït title\n")
f.write("Commït title\n")
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename])
@ -307,7 +297,7 @@ class CLITests(BaseTestCase):
self.assertEqual(result.exit_code, 1)
self.assertEqual(result.output, "")
@patch('gitlint.cli.get_stdin_data', return_value=u"WIP: tïtle \n")
@patch('gitlint.cli.get_stdin_data', return_value="WIP: tïtle \n")
def test_silent_mode(self, _):
""" Test for --silent option """
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
@ -316,7 +306,7 @@ class CLITests(BaseTestCase):
self.assertEqual(result.exit_code, 3)
self.assertEqual(result.output, "")
@patch('gitlint.cli.get_stdin_data', return_value=u"WIP: tïtle \n")
@patch('gitlint.cli.get_stdin_data', return_value="WIP: tïtle \n")
def test_verbosity(self, _):
""" Test for --verbosity option """
# We only test -v and -vv, more testing is really not required here
@ -333,7 +323,7 @@ class CLITests(BaseTestCase):
"3: B6 Body message is missing\n"
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["-vv"], input=u"WIP: tïtle \n")
result = self.cli.invoke(cli.cli, ["-vv"], input="WIP: tïtle \n")
self.assertEqual(stderr.getvalue(), expected_output)
self.assertEqual(result.exit_code, 3)
self.assertEqual(result.output, "")
@ -355,19 +345,19 @@ class CLITests(BaseTestCase):
"25053ccec5e28e1bb8f7551fdbb5ab213ada2401\n"
"4da2656b0dadc76c7ee3fd0243a96cb64007f125\n",
# git log --pretty <FORMAT> <SHA>
u"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00abc\n"
u"commït-title1\n\ncommït-body1",
u"#", # git config --get core.commentchar
u"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
u"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
u"test åuthor2\x00test-email2@föo.com\x002016-12-04 15:28:15 +0100\x00abc\n"
u"commït-title2.\n\ncommït-body2",
u"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
u"commit-2/file-1\ncommit-2/file-2\n", # git diff-tree
u"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00abc\n"
u"föobar\nbar",
u"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
u"commit-3/file-1\ncommit-3/file-2\n", # git diff-tree
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00abc\n"
"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
"test åuthor2\x00test-email2@föo.com\x002016-12-04 15:28:15 +0100\x00abc\n"
"commït-title2.\n\ncommït-body2",
"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
"commit-2/file-1\ncommit-2/file-2\n", # git diff-tree
"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00abc\n"
"föobar\nbar",
"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
"commit-3/file-1\ncommit-3/file-2\n", # git diff-tree
]
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
@ -387,14 +377,14 @@ class CLITests(BaseTestCase):
expected_logs = self.get_expected('cli/test_cli/test_debug_1', expected_kwargs)
self.assert_logged(expected_logs)
@patch('gitlint.cli.get_stdin_data', return_value=u"Test tïtle\n")
@patch('gitlint.cli.get_stdin_data', return_value="Test tïtle\n")
def test_extra_path(self, _):
""" Test for --extra-path flag """
# Test extra-path pointing to a directory
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
extra_path = self.get_sample_path("user_rules")
result = self.cli.invoke(cli.cli, ["--extra-path", extra_path])
expected_output = u"1: UC1 Commit violåtion 1: \"Contënt 1\"\n" + \
expected_output = "1: UC1 Commit violåtion 1: \"Contënt 1\"\n" + \
"3: B6 Body message is missing\n"
self.assertEqual(stderr.getvalue(), expected_output)
self.assertEqual(result.exit_code, 2)
@ -403,12 +393,12 @@ class CLITests(BaseTestCase):
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
extra_path = self.get_sample_path(os.path.join("user_rules", "my_commit_rules.py"))
result = self.cli.invoke(cli.cli, ["--extra-path", extra_path])
expected_output = u"1: UC1 Commit violåtion 1: \"Contënt 1\"\n" + \
expected_output = "1: UC1 Commit violåtion 1: \"Contënt 1\"\n" + \
"3: B6 Body message is missing\n"
self.assertEqual(stderr.getvalue(), expected_output)
self.assertEqual(result.exit_code, 2)
@patch('gitlint.cli.get_stdin_data', return_value=u"Test tïtle\n\nMy body that is long enough")
@patch('gitlint.cli.get_stdin_data', return_value="Test tïtle\n\nMy body that is long enough")
def test_contrib(self, _):
# Test enabled contrib rules
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
@ -417,13 +407,13 @@ class CLITests(BaseTestCase):
self.assertEqual(stderr.getvalue(), expected_output)
self.assertEqual(result.exit_code, 3)
@patch('gitlint.cli.get_stdin_data', return_value=u"Test tïtle\n")
@patch('gitlint.cli.get_stdin_data', return_value="Test tïtle\n")
def test_contrib_negative(self, _):
result = self.cli.invoke(cli.cli, ["--contrib", u"föobar,CC1"])
self.assertEqual(result.output, u"Config Error: No contrib rule with id or name 'föobar' found.\n")
result = self.cli.invoke(cli.cli, ["--contrib", "föobar,CC1"])
self.assertEqual(result.output, "Config Error: No contrib rule with id or name 'föobar' found.\n")
self.assertEqual(result.exit_code, self.CONFIG_ERROR_CODE)
@patch('gitlint.cli.get_stdin_data', return_value=u"WIP: tëst")
@patch('gitlint.cli.get_stdin_data', return_value="WIP: tëst")
def test_config_file(self, _):
""" Test for --config option """
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
@ -438,16 +428,14 @@ class CLITests(BaseTestCase):
# Directory as config file
config_path = self.get_sample_path("config")
result = self.cli.invoke(cli.cli, ["--config", config_path])
expected_string = u"Error: Invalid value for \"-C\" / \"--config\": File \"{0}\" is a directory.".format(
config_path)
expected_string = f"Error: Invalid value for '-C' / '--config': File '{config_path}' is a directory."
self.assertEqual(result.output.split("\n")[3], expected_string)
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
# Non existing file
config_path = self.get_sample_path(u"föo")
config_path = self.get_sample_path("föo")
result = self.cli.invoke(cli.cli, ["--config", config_path])
expected_string = u"Error: Invalid value for \"-C\" / \"--config\": File \"{0}\" does not exist.".format(
config_path)
expected_string = f"Error: Invalid value for '-C' / '--config': File '{config_path}' does not exist."
self.assertEqual(result.output.split("\n")[3], expected_string)
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
@ -471,37 +459,37 @@ class CLITests(BaseTestCase):
def test_target_negative(self):
""" Negative test for the --target option """
# try setting a non-existing target
result = self.cli.invoke(cli.cli, ["--target", u"/föo/bar"])
result = self.cli.invoke(cli.cli, ["--target", "/föo/bar"])
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
expected_msg = u"Error: Invalid value for \"--target\": Directory \"/föo/bar\" does not exist."
expected_msg = "Error: Invalid value for '--target': Directory '/föo/bar' does not exist."
self.assertEqual(result.output.split("\n")[3], expected_msg)
# try setting a file as target
target_path = self.get_sample_path(os.path.join("config", "gitlintconfig"))
result = self.cli.invoke(cli.cli, ["--target", target_path])
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
expected_msg = u"Error: Invalid value for \"--target\": Directory \"{0}\" is a file.".format(target_path)
expected_msg = f"Error: Invalid value for '--target': Directory '{target_path}' is a file."
self.assertEqual(result.output.split("\n")[3], expected_msg)
@patch('gitlint.config.LintConfigGenerator.generate_config')
def test_generate_config(self, generate_config):
""" Test for the generate-config subcommand """
result = self.cli.invoke(cli.cli, ["generate-config"], input=u"tëstfile\n")
result = self.cli.invoke(cli.cli, ["generate-config"], input="tëstfile\n")
self.assertEqual(result.exit_code, 0)
expected_msg = u"Please specify a location for the sample gitlint config file [.gitlint]: tëstfile\n" + \
u"Successfully generated {0}\n".format(os.path.realpath(u"tëstfile"))
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)
generate_config.assert_called_once_with(os.path.realpath(u"tëstfile"))
generate_config.assert_called_once_with(os.path.realpath("tëstfile"))
def test_generate_config_negative(self):
""" Negative test for the generate-config subcommand """
# Non-existing directory
fake_dir = os.path.abspath(u"/föo")
fake_path = os.path.join(fake_dir, u"bar")
fake_dir = os.path.abspath("/föo")
fake_path = os.path.join(fake_dir, "bar")
result = self.cli.invoke(cli.cli, ["generate-config"], input=fake_path)
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
expected_msg = (u"Please specify a location for the sample gitlint config file [.gitlint]: {0}\n"
+ u"Error: Directory '{1}' does not exist.\n").format(fake_path, fake_dir)
expected_msg = f"Please specify a location for the sample gitlint config file [.gitlint]: {fake_path}\n" + \
f"Error: Directory '{fake_dir}' does not exist.\n"
self.assertEqual(result.output, expected_msg)
# Existing file
@ -509,8 +497,8 @@ class CLITests(BaseTestCase):
result = self.cli.invoke(cli.cli, ["generate-config"], input=sample_path)
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
expected_msg = "Please specify a location for the sample gitlint " + \
"config file [.gitlint]: {0}\n".format(sample_path) + \
"Error: File \"{0}\" already exists.\n".format(sample_path)
f"config file [.gitlint]: {sample_path}\n" + \
f"Error: File \"{sample_path}\" already exists.\n"
self.assertEqual(result.output, expected_msg)
@patch('gitlint.cli.get_stdin_data', return_value=False)
@ -528,10 +516,10 @@ class CLITests(BaseTestCase):
sh.git.side_effect = lambda *_args, **_kwargs: ""
result = self.cli.invoke(cli.cli, ["--commits", "master...HEAD"])
self.assert_log_contains(u"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)
@patch('gitlint.cli.get_stdin_data', return_value=u"WIP: tëst tïtle")
@patch('gitlint.cli.get_stdin_data', return_value="WIP: tëst tïtle")
def test_named_rules(self, _):
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
config_path = self.get_sample_path(os.path.join("config", "named-rules"))

View file

@ -1,28 +1,18 @@
# -*- coding: utf-8 -*-
import io
from io import StringIO
import os
from click.testing import CliRunner
try:
# python 2.x
from StringIO import StringIO
except ImportError:
# python 3.x
from io import StringIO # pylint: disable=ungrouped-imports
try:
# python 2.x
from mock import patch
except ImportError:
# python 3.x
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
from unittest.mock import patch
from gitlint.tests.base import BaseTestCase
from gitlint import cli
from gitlint import hooks
from gitlint import config
from gitlint.shell import ErrorReturnCode
from gitlint.utils import DEFAULT_ENCODING
@ -45,12 +35,12 @@ class CLIHookTests(BaseTestCase):
self.git_version_path.stop()
@patch('gitlint.hooks.GitHookInstaller.install_commit_msg_hook')
@patch('gitlint.hooks.git_hooks_dir', return_value=os.path.join(u"/hür", u"dur"))
@patch('gitlint.hooks.git_hooks_dir', return_value=os.path.join("/hür", "dur"))
def test_install_hook(self, _, install_hook):
""" Test for install-hook subcommand """
result = self.cli.invoke(cli.cli, ["install-hook"])
expected_path = os.path.join(u"/hür", u"dur", hooks.COMMIT_MSG_HOOK_DST_PATH)
expected = u"Successfully installed gitlint commit-msg hook in {0}\n".format(expected_path)
expected_path = os.path.join("/hür", "dur", hooks.COMMIT_MSG_HOOK_DST_PATH)
expected = f"Successfully installed gitlint commit-msg hook in {expected_path}\n"
self.assertEqual(result.output, expected)
self.assertEqual(result.exit_code, 0)
expected_config = config.LintConfig()
@ -58,12 +48,12 @@ class CLIHookTests(BaseTestCase):
install_hook.assert_called_once_with(expected_config)
@patch('gitlint.hooks.GitHookInstaller.install_commit_msg_hook')
@patch('gitlint.hooks.git_hooks_dir', return_value=os.path.join(u"/hür", u"dur"))
@patch('gitlint.hooks.git_hooks_dir', return_value=os.path.join("/hür", "dur"))
def test_install_hook_target(self, _, install_hook):
""" Test for install-hook subcommand with a specific --target option specified """
# Specified target
result = self.cli.invoke(cli.cli, ["--target", self.SAMPLES_DIR, "install-hook"])
expected_path = os.path.join(u"/hür", u"dur", hooks.COMMIT_MSG_HOOK_DST_PATH)
expected_path = os.path.join("/hür", "dur", hooks.COMMIT_MSG_HOOK_DST_PATH)
expected = "Successfully installed gitlint commit-msg hook in %s\n" % expected_path
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.output, expected)
@ -72,40 +62,40 @@ class CLIHookTests(BaseTestCase):
expected_config.target = self.SAMPLES_DIR
install_hook.assert_called_once_with(expected_config)
@patch('gitlint.hooks.GitHookInstaller.install_commit_msg_hook', side_effect=hooks.GitHookInstallerError(u"tëst"))
@patch('gitlint.hooks.GitHookInstaller.install_commit_msg_hook', side_effect=hooks.GitHookInstallerError("tëst"))
def test_install_hook_negative(self, install_hook):
""" Negative test for install-hook subcommand """
result = self.cli.invoke(cli.cli, ["install-hook"])
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
self.assertEqual(result.output, u"tëst\n")
self.assertEqual(result.output, "tëst\n")
expected_config = config.LintConfig()
expected_config.target = os.path.realpath(os.getcwd())
install_hook.assert_called_once_with(expected_config)
@patch('gitlint.hooks.GitHookInstaller.uninstall_commit_msg_hook')
@patch('gitlint.hooks.git_hooks_dir', return_value=os.path.join(u"/hür", u"dur"))
@patch('gitlint.hooks.git_hooks_dir', return_value=os.path.join("/hür", "dur"))
def test_uninstall_hook(self, _, uninstall_hook):
""" Test for uninstall-hook subcommand """
result = self.cli.invoke(cli.cli, ["uninstall-hook"])
expected_path = os.path.join(u"/hür", u"dur", hooks.COMMIT_MSG_HOOK_DST_PATH)
expected = u"Successfully uninstalled gitlint commit-msg hook from {0}\n".format(expected_path)
expected_path = os.path.join("/hür", "dur", hooks.COMMIT_MSG_HOOK_DST_PATH)
expected = f"Successfully uninstalled gitlint commit-msg hook from {expected_path}\n"
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.output, expected)
expected_config = config.LintConfig()
expected_config.target = os.path.realpath(os.getcwd())
uninstall_hook.assert_called_once_with(expected_config)
@patch('gitlint.hooks.GitHookInstaller.uninstall_commit_msg_hook', side_effect=hooks.GitHookInstallerError(u"tëst"))
@patch('gitlint.hooks.GitHookInstaller.uninstall_commit_msg_hook', side_effect=hooks.GitHookInstallerError("tëst"))
def test_uninstall_hook_negative(self, uninstall_hook):
""" Negative test for uninstall-hook subcommand """
result = self.cli.invoke(cli.cli, ["uninstall-hook"])
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
self.assertEqual(result.output, u"tëst\n")
self.assertEqual(result.output, "tëst\n")
expected_config = config.LintConfig()
expected_config.target = os.path.realpath(os.getcwd())
uninstall_hook.assert_called_once_with(expected_config)
def test_hook_no_tty(self):
def test_run_hook_no_tty(self):
""" Test for run-hook subcommand.
When no TTY is available (like is the case for this test), the hook will abort after the first check.
"""
@ -119,9 +109,9 @@ class CLIHookTests(BaseTestCase):
# check the output which indirectly proves the same thing.
with self.tempdir() as tmpdir:
msg_filename = os.path.join(tmpdir, u"hür")
msg_filename = os.path.join(tmpdir, "hür")
with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
f.write(u"WIP: tïtle\n")
f.write("WIP: tïtle\n")
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"])
@ -132,12 +122,12 @@ class CLIHookTests(BaseTestCase):
self.assertEqual(result.exit_code, 1)
@patch('gitlint.cli.shell')
def test_hook_edit(self, shell):
def test_run_hook_edit(self, shell):
""" Test for run-hook subcommand, answering 'e(dit)' after commit-hook """
set_editors = [None, u"myeditor"]
expected_editors = [u"vim -n", u"myeditor"]
commit_messages = [u"WIP: höok edit 1", u"WIP: höok edit 2"]
set_editors = [None, "myeditor"]
expected_editors = ["vim -n", "myeditor"]
commit_messages = ["WIP: höok edit 1", "WIP: höok edit 2"]
for i in range(0, len(set_editors)):
if set_editors[i]:
@ -145,7 +135,7 @@ class CLIHookTests(BaseTestCase):
with self.patch_input(['e', 'e', 'n']):
with self.tempdir() as tmpdir:
msg_filename = os.path.realpath(os.path.join(tmpdir, u"hür"))
msg_filename = os.path.realpath(os.path.join(tmpdir, "hür"))
with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
f.write(commit_messages[i] + "\n")
@ -161,18 +151,17 @@ class CLIHookTests(BaseTestCase):
self.assertEqual(result.exit_code, 2)
shell.assert_called_with(expected_editors[i] + " " + msg_filename)
self.assert_log_contains(u"DEBUG: gitlint.cli run-hook: editing commit message")
self.assert_log_contains(u"DEBUG: gitlint.cli run-hook: {0} {1}".format(expected_editors[i],
msg_filename))
self.assert_log_contains("DEBUG: gitlint.cli run-hook: editing commit message")
self.assert_log_contains(f"DEBUG: gitlint.cli run-hook: {expected_editors[i]} {msg_filename}")
def test_hook_no(self):
def test_run_hook_no(self):
""" Test for run-hook subcommand, answering 'n(o)' after commit-hook """
with self.patch_input(['n']):
with self.tempdir() as tmpdir:
msg_filename = os.path.join(tmpdir, u"hür")
msg_filename = os.path.join(tmpdir, "hür")
with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
f.write(u"WIP: höok no\n")
f.write("WIP: höok no\n")
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"])
@ -184,13 +173,13 @@ class CLIHookTests(BaseTestCase):
self.assertEqual(result.exit_code, 2)
self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message declined")
def test_hook_yes(self):
def test_run_hook_yes(self):
""" Test for run-hook subcommand, answering 'y(es)' after commit-hook """
with self.patch_input(['y']):
with self.tempdir() as tmpdir:
msg_filename = os.path.join(tmpdir, u"hür")
msg_filename = os.path.join(tmpdir, "hür")
with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
f.write(u"WIP: höok yes\n")
f.write("WIP: höok yes\n")
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"])
@ -202,8 +191,32 @@ class CLIHookTests(BaseTestCase):
self.assertEqual(result.exit_code, 0)
self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message accepted")
@patch('gitlint.cli.get_stdin_data', return_value=u"WIP: Test hook stdin tïtle\n")
def test_hook_stdin_violations(self, _):
@patch('gitlint.cli.get_stdin_data', return_value=False)
@patch('gitlint.git.sh')
def test_run_hook_negative(self, sh, _):
""" Negative test for the run-hook subcommand: testing whether exceptions are correctly handled when
running `gitlint run-hook`.
"""
# GIT_CONTEXT_ERROR_CODE: git error
error_msg = b"fatal: not a git repository (or any of the parent directories): .git"
sh.git.side_effect = ErrorReturnCode("full command", b"stdout", error_msg)
result = self.cli.invoke(cli.cli, ["run-hook"])
expected = self.get_expected('cli/test_cli_hooks/test_run_hook_negative_1', {'git_repo': os.getcwd()})
self.assertEqual(result.output, expected)
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
# USAGE_ERROR_CODE: incorrect use of gitlint
result = self.cli.invoke(cli.cli, ["--staged", "run-hook"])
self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_run_hook_negative_2'))
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
# CONFIG_ERROR_CODE: incorrect config. Note that this is handled before the hook even runs
result = self.cli.invoke(cli.cli, ["-c", "föo.bár=1", "run-hook"])
self.assertEqual(result.output, "Config Error: No such rule 'föo'\n")
self.assertEqual(result.exit_code, self.CONFIG_ERROR_CODE)
@patch('gitlint.cli.get_stdin_data', return_value="WIP: Test hook stdin tïtle\n")
def test_run_hook_stdin_violations(self, _):
""" Test for passing stdin data to run-hook, expecting some violations. Equivalent of:
$ echo "WIP: Test hook stdin tïtle" | gitlint run-hook
"""
@ -216,8 +229,8 @@ class CLIHookTests(BaseTestCase):
# Hook will auto-abort because we're using stdin. Abort = exit code 1
self.assertEqual(result.exit_code, 1)
@patch('gitlint.cli.get_stdin_data', return_value=u"Test tïtle\n\nTest bödy that is long enough")
def test_hook_stdin_no_violations(self, _):
@patch('gitlint.cli.get_stdin_data', return_value="Test tïtle\n\nTest bödy that is long enough")
def test_run_hook_stdin_no_violations(self, _):
""" Test for passing stdin data to run-hook, expecting *NO* violations, Equivalent of:
$ echo -e "Test tïtle\n\nTest bödy that is long enough" | gitlint run-hook
"""
@ -229,8 +242,8 @@ class CLIHookTests(BaseTestCase):
self.assertEqual(result.output, expected_stdout)
self.assertEqual(result.exit_code, 0)
@patch('gitlint.cli.get_stdin_data', return_value=u"WIP: Test hook config tïtle\n")
def test_hook_config(self, _):
@patch('gitlint.cli.get_stdin_data', return_value="WIP: Test hook config tïtle\n")
def test_run_hook_config(self, _):
""" Test that gitlint still respects config when running run-hook, equivalent of:
$ echo "WIP: Test hook config tïtle" | gitlint -c title-max-length.line-length=5 --ignore B6 run-hook
"""
@ -244,18 +257,18 @@ class CLIHookTests(BaseTestCase):
@patch('gitlint.cli.get_stdin_data', return_value=False)
@patch('gitlint.git.sh')
def test_hook_local_commit(self, sh, _):
def test_run_hook_local_commit(self, sh, _):
""" Test running the hook on the last commit-msg from the local repo, equivalent of:
$ gitlint run-hook
and then choosing 'e'
"""
sh.git.side_effect = [
"6f29bf81a8322a04071bb794666e48c443a90360",
u"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
u"WIP: commït-title\n\ncommït-body",
u"#", # git config --get core.commentchar
u"commit-1-branch-1\ncommit-1-branch-2\n",
u"file1.txt\npåth/to/file2.txt\n"
"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
"WIP: commït-title\n\ncommït-body",
"#", # git config --get core.commentchar
"commit-1-branch-1\ncommit-1-branch-2\n",
"file1.txt\npåth/to/file2.txt\n"
]
with self.patch_input(['e']):

View file

@ -1,16 +1,11 @@
# -*- coding: utf-8 -*-
try:
# python 2.x
from mock import patch
except ImportError:
# python 3.x
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
from unittest.mock import patch
from gitlint import rules
from gitlint.config import LintConfig, LintConfigError, LintConfigGenerator, GITLINT_CONFIG_TEMPLATE_SRC_PATH
from gitlint import options
from gitlint.tests.base import BaseTestCase, ustr
from gitlint.tests.base import BaseTestCase
class LintConfigTests(BaseTestCase):
@ -29,20 +24,20 @@ class LintConfigTests(BaseTestCase):
config = LintConfig()
# non-existing rule
expected_error_msg = u"No such rule 'föobar'"
expected_error_msg = "No such rule 'föobar'"
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
config.set_rule_option(u'föobar', u'lïne-length', 60)
# non-existing option
expected_error_msg = u"Rule 'title-max-length' has no option 'föobar'"
expected_error_msg = "Rule 'title-max-length' has no option 'föobar'"
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
config.set_rule_option('title-max-length', u'föobar', 60)
# invalid option value
expected_error_msg = u"'föo' is not a valid value for option 'title-max-length.line-length'. " + \
u"Option 'line-length' must be a positive integer (current value: 'föo')."
expected_error_msg = "'föo' is not a valid value for option 'title-max-length.line-length'. " + \
"Option 'line-length' must be a positive integer (current value: 'föo')."
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
config.set_rule_option('title-max-length', 'line-length', u"föo")
config.set_rule_option('title-max-length', 'line-length', "föo")
def test_set_general_option(self):
config = LintConfig()
@ -117,7 +112,7 @@ class LintConfigTests(BaseTestCase):
actual_rule = config.rules.find_rule("contrib-title-conventional-commits")
self.assertTrue(actual_rule.is_contrib)
self.assertEqual(ustr(type(actual_rule)), "<class 'conventional_commit.ConventionalCommit'>")
self.assertEqual(str(type(actual_rule)), "<class 'conventional_commit.ConventionalCommit'>")
self.assertEqual(actual_rule.id, 'CT1')
self.assertEqual(actual_rule.name, u'contrib-title-conventional-commits')
self.assertEqual(actual_rule.target, rules.CommitMessageTitle)
@ -135,7 +130,7 @@ class LintConfigTests(BaseTestCase):
actual_rule = config.rules.find_rule("contrib-body-requires-signed-off-by")
self.assertTrue(actual_rule.is_contrib)
self.assertEqual(ustr(type(actual_rule)), "<class 'signedoff_by.SignedOffBy'>")
self.assertEqual(str(type(actual_rule)), "<class 'signedoff_by.SignedOffBy'>")
self.assertEqual(actual_rule.id, 'CC1')
self.assertEqual(actual_rule.name, u'contrib-body-requires-signed-off-by')
@ -151,15 +146,15 @@ class LintConfigTests(BaseTestCase):
def test_contrib_negative(self):
config = LintConfig()
# non-existent contrib rule
with self.assertRaisesMessage(LintConfigError, u"No contrib rule with id or name 'föo' found."):
config.contrib = u"contrib-title-conventional-commits,föo"
with self.assertRaisesMessage(LintConfigError, "No contrib rule with id or name 'föo' found."):
config.contrib = "contrib-title-conventional-commits,föo"
# UserRuleError, RuleOptionError should be re-raised as LintConfigErrors
side_effects = [rules.UserRuleError(u"üser-rule"), options.RuleOptionError(u"rüle-option")]
side_effects = [rules.UserRuleError("üser-rule"), options.RuleOptionError("rüle-option")]
for side_effect in side_effects:
with patch('gitlint.config.rule_finder.find_rule_classes', side_effect=side_effect):
with self.assertRaisesMessage(LintConfigError, ustr(side_effect)):
config.contrib = u"contrib-title-conventional-commits"
with self.assertRaisesMessage(LintConfigError, str(side_effect)):
config.contrib = "contrib-title-conventional-commits"
def test_extra_path(self):
config = LintConfig()
@ -168,11 +163,11 @@ class LintConfigTests(BaseTestCase):
self.assertEqual(config.extra_path, self.get_user_rules_path())
actual_rule = config.rules.find_rule('UC1')
self.assertTrue(actual_rule.is_user_defined)
self.assertEqual(ustr(type(actual_rule)), "<class 'my_commit_rules.MyUserCommitRule'>")
self.assertEqual(str(type(actual_rule)), "<class 'my_commit_rules.MyUserCommitRule'>")
self.assertEqual(actual_rule.id, 'UC1')
self.assertEqual(actual_rule.name, u'my-üser-commit-rule')
self.assertEqual(actual_rule.target, None)
expected_rule_option = options.IntOption('violation-count', 1, u"Number of violåtions to return")
expected_rule_option = options.IntOption('violation-count', 1, "Number of violåtions to return")
self.assertListEqual(actual_rule.options_spec, [expected_rule_option])
self.assertDictEqual(actual_rule.options, {'violation-count': expected_rule_option})
@ -183,10 +178,10 @@ class LintConfigTests(BaseTestCase):
def test_extra_path_negative(self):
config = LintConfig()
regex = u"Option extra-path must be either an existing directory or file (current value: 'föo/bar')"
regex = "Option extra-path must be either an existing directory or file (current value: 'föo/bar')"
# incorrect extra_path
with self.assertRaisesMessage(LintConfigError, regex):
config.extra_path = u"föo/bar"
config.extra_path = "föo/bar"
# extra path contains classes with errors
with self.assertRaisesMessage(LintConfigError,
@ -198,17 +193,17 @@ class LintConfigTests(BaseTestCase):
# Note that we shouldn't test whether we can set unicode because python just doesn't allow unicode attributes
with self.assertRaisesMessage(LintConfigError, "'foo' is not a valid gitlint option"):
config.set_general_option("foo", u"bår")
config.set_general_option("foo", "bår")
# try setting _config_path, this is a real attribute of LintConfig, but the code should prevent it from
# being set
with self.assertRaisesMessage(LintConfigError, "'_config_path' is not a valid gitlint option"):
config.set_general_option("_config_path", u"bår")
config.set_general_option("_config_path", "bår")
# invalid verbosity
incorrect_values = [-1, u"föo"]
incorrect_values = [-1, "föo"]
for value in incorrect_values:
expected_msg = u"Option 'verbosity' must be a positive integer (current value: '{0}')".format(value)
expected_msg = f"Option 'verbosity' must be a positive integer (current value: '{value}')"
with self.assertRaisesMessage(LintConfigError, expected_msg):
config.verbosity = value
@ -220,12 +215,12 @@ class LintConfigTests(BaseTestCase):
# invalid ignore_xxx_commits
ignore_attributes = ["ignore_merge_commits", "ignore_fixup_commits", "ignore_squash_commits",
"ignore_revert_commits"]
incorrect_values = [-1, 4, u"föo"]
incorrect_values = [-1, 4, "föo"]
for attribute in ignore_attributes:
for value in incorrect_values:
option_name = attribute.replace("_", "-")
with self.assertRaisesMessage(LintConfigError,
"Option '{0}' must be either 'true' or 'false'".format(option_name)):
f"Option '{option_name}' must be either 'true' or 'false'"):
setattr(config, attribute, value)
# invalid ignore -> not here because ignore is a ListOption which converts everything to a string before
@ -235,15 +230,15 @@ class LintConfigTests(BaseTestCase):
for attribute in ['debug', 'staged', 'ignore_stdin']:
option_name = attribute.replace("_", "-")
with self.assertRaisesMessage(LintConfigError,
"Option '{0}' must be either 'true' or 'false'".format(option_name)):
setattr(config, attribute, u"föobar")
f"Option '{option_name}' must be either 'true' or 'false'"):
setattr(config, attribute, "föobar")
# extra-path has its own negative test
# invalid target
with self.assertRaisesMessage(LintConfigError,
u"Option target must be an existing directory (current value: 'föo/bar')"):
config.target = u"föo/bar"
"Option target must be an existing directory (current value: 'föo/bar')"):
config.target = "föo/bar"
def test_ignore_independent_from_rules(self):
# Test that the lintconfig rules are not modified when setting config.ignore
@ -273,9 +268,9 @@ class LintConfigTests(BaseTestCase):
# Other attributes don't matter
config1 = LintConfig()
config2 = LintConfig()
config1.foo = u"bår"
config1.foo = "bår"
self.assertEqual(config1, config2)
config2.foo = u"dūr"
config2.foo = "dūr"
self.assertEqual(config1, config2)
@ -283,5 +278,5 @@ class LintConfigGeneratorTests(BaseTestCase):
@staticmethod
@patch('gitlint.config.shutil.copyfile')
def test_install_commit_msg_hook_negative(copy):
LintConfigGenerator.generate_config(u"föo/bar/test")
copy.assert_called_with(GITLINT_CONFIG_TEMPLATE_SRC_PATH, u"föo/bar/test")
LintConfigGenerator.generate_config("föo/bar/test")
copy.assert_called_with(GITLINT_CONFIG_TEMPLATE_SRC_PATH, "föo/bar/test")

View file

@ -42,30 +42,30 @@ class LintConfigBuilderTests(BaseTestCase):
config_builder = LintConfigBuilder()
# nothing gitlint
config_builder.set_config_from_commit(self.gitcommit(u"tëst\ngitlint\nfoo"))
config_builder.set_config_from_commit(self.gitcommit("tëst\ngitlint\nfoo"))
config = config_builder.build()
self.assertSequenceEqual(config.rules, original_rules)
self.assertListEqual(config.ignore, [])
# ignore all rules
config_builder.set_config_from_commit(self.gitcommit(u"tëst\ngitlint-ignore: all\nfoo"))
config_builder.set_config_from_commit(self.gitcommit("tëst\ngitlint-ignore: all\nfoo"))
config = config_builder.build()
self.assertEqual(config.ignore, original_rule_ids)
# ignore all rules, no space
config_builder.set_config_from_commit(self.gitcommit(u"tëst\ngitlint-ignore:all\nfoo"))
config_builder.set_config_from_commit(self.gitcommit("tëst\ngitlint-ignore:all\nfoo"))
config = config_builder.build()
self.assertEqual(config.ignore, original_rule_ids)
# ignore all rules, more spacing
config_builder.set_config_from_commit(self.gitcommit(u"tëst\ngitlint-ignore: \t all\nfoo"))
config_builder.set_config_from_commit(self.gitcommit("tëst\ngitlint-ignore: \t all\nfoo"))
config = config_builder.build()
self.assertEqual(config.ignore, original_rule_ids)
def test_set_from_commit_ignore_specific(self):
# ignore specific rules
config_builder = LintConfigBuilder()
config_builder.set_config_from_commit(self.gitcommit(u"tëst\ngitlint-ignore: T1, body-hard-tab"))
config_builder.set_config_from_commit(self.gitcommit("tëst\ngitlint-ignore: T1, body-hard-tab"))
config = config_builder.build()
self.assertEqual(config.ignore, ["T1", "body-hard-tab"])
@ -89,14 +89,14 @@ class LintConfigBuilderTests(BaseTestCase):
config_builder = LintConfigBuilder()
# bad config file load
foo_path = self.get_sample_path(u"föo")
expected_error_msg = u"Invalid file path: {0}".format(foo_path)
foo_path = self.get_sample_path("föo")
expected_error_msg = f"Invalid file path: {foo_path}"
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
config_builder.set_from_config_file(foo_path)
# error during file parsing
path = self.get_sample_path("config/no-sections")
expected_error_msg = u"File contains no section headers."
expected_error_msg = "File contains no section headers."
# We only match the start of the message here, since the exact message can vary depending on platform
with self.assertRaisesRegex(LintConfigError, expected_error_msg):
config_builder.set_from_config_file(path)
@ -105,7 +105,7 @@ class LintConfigBuilderTests(BaseTestCase):
path = self.get_sample_path("config/nonexisting-rule")
config_builder = LintConfigBuilder()
config_builder.set_from_config_file(path)
expected_error_msg = u"No such rule 'föobar'"
expected_error_msg = "No such rule 'föobar'"
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
config_builder.build()
@ -113,7 +113,7 @@ class LintConfigBuilderTests(BaseTestCase):
path = self.get_sample_path("config/nonexisting-general-option")
config_builder = LintConfigBuilder()
config_builder.set_from_config_file(path)
expected_error_msg = u"'foo' is not a valid gitlint option"
expected_error_msg = "'foo' is not a valid gitlint option"
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
config_builder.build()
@ -121,7 +121,7 @@ class LintConfigBuilderTests(BaseTestCase):
path = self.get_sample_path("config/nonexisting-option")
config_builder = LintConfigBuilder()
config_builder.set_from_config_file(path)
expected_error_msg = u"Rule 'title-max-length' has no option 'föobar'"
expected_error_msg = "Rule 'title-max-length' has no option 'föobar'"
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
config_builder.build()
@ -129,8 +129,8 @@ class LintConfigBuilderTests(BaseTestCase):
path = self.get_sample_path("config/invalid-option-value")
config_builder = LintConfigBuilder()
config_builder.set_from_config_file(path)
expected_error_msg = u"'föo' is not a valid value for option 'title-max-length.line-length'. " + \
u"Option 'line-length' must be a positive integer (current value: 'föo')."
expected_error_msg = "'föo' is not a valid value for option 'title-max-length.line-length'. " + \
"Option 'line-length' must be a positive integer (current value: 'föo')."
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
config_builder.build()
@ -141,39 +141,39 @@ class LintConfigBuilderTests(BaseTestCase):
config_builder = LintConfigBuilder()
config_builder.set_config_from_string_list(['general.verbosity=1', 'title-max-length.line-length=60',
'body-max-line-length.line-length=120',
u"title-must-not-contain-word.words=håha"])
"title-must-not-contain-word.words=håha"])
config = config_builder.build()
self.assertEqual(config.get_rule_option('title-max-length', 'line-length'), 60)
self.assertEqual(config.get_rule_option('body-max-line-length', 'line-length'), 120)
self.assertListEqual(config.get_rule_option('title-must-not-contain-word', 'words'), [u"håha"])
self.assertListEqual(config.get_rule_option('title-must-not-contain-word', 'words'), ["håha"])
self.assertEqual(config.verbosity, 1)
def test_set_config_from_string_list_negative(self):
config_builder = LintConfigBuilder()
# assert error on incorrect rule - this happens at build time
config_builder.set_config_from_string_list([u"föo.bar=1"])
with self.assertRaisesMessage(LintConfigError, u"No such rule 'föo'"):
config_builder.set_config_from_string_list(["föo.bar=1"])
with self.assertRaisesMessage(LintConfigError, "No such rule 'föo'"):
config_builder.build()
# no equal sign
expected_msg = u"'föo.bar' is an invalid configuration option. Use '<rule>.<option>=<value>'"
expected_msg = "'föo.bar' is an invalid configuration option. Use '<rule>.<option>=<value>'"
with self.assertRaisesMessage(LintConfigError, expected_msg):
config_builder.set_config_from_string_list([u"föo.bar"])
config_builder.set_config_from_string_list(["föo.bar"])
# missing value
expected_msg = u"'föo.bar=' is an invalid configuration option. Use '<rule>.<option>=<value>'"
expected_msg = "'föo.bar=' is an invalid configuration option. Use '<rule>.<option>=<value>'"
with self.assertRaisesMessage(LintConfigError, expected_msg):
config_builder.set_config_from_string_list([u"föo.bar="])
config_builder.set_config_from_string_list(["föo.bar="])
# space instead of equal sign
expected_msg = u"'föo.bar 1' is an invalid configuration option. Use '<rule>.<option>=<value>'"
expected_msg = "'föo.bar 1' is an invalid configuration option. Use '<rule>.<option>=<value>'"
with self.assertRaisesMessage(LintConfigError, expected_msg):
config_builder.set_config_from_string_list([u"föo.bar 1"])
config_builder.set_config_from_string_list(["föo.bar 1"])
# no period between rule and option names
expected_msg = u"'föobar=1' is an invalid configuration option. Use '<rule>.<option>=<value>'"
expected_msg = "'föobar=1' is an invalid configuration option. Use '<rule>.<option>=<value>'"
with self.assertRaisesMessage(LintConfigError, expected_msg):
config_builder.set_config_from_string_list([u'föobar=1'])
@ -216,15 +216,15 @@ class LintConfigBuilderTests(BaseTestCase):
# Add a named rule by setting an option in the config builder that follows the named rule pattern
# Assert that whitespace in the rule name is stripped
rule_qualifiers = [u'T7:my-extra-rüle', u' T7 : my-extra-rüle ', u'\tT7:\tmy-extra-rüle\t',
u'T7:\t\n \tmy-extra-rüle\t\n\n', u"title-match-regex:my-extra-rüle"]
u'T7:\t\n \tmy-extra-rüle\t\n\n', "title-match-regex:my-extra-rüle"]
for rule_qualifier in rule_qualifiers:
config_builder = LintConfigBuilder()
config_builder.set_option(rule_qualifier, 'regex', u"föo")
config_builder.set_option(rule_qualifier, 'regex', "föo")
expected_rules = copy.deepcopy(default_rules)
my_rule = rules.TitleRegexMatches({'regex': u"föo"})
my_rule.id = rules.TitleRegexMatches.id + u":my-extra-rüle"
my_rule.name = rules.TitleRegexMatches.name + u":my-extra-rüle"
my_rule = rules.TitleRegexMatches({'regex': "föo"})
my_rule.id = rules.TitleRegexMatches.id + ":my-extra-rüle"
my_rule.name = rules.TitleRegexMatches.name + ":my-extra-rüle"
expected_rules._rules[u'T7:my-extra-rüle'] = my_rule
self.assertEqual(config_builder.build().rules, expected_rules)
@ -233,32 +233,32 @@ class LintConfigBuilderTests(BaseTestCase):
# to the same rule
for other_rule_qualifier in rule_qualifiers:
cb = config_builder.clone()
cb.set_option(other_rule_qualifier, 'regex', other_rule_qualifier + u"bōr")
cb.set_option(other_rule_qualifier, 'regex', other_rule_qualifier + "bōr")
# before setting the expected rule option value correctly, the RuleCollection should be different
self.assertNotEqual(cb.build().rules, expected_rules)
# after setting the option on the expected rule, it should be equal
my_rule.options['regex'].set(other_rule_qualifier + u"bōr")
my_rule.options['regex'].set(other_rule_qualifier + "bōr")
self.assertEqual(cb.build().rules, expected_rules)
my_rule.options['regex'].set(u"wrong")
my_rule.options['regex'].set("wrong")
def test_named_rules_negative(self):
# T7 = title-match-regex
# Invalid rule name
for invalid_name in ["", " ", " ", "\t", "\n", u"å b", u"å:b", u"åb:", u":åb"]:
for invalid_name in ["", " ", " ", "\t", "\n", "å b", "å:b", "åb:", ":åb"]:
config_builder = LintConfigBuilder()
config_builder.set_option(u"T7:{0}".format(invalid_name), 'regex', u"tëst")
expected_msg = u"The rule-name part in 'T7:{0}' cannot contain whitespace, colons or be empty"
with self.assertRaisesMessage(LintConfigError, expected_msg.format(invalid_name)):
config_builder.set_option(f"T7:{invalid_name}", 'regex', "tëst")
expected_msg = f"The rule-name part in 'T7:{invalid_name}' cannot contain whitespace, colons or be empty"
with self.assertRaisesMessage(LintConfigError, expected_msg):
config_builder.build()
# Invalid parent rule name
config_builder = LintConfigBuilder()
config_builder.set_option(u"Ž123:foöbar", u"fåke-option", u"fåke-value")
with self.assertRaisesMessage(LintConfigError, u"No such rule 'Ž123' (named rule: 'Ž123:foöbar')"):
config_builder.set_option("Ž123:foöbar", "fåke-option", "fåke-value")
with self.assertRaisesMessage(LintConfigError, "No such rule 'Ž123' (named rule: 'Ž123:foöbar')"):
config_builder.build()
# Invalid option name (this is the same as with regular rules)
config_builder = LintConfigBuilder()
config_builder.set_option(u"T7:foöbar", u"blå", u"my-rëgex")
with self.assertRaisesMessage(LintConfigError, u"Rule 'T7:foöbar' has no option 'blå'"):
config_builder.set_option("T7:foöbar", "blå", "my-rëgex")
with self.assertRaisesMessage(LintConfigError, "Rule 'T7:foöbar' has no option 'blå'"):
config_builder.build()

View file

@ -1,20 +1,10 @@
# -*- coding: utf-8 -*-
try:
# python 2.x
from StringIO import StringIO
except ImportError:
# python 3.x
from io import StringIO
from io import StringIO
from click.testing import CliRunner
try:
# python 2.x
from mock import patch
except ImportError:
# python 3.x
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
from unittest.mock import patch
from gitlint.tests.base import BaseTestCase
from gitlint import cli
@ -25,7 +15,7 @@ class LintConfigPrecedenceTests(BaseTestCase):
def setUp(self):
self.cli = CliRunner()
@patch('gitlint.cli.get_stdin_data', return_value=u"WIP:fö\n\nThis is å test message\n")
@patch('gitlint.cli.get_stdin_data', return_value="WIP:fö\n\nThis is å test message\n")
def test_config_precedence(self, _):
# TODO(jroovers): this test really only test verbosity, we need to do some refactoring to gitlint.cli
# to more easily test everything
@ -41,14 +31,14 @@ class LintConfigPrecedenceTests(BaseTestCase):
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["-vvv", "-c", "general.verbosity=2", "--config", config_path])
self.assertEqual(result.output, "")
self.assertEqual(stderr.getvalue(), u"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n")
self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n")
# 2. environment variables
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["-c", "general.verbosity=2", "--config", config_path],
env={"GITLINT_VERBOSITY": "3"})
self.assertEqual(result.output, "")
self.assertEqual(stderr.getvalue(), u"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n")
self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n")
# 3. commandline -c flags
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
@ -66,9 +56,9 @@ class LintConfigPrecedenceTests(BaseTestCase):
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli)
self.assertEqual(result.output, "")
self.assertEqual(stderr.getvalue(), u"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n")
self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n")
@patch('gitlint.cli.get_stdin_data', return_value=u"WIP: This is å test")
@patch('gitlint.cli.get_stdin_data', return_value="WIP: This is å test")
def test_ignore_precedence(self, get_stdin_data):
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
# --ignore takes precedence over -c general.ignore
@ -77,11 +67,11 @@ class LintConfigPrecedenceTests(BaseTestCase):
self.assertEqual(result.exit_code, 1)
# We still expect the T5 violation, but no B6 violation as --ignore overwrites -c general.ignore
self.assertEqual(stderr.getvalue(),
u"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: This is å test\"\n")
"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: This is å test\"\n")
# test that we can also still configure a rule that is first ignored but then not
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
get_stdin_data.return_value = u"This is å test"
get_stdin_data.return_value = "This is å test"
# --ignore takes precedence over -c general.ignore
result = self.cli.invoke(cli.cli, ["-c", "general.ignore=title-max-length",
"-c", "title-max-length.line-length=5",
@ -91,7 +81,7 @@ class LintConfigPrecedenceTests(BaseTestCase):
# We still expect the T1 violation with custom config,
# but no B6 violation as --ignore overwrites -c general.ignore
self.assertEqual(stderr.getvalue(), u"1: T1 Title exceeds max length (14>5): \"This is å test\"\n")
self.assertEqual(stderr.getvalue(), "1: T1 Title exceeds max length (14>5): \"This is å test\"\n")
def test_general_option_after_rule_option(self):
# We used to have a bug where we didn't process general options before setting specific options, this would

View file

@ -10,34 +10,34 @@ class RuleCollectionTests(BaseTestCase):
def test_add_rule(self):
collection = RuleCollection()
collection.add_rule(rules.TitleMaxLength, u"my-rüle", {"my_attr": u"föo", "my_attr2": 123})
collection.add_rule(rules.TitleMaxLength, "my-rüle", {"my_attr": "föo", "my_attr2": 123})
expected = rules.TitleMaxLength()
expected.id = u"my-rüle"
expected.my_attr = u"föo"
expected.id = "my-rüle"
expected.my_attr = "föo"
expected.my_attr2 = 123
self.assertEqual(len(collection), 1)
self.assertDictEqual(collection._rules, OrderedDict({u"my-rüle": expected}))
self.assertDictEqual(collection._rules, OrderedDict({"my-rüle": expected}))
# Need to explicitely compare expected attributes as the rule.__eq__ method does not compare these attributes
self.assertEqual(collection._rules[expected.id].my_attr, expected.my_attr)
self.assertEqual(collection._rules[expected.id].my_attr2, expected.my_attr2)
def test_add_find_rule(self):
collection = RuleCollection()
collection.add_rules([rules.TitleMaxLength, rules.TitleTrailingWhitespace], {"my_attr": u"föo"})
collection.add_rules([rules.TitleMaxLength, rules.TitleTrailingWhitespace], {"my_attr": "föo"})
# find by id
expected = rules.TitleMaxLength()
rule = collection.find_rule('T1')
self.assertEqual(rule, expected)
self.assertEqual(rule.my_attr, u"föo")
self.assertEqual(rule.my_attr, "föo")
# find by name
expected2 = rules.TitleTrailingWhitespace()
rule = collection.find_rule('title-trailing-whitespace')
self.assertEqual(rule, expected2)
self.assertEqual(rule.my_attr, u"föo")
self.assertEqual(rule.my_attr, "föo")
# find non-existing
rule = collection.find_rule(u'föo')
@ -45,8 +45,8 @@ class RuleCollectionTests(BaseTestCase):
def test_delete_rules_by_attr(self):
collection = RuleCollection()
collection.add_rules([rules.TitleMaxLength, rules.TitleTrailingWhitespace], {"foo": u"bår"})
collection.add_rules([rules.BodyHardTab], {"hur": u"dûr"})
collection.add_rules([rules.TitleMaxLength, rules.TitleTrailingWhitespace], {"foo": "bår"})
collection.add_rules([rules.BodyHardTab], {"hur": "dûr"})
# Assert all rules are there as expected
self.assertEqual(len(collection), 3)
@ -54,11 +54,11 @@ class RuleCollectionTests(BaseTestCase):
self.assertEqual(collection.find_rule(expected_rule.id), expected_rule)
# Delete rules by attr, assert that we still have the right rules in the collection
collection.delete_rules_by_attr("foo", u"bår")
collection.delete_rules_by_attr("foo", "bår")
self.assertEqual(len(collection), 1)
self.assertIsNone(collection.find_rule(rules.TitleMaxLength.id), None)
self.assertIsNone(collection.find_rule(rules.TitleTrailingWhitespace.id), None)
found = collection.find_rule(rules.BodyHardTab.id)
self.assertEqual(found, rules.BodyHardTab())
self.assertEqual(found.hur, u"dûr")
self.assertEqual(found.hur, "dûr")

View file

@ -20,28 +20,28 @@ class ContribConventionalCommitTests(BaseTestCase):
# No violations when using a correct type and format
for type in ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert", "ci", "build"]:
violations = rule.validate(type + u": föo", None)
violations = rule.validate(type + ": föo", None)
self.assertListEqual([], violations)
# assert violation on wrong type
expected_violation = RuleViolation("CT1", "Title does not start with one of fix, feat, chore, docs,"
" style, refactor, perf, test, revert, ci, build", u"bår: foo")
violations = rule.validate(u"bår: foo", None)
" style, refactor, perf, test, revert, ci, build", "bår: foo")
violations = rule.validate("bår: foo", 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'", u"fix föo")
violations = rule.validate(u"fix föo", None)
"'type(optional-scope): description'", "fix föo")
violations = rule.validate("fix föo", None)
self.assertListEqual([expected_violation], violations)
# assert no violation when adding new type
rule = ConventionalCommit({'types': [u"föo", u"bär"]})
for typ in [u"föo", u"bär"]:
violations = rule.validate(typ + u": hür dur", None)
rule = ConventionalCommit({'types': ["föo", "bär"]})
for typ in ["föo", "bär"]:
violations = rule.validate(typ + ": hür dur", None)
self.assertListEqual([], violations)
# assert violation when using incorrect type when types have been reconfigured
violations = rule.validate(u"fix: hür dur", None)
expected_violation = RuleViolation("CT1", u"Title does not start with one of föo, bär", u"fix: hür dur")
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)

View file

@ -19,14 +19,14 @@ class ContribSignedOffByTests(BaseTestCase):
def test_signedoff_by(self):
# No violations when 'Signed-Off-By' line is present
rule = SignedOffBy()
violations = rule.validate(self.gitcommit(u"Föobar\n\nMy Body\nSigned-Off-By: John Smith"))
violations = rule.validate(self.gitcommit("Föobar\n\nMy Body\nSigned-Off-By: John Smith"))
self.assertListEqual([], violations)
# Assert violation when no 'Signed-Off-By' line is present
violations = rule.validate(self.gitcommit(u"Föobar\n\nMy Body"))
violations = rule.validate(self.gitcommit("Föobar\n\nMy Body"))
expected_violation = RuleViolation("CC1", "Body does not contain a 'Signed-Off-By' line", line_nr=1)
self.assertListEqual(violations, [expected_violation])
# Assert violation when no 'Signed-Off-By' in title but not in body
violations = rule.validate(self.gitcommit(u"Signed-Off-By\n\nFöobar"))
violations = rule.validate(self.gitcommit("Signed-Off-By\n\nFöobar"))
self.assertListEqual(violations, [expected_violation])

View file

@ -6,8 +6,6 @@ from gitlint.contrib import rules as contrib_rules
from gitlint.tests.contrib import rules as contrib_tests
from gitlint import rule_finder, rules
from gitlint.utils import ustr
class ContribRuleTests(BaseTestCase):
@ -24,10 +22,9 @@ class ContribRuleTests(BaseTestCase):
# Find all python files in the contrib dir and assert there's a corresponding test file
for filename in os.listdir(self.CONTRIB_DIR):
if filename.endswith(".py") and filename not in ["__init__.py"]:
expected_test_file = ustr(u"test_" + filename)
error_msg = u"Every Contrib Rule must have associated tests. " + \
"Expected test file {0} not found.".format(os.path.join(contrib_tests_dir,
expected_test_file))
expected_test_file = "test_" + filename
error_msg = "Every Contrib Rule must have associated tests. " + \
f"Expected test file {os.path.join(contrib_tests_dir, expected_test_file)} not found."
self.assertIn(expected_test_file, contrib_test_files, error_msg)
def test_contrib_rule_naming_conventions(self):

View file

@ -0,0 +1,2 @@
gitlint: checking commit message...
{git_repo} is not a git repository.

View file

@ -0,0 +1,2 @@
gitlint: checking commit message...
Error: The 'staged' option (--staged) can only be used when using '--msg-filename' or when piping data to gitlint via stdin.

View file

@ -1,12 +1,7 @@
# -*- coding: utf-8 -*-
import os
try:
# python 2.x
from mock import patch
except ImportError:
# python 3.x
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
from unittest.mock import patch
from gitlint.shell import ErrorReturnCode, CommandNotFound
@ -19,7 +14,7 @@ class GitTests(BaseTestCase):
# Expected special_args passed to 'sh'
expected_sh_special_args = {
'_tty_out': False,
'_cwd': u"fåke/path"
'_cwd': "fåke/path"
}
@patch('gitlint.git.sh')
@ -28,7 +23,7 @@ class GitTests(BaseTestCase):
expected_msg = "'git' command not found. You need to install git to use gitlint on a local repository. " + \
"See https://git-scm.com/book/en/v2/Getting-Started-Installing-Git on how to install git."
with self.assertRaisesMessage(GitNotInstalledError, expected_msg):
GitContext.from_local_repository(u"fåke/path")
GitContext.from_local_repository("fåke/path")
# assert that commit message was read using git command
sh.git.assert_called_once_with("log", "-1", "--pretty=%H", **self.expected_sh_special_args)
@ -39,8 +34,8 @@ class GitTests(BaseTestCase):
err = b"fatal: Not a git repository (or any of the parent directories): .git"
sh.git.side_effect = ErrorReturnCode("git log -1 --pretty=%H", b"", err)
with self.assertRaisesMessage(GitContextError, u"fåke/path is not a git repository."):
GitContext.from_local_repository(u"fåke/path")
with self.assertRaisesMessage(GitContextError, "fåke/path is not a git repository."):
GitContext.from_local_repository("fåke/path")
# assert that commit message was read using git command
sh.git.assert_called_once_with("log", "-1", "--pretty=%H", **self.expected_sh_special_args)
@ -49,9 +44,9 @@ class GitTests(BaseTestCase):
err = b"fatal: Random git error"
sh.git.side_effect = ErrorReturnCode("git log -1 --pretty=%H", b"", err)
expected_msg = u"An error occurred while executing 'git log -1 --pretty=%H': {0}".format(err)
expected_msg = f"An error occurred while executing 'git log -1 --pretty=%H': {err}"
with self.assertRaisesMessage(GitContextError, expected_msg):
GitContext.from_local_repository(u"fåke/path")
GitContext.from_local_repository("fåke/path")
# assert that commit message was read using git command
sh.git.assert_called_once_with("log", "-1", "--pretty=%H", **self.expected_sh_special_args)
@ -63,9 +58,9 @@ class GitTests(BaseTestCase):
sh.git.side_effect = ErrorReturnCode("git log -1 --pretty=%H", b"", err)
expected_msg = u"Current branch has no commits. Gitlint requires at least one commit to function."
expected_msg = "Current branch has no commits. Gitlint requires at least one commit to function."
with self.assertRaisesMessage(GitContextError, expected_msg):
GitContext.from_local_repository(u"fåke/path")
GitContext.from_local_repository("fåke/path")
# assert that commit message was read using git command
sh.git.assert_called_once_with("log", "-1", "--pretty=%H", **self.expected_sh_special_args)
@ -78,12 +73,12 @@ class GitTests(BaseTestCase):
b"'git <command> [<revision>...] -- [<file>...]'")
sh.git.side_effect = [
u"#\n", # git config --get core.commentchar
"#\n", # git config --get core.commentchar
ErrorReturnCode("rev-parse --abbrev-ref HEAD", b"", err)
]
with self.assertRaisesMessage(GitContextError, expected_msg):
context = GitContext.from_commit_msg(u"test")
context = GitContext.from_commit_msg("test")
context.current_branch
# assert that commit message was read using git command
@ -95,21 +90,19 @@ class GitTests(BaseTestCase):
self.assertEqual(git_commentchar(), "#")
git.return_value.exit_code = 0
git.return_value.__str__ = lambda _: u"ä"
git.return_value.__unicode__ = lambda _: u"ä"
self.assertEqual(git_commentchar(), u"ä")
git.return_value = "ä"
self.assertEqual(git_commentchar(), "ä")
git.return_value = ';\n'
self.assertEqual(git_commentchar(os.path.join(u"/föo", u"bar")), ';')
self.assertEqual(git_commentchar(os.path.join("/föo", "bar")), ';')
git.assert_called_with("config", "--get", "core.commentchar", _ok_code=[0, 1],
_cwd=os.path.join(u"/föo", u"bar"))
_cwd=os.path.join("/föo", "bar"))
@patch("gitlint.git._git")
def test_git_hooks_dir(self, git):
hooks_dir = os.path.join(u"föo", ".git", "hooks")
git.return_value.__str__ = lambda _: hooks_dir + "\n"
git.return_value.__unicode__ = lambda _: hooks_dir + "\n"
self.assertEqual(git_hooks_dir(u"/blä"), os.path.abspath(os.path.join(u"/blä", hooks_dir)))
hooks_dir = os.path.join("föo", ".git", "hooks")
git.return_value = hooks_dir + "\n"
self.assertEqual(git_hooks_dir("/blä"), os.path.abspath(os.path.join("/blä", hooks_dir)))
git.assert_called_once_with("rev-parse", "--git-path", "hooks", _cwd=u"/blä")
git.assert_called_once_with("rev-parse", "--git-path", "hooks", _cwd="/blä")

View file

@ -6,17 +6,11 @@ import dateutil
import arrow
try:
# python 2.x
from mock import patch, call
except ImportError:
# python 3.x
from unittest.mock import patch, call # pylint: disable=no-name-in-module, import-error
from unittest.mock import patch, call
from gitlint.tests.base import BaseTestCase
from gitlint.git import GitContext, GitCommit, GitContextError, LocalGitCommit, StagedLocalGitCommit, GitCommitMessage
from gitlint.shell import ErrorReturnCode
from gitlint.utils import ustr
class GitCommitTests(BaseTestCase):
@ -24,7 +18,7 @@ class GitCommitTests(BaseTestCase):
# Expected special_args passed to 'sh'
expected_sh_special_args = {
'_tty_out': False,
'_cwd': u"fåke/path"
'_cwd': "fåke/path"
}
@patch('gitlint.git.sh')
@ -33,14 +27,14 @@ class GitCommitTests(BaseTestCase):
sh.git.side_effect = [
sample_sha,
u"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
u"cömmit-title\n\ncömmit-body",
u"#", # git config --get core.commentchar
u"file1.txt\npåth/to/file2.txt\n",
u"foöbar\n* hürdur\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",
"#", # git config --get core.commentchar
"file1.txt\npåth/to/file2.txt\n",
"foöbar\n* hürdur\n"
]
context = GitContext.from_local_repository(u"fåke/path")
context = GitContext.from_local_repository("fåke/path")
# assert that commit info was read using git command
expected_calls = [
call("log", "-1", "--pretty=%H", **self.expected_sh_special_args),
@ -57,13 +51,13 @@ class GitCommitTests(BaseTestCase):
last_commit = context.commits[-1]
self.assertIsInstance(last_commit, LocalGitCommit)
self.assertEqual(last_commit.sha, sample_sha)
self.assertEqual(last_commit.message.title, u"cömmit-title")
self.assertEqual(last_commit.message.body, ["", u"cömmit-body"])
self.assertEqual(last_commit.author_name, u"test åuthor")
self.assertEqual(last_commit.author_email, u"test-emåil@foo.com")
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, [u"åbc"])
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)
@ -72,11 +66,11 @@ class GitCommitTests(BaseTestCase):
# 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", u"påth/to/file2.txt"])
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, [u"foöbar", u"hürdur"])
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)
@ -86,14 +80,14 @@ class GitCommitTests(BaseTestCase):
sh.git.side_effect = [
sample_sha,
u"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
u"cömmit-title\n\ncömmit-body",
u"#", # git config --get core.commentchar
u"file1.txt\npåth/to/file2.txt\n",
u"foöbar\n* hürdur\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",
"#", # git config --get core.commentchar
"file1.txt\npåth/to/file2.txt\n",
"foöbar\n* hürdur\n"
]
context = GitContext.from_local_repository(u"fåke/path", sample_sha)
context = GitContext.from_local_repository("fåke/path", sample_sha)
# assert that commit info was read using git command
expected_calls = [
call("rev-list", sample_sha, **self.expected_sh_special_args),
@ -110,13 +104,13 @@ class GitCommitTests(BaseTestCase):
last_commit = context.commits[-1]
self.assertIsInstance(last_commit, LocalGitCommit)
self.assertEqual(last_commit.sha, sample_sha)
self.assertEqual(last_commit.message.title, u"cömmit-title")
self.assertEqual(last_commit.message.body, ["", u"cömmit-body"])
self.assertEqual(last_commit.author_name, u"test åuthor")
self.assertEqual(last_commit.author_email, u"test-emåil@foo.com")
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, [u"åbc"])
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)
@ -125,11 +119,11 @@ class GitCommitTests(BaseTestCase):
# 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", u"påth/to/file2.txt"])
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, [u"foöbar", u"hürdur"])
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)
@ -139,14 +133,14 @@ class GitCommitTests(BaseTestCase):
sh.git.side_effect = [
sample_sha,
u"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc def\n"
u"Merge \"foo bår commit\"",
u"#", # git config --get core.commentchar
u"file1.txt\npåth/to/file2.txt\n",
u"foöbar\n* hürdur\n"
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc def\n"
"Merge \"foo bår commit\"",
"#", # git config --get core.commentchar
"file1.txt\npåth/to/file2.txt\n",
"foöbar\n* hürdur\n"
]
context = GitContext.from_local_repository(u"fåke/path")
context = GitContext.from_local_repository("fåke/path")
# assert that commit info was read using git command
expected_calls = [
call("log", "-1", "--pretty=%H", **self.expected_sh_special_args),
@ -163,13 +157,13 @@ class GitCommitTests(BaseTestCase):
last_commit = context.commits[-1]
self.assertIsInstance(last_commit, LocalGitCommit)
self.assertEqual(last_commit.sha, sample_sha)
self.assertEqual(last_commit.message.title, u"Merge \"foo bår commit\"")
self.assertEqual(last_commit.message.title, "Merge \"foo bår commit\"")
self.assertEqual(last_commit.message.body, [])
self.assertEqual(last_commit.author_name, u"test åuthor")
self.assertEqual(last_commit.author_email, u"test-emåil@foo.com")
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, [u"åbc", "def"])
self.assertListEqual(last_commit.parents, ["åbc", "def"])
self.assertTrue(last_commit.is_merge_commit)
self.assertFalse(last_commit.is_fixup_commit)
self.assertFalse(last_commit.is_squash_commit)
@ -178,11 +172,11 @@ class GitCommitTests(BaseTestCase):
# 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", u"påth/to/file2.txt"])
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, [u"foöbar", u"hürdur"])
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)
@ -194,14 +188,14 @@ class GitCommitTests(BaseTestCase):
sh.git.side_effect = [
sample_sha,
u"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
u"{0}! \"foo bår commit\"".format(commit_type),
u"#", # git config --get core.commentchar
u"file1.txt\npåth/to/file2.txt\n",
u"foöbar\n* hürdur\n"
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
f"{commit_type}! \"foo bår commit\"",
"#", # git config --get core.commentchar
"file1.txt\npåth/to/file2.txt\n",
"foöbar\n* hürdur\n"
]
context = GitContext.from_local_repository(u"fåke/path")
context = GitContext.from_local_repository("fåke/path")
# assert that commit info was read using git command
expected_calls = [
call("log", "-1", "--pretty=%H", **self.expected_sh_special_args),
@ -218,13 +212,13 @@ class GitCommitTests(BaseTestCase):
last_commit = context.commits[-1]
self.assertIsInstance(last_commit, LocalGitCommit)
self.assertEqual(last_commit.sha, sample_sha)
self.assertEqual(last_commit.message.title, u"{0}! \"foo bår commit\"".format(commit_type))
self.assertEqual(last_commit.message.title, f"{commit_type}! \"foo bår commit\"")
self.assertEqual(last_commit.message.body, [])
self.assertEqual(last_commit.author_name, u"test åuthor")
self.assertEqual(last_commit.author_email, u"test-emåil@foo.com")
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, [u"åbc"])
self.assertListEqual(last_commit.parents, ["åbc"])
# First 2 'git log' calls should've happened at this point
self.assertEqual(sh.git.mock_calls, expected_calls[:3])
@ -236,13 +230,13 @@ class GitCommitTests(BaseTestCase):
self.assertFalse(last_commit.is_merge_commit)
self.assertFalse(last_commit.is_revert_commit)
self.assertListEqual(last_commit.changed_files, ["file1.txt", u"påth/to/file2.txt"])
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.txt"])
self.assertListEqual(last_commit.changed_files, ["file1.txt", u"påth/to/file2.txt"])
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, [u"foöbar", u"hürdur"])
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)
@ -250,27 +244,27 @@ class GitCommitTests(BaseTestCase):
@patch("gitlint.git.git_commentchar")
def test_from_commit_msg_full(self, commentchar):
commentchar.return_value = u"#"
commentchar.return_value = "#"
gitcontext = GitContext.from_commit_msg(self.get_sample("commit_message/sample1"))
expected_title = u"Commit title contåining 'WIP', as well as trailing punctuation."
expected_title = "Commit title contåining 'WIP', as well as trailing punctuation."
expected_body = ["This line should be empty",
"This is the first line of the commit message body and it is meant to test a " +
"line that exceeds the maximum line length of 80 characters.",
u"This line has a tråiling space. ",
"This line has a tråiling space. ",
"This line has a trailing tab.\t"]
expected_full = expected_title + "\n" + "\n".join(expected_body)
expected_original = expected_full + (
u"\n# This is a cömmented line\n"
u"# ------------------------ >8 ------------------------\n"
u"# Anything after this line should be cleaned up\n"
u"# this line appears on `git commit -v` command\n"
u"diff --git a/gitlint/tests/samples/commit_message/sample1 "
u"b/gitlint/tests/samples/commit_message/sample1\n"
u"index 82dbe7f..ae71a14 100644\n"
u"--- a/gitlint/tests/samples/commit_message/sample1\n"
u"+++ b/gitlint/tests/samples/commit_message/sample1\n"
u"@@ -1 +1 @@\n"
"\n# This is a cömmented line\n"
"# ------------------------ >8 ------------------------\n"
"# Anything after this line should be cleaned up\n"
"# this line appears on `git commit -v` command\n"
"diff --git a/gitlint/tests/samples/commit_message/sample1 "
"b/gitlint/tests/samples/commit_message/sample1\n"
"index 82dbe7f..ae71a14 100644\n"
"--- a/gitlint/tests/samples/commit_message/sample1\n"
"+++ b/gitlint/tests/samples/commit_message/sample1\n"
"@@ -1 +1 @@\n"
)
commit = gitcontext.commits[-1]
@ -297,10 +291,10 @@ class GitCommitTests(BaseTestCase):
self.assertIsInstance(commit, GitCommit)
self.assertFalse(isinstance(commit, LocalGitCommit))
self.assertEqual(commit.message.title, u"Just a title contåining WIP")
self.assertEqual(commit.message.title, "Just a title contåining WIP")
self.assertEqual(commit.message.body, [])
self.assertEqual(commit.message.full, u"Just a title contåining WIP")
self.assertEqual(commit.message.original, u"Just a title contåining WIP")
self.assertEqual(commit.message.full, "Just a title contåining WIP")
self.assertEqual(commit.message.original, "Just a title contåining WIP")
self.assertEqual(commit.author_name, None)
self.assertEqual(commit.author_email, None)
self.assertListEqual(commit.parents, [])
@ -334,16 +328,16 @@ class GitCommitTests(BaseTestCase):
@patch("gitlint.git.git_commentchar")
def test_from_commit_msg_comment(self, commentchar):
commentchar.return_value = u"#"
gitcontext = GitContext.from_commit_msg(u"Tïtle\n\nBödy 1\n#Cömment\nBody 2")
commentchar.return_value = "#"
gitcontext = GitContext.from_commit_msg("Tïtle\n\nBödy 1\n#Cömment\nBody 2")
commit = gitcontext.commits[-1]
self.assertIsInstance(commit, GitCommit)
self.assertFalse(isinstance(commit, LocalGitCommit))
self.assertEqual(commit.message.title, u"Tïtle")
self.assertEqual(commit.message.body, ["", u"Bödy 1", "Body 2"])
self.assertEqual(commit.message.full, u"Tïtle\n\nBödy 1\nBody 2")
self.assertEqual(commit.message.original, u"Tïtle\n\nBödy 1\n#Cömment\nBody 2")
self.assertEqual(commit.message.title, "Tïtle")
self.assertEqual(commit.message.body, ["", "Bödy 1", "Body 2"])
self.assertEqual(commit.message.full, "Tïtle\n\nBödy 1\nBody 2")
self.assertEqual(commit.message.original, "Tïtle\n\nBödy 1\n#Cömment\nBody 2")
self.assertEqual(commit.author_name, None)
self.assertEqual(commit.author_email, None)
self.assertEqual(commit.date, None)
@ -402,7 +396,7 @@ class GitCommitTests(BaseTestCase):
def test_from_commit_msg_fixup_squash_commit(self):
commit_types = ["fixup", "squash"]
for commit_type in commit_types:
commit_msg = "{0}! Test message".format(commit_type)
commit_msg = f"{commit_type}! Test message"
gitcontext = GitContext.from_commit_msg(commit_msg)
commit = gitcontext.commits[-1]
@ -431,16 +425,16 @@ class GitCommitTests(BaseTestCase):
# StagedLocalGitCommit()
sh.git.side_effect = [
u"#", # git config --get core.commentchar
u"test åuthor\n", # git config --get user.name
u"test-emåil@foo.com\n", # git config --get user.email
u"my-brånch\n", # git rev-parse --abbrev-ref HEAD
u"file1.txt\npåth/to/file2.txt\n",
"#", # git config --get core.commentchar
"test åuthor\n", # git config --get user.name
"test-emåil@foo.com\n", # git config --get user.email
"my-brånch\n", # git rev-parse --abbrev-ref HEAD
"file1.txt\npåth/to/file2.txt\n",
]
now.side_effect = [arrow.get("2020-02-19T12:18:46.675182+01:00")]
# We use a fixup commit, just to test a non-default path
context = GitContext.from_staged_commit(u"fixup! Foōbar 123\n\ncömmit-body\n", u"fåke/path")
context = GitContext.from_staged_commit("fixup! Foōbar 123\n\ncömmit-body\n", "fåke/path")
# git calls we're expexting
expected_calls = [
@ -454,15 +448,15 @@ class GitCommitTests(BaseTestCase):
last_commit = context.commits[-1]
self.assertIsInstance(last_commit, StagedLocalGitCommit)
self.assertIsNone(last_commit.sha, None)
self.assertEqual(last_commit.message.title, u"fixup! Foōbar 123")
self.assertEqual(last_commit.message.body, ["", u"cömmit-body"])
self.assertEqual(last_commit.message.title, "fixup! Foōbar 123")
self.assertEqual(last_commit.message.body, ["", "cömmit-body"])
# Only `git config --get core.commentchar` should've happened up until this point
self.assertListEqual(sh.git.mock_calls, expected_calls[0:1])
self.assertEqual(last_commit.author_name, u"test åuthor")
self.assertEqual(last_commit.author_name, "test åuthor")
self.assertListEqual(sh.git.mock_calls, expected_calls[0:2])
self.assertEqual(last_commit.author_email, u"test-emåil@foo.com")
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
self.assertListEqual(sh.git.mock_calls, expected_calls[0:3])
self.assertEqual(last_commit.date, datetime.datetime(2020, 2, 19, 12, 18, 46,
@ -475,10 +469,10 @@ class GitCommitTests(BaseTestCase):
self.assertFalse(last_commit.is_squash_commit)
self.assertFalse(last_commit.is_revert_commit)
self.assertListEqual(last_commit.branches, [u"my-brånch"])
self.assertListEqual(last_commit.branches, ["my-brånch"])
self.assertListEqual(sh.git.mock_calls, expected_calls[0:4])
self.assertListEqual(last_commit.changed_files, ["file1.txt", u"påth/to/file2.txt"])
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.txt"])
self.assertListEqual(sh.git.mock_calls, expected_calls[0:5])
@patch('gitlint.git.sh')
@ -486,32 +480,32 @@ class GitCommitTests(BaseTestCase):
# StagedLocalGitCommit()
sh.git.side_effect = [
u"#", # git config --get core.commentchar
"#", # git config --get core.commentchar
ErrorReturnCode('git config --get user.name', b"", b""),
]
expected_msg = "Missing git configuration: please set user.name"
with self.assertRaisesMessage(GitContextError, expected_msg):
ctx = GitContext.from_staged_commit(u"Foōbar 123\n\ncömmit-body\n", u"fåke/path")
[ustr(commit) for commit in ctx.commits]
ctx = GitContext.from_staged_commit("Foōbar 123\n\ncömmit-body\n", "fåke/path")
[str(commit) for commit in ctx.commits]
@patch('gitlint.git.sh')
def test_staged_commit_with_missing_email(self, sh):
# StagedLocalGitCommit()
sh.git.side_effect = [
u"#", # git config --get core.commentchar
u"test åuthor\n", # git config --get user.name
"#", # git config --get core.commentchar
"test åuthor\n", # git config --get user.name
ErrorReturnCode('git config --get user.name', b"", b""),
]
expected_msg = "Missing git configuration: please set user.email"
with self.assertRaisesMessage(GitContextError, expected_msg):
ctx = GitContext.from_staged_commit(u"Foōbar 123\n\ncömmit-body\n", u"fåke/path")
[ustr(commit) for commit in ctx.commits]
ctx = GitContext.from_staged_commit("Foōbar 123\n\ncömmit-body\n", "fåke/path")
[str(commit) for commit in ctx.commits]
def test_gitcommitmessage_equality(self):
commit_message1 = GitCommitMessage(GitContext(), u"tëst\n\nfoo", u"tëst\n\nfoo", u"tēst", ["", u"föo"])
commit_message1 = GitCommitMessage(GitContext(), "tëst\n\nfoo", "tëst\n\nfoo", "tēst", ["", "föo"])
attrs = ['original', 'full', 'title', 'body']
self.object_equality_test(commit_message1, attrs, {"context": commit_message1.context})
@ -519,20 +513,20 @@ class GitCommitTests(BaseTestCase):
def test_gitcommit_equality(self, git):
# git will be called to setup the context (commentchar and current_branch), just return the same value
# This only matters to test gitcontext equality, not gitcommit equality
git.return_value = u"foöbar"
git.return_value = "foöbar"
# Test simple equality case
now = datetime.datetime.utcnow()
context1 = GitContext()
commit_message1 = GitCommitMessage(context1, u"tëst\n\nfoo", u"tëst\n\nfoo", u"tēst", ["", u"föo"])
commit1 = GitCommit(context1, commit_message1, u"shä", now, u"Jöhn Smith", u"jöhn.smith@test.com", None,
[u"föo/bar"], [u"brånch1", u"brånch2"])
commit_message1 = GitCommitMessage(context1, "tëst\n\nfoo", "tëst\n\nfoo", "tēst", ["", "föo"])
commit1 = GitCommit(context1, commit_message1, "shä", now, "Jöhn Smith", "jöhn.smith@test.com", None,
["föo/bar"], ["brånch1", "brånch2"])
context1.commits = [commit1]
context2 = GitContext()
commit_message2 = GitCommitMessage(context2, u"tëst\n\nfoo", u"tëst\n\nfoo", u"tēst", ["", u"föo"])
commit2 = GitCommit(context2, commit_message1, u"shä", now, u"Jöhn Smith", u"jöhn.smith@test.com", None,
[u"föo/bar"], [u"brånch1", u"brånch2"])
commit_message2 = GitCommitMessage(context2, "tëst\n\nfoo", "tëst\n\nfoo", "tēst", ["", "föo"])
commit2 = GitCommit(context2, commit_message1, "shä", now, "Jöhn Smith", "jöhn.smith@test.com", None,
["föo/bar"], ["brånch1", "brånch2"])
context2.commits = [commit2]
self.assertEqual(context1, context2)
@ -547,8 +541,8 @@ class GitCommitTests(BaseTestCase):
self.object_equality_test(commit1, kwargs.keys(), {"context": commit1.context})
# Check that the is_* attributes that are affected by the commit message affect equality
special_messages = {'is_merge_commit': u"Merge: foöbar", 'is_fixup_commit': u"fixup! foöbar",
'is_squash_commit': u"squash! foöbar", 'is_revert_commit': u"Revert: foöbar"}
special_messages = {'is_merge_commit': "Merge: foöbar", 'is_fixup_commit': "fixup! foöbar",
'is_squash_commit': "squash! foöbar", 'is_revert_commit': "Revert: foöbar"}
for key in special_messages:
kwargs_copy = copy.deepcopy(kwargs)
clone1 = GitCommit(context=commit1.context, **kwargs_copy)
@ -556,16 +550,16 @@ class GitCommitTests(BaseTestCase):
self.assertTrue(getattr(clone1, key))
clone2 = GitCommit(context=commit1.context, **kwargs_copy)
clone2.message = GitCommitMessage.from_full_message(context1, u"foöbar")
clone2.message = GitCommitMessage.from_full_message(context1, "foöbar")
self.assertNotEqual(clone1, clone2)
@patch("gitlint.git.git_commentchar")
def test_commit_msg_custom_commentchar(self, patched):
patched.return_value = u"ä"
patched.return_value = "ä"
context = GitContext()
message = GitCommitMessage.from_full_message(context, u"Tïtle\n\nBödy 1\näCömment\nBody 2")
message = GitCommitMessage.from_full_message(context, "Tïtle\n\nBödy 1\näCömment\nBody 2")
self.assertEqual(message.title, u"Tïtle")
self.assertEqual(message.body, ["", u"Bödy 1", "Body 2"])
self.assertEqual(message.full, u"Tïtle\n\nBödy 1\nBody 2")
self.assertEqual(message.original, u"Tïtle\n\nBödy 1\näCömment\nBody 2")
self.assertEqual(message.title, "Tïtle")
self.assertEqual(message.body, ["", "Bödy 1", "Body 2"])
self.assertEqual(message.full, "Tïtle\n\nBödy 1\nBody 2")
self.assertEqual(message.original, "Tïtle\n\nBödy 1\näCömment\nBody 2")

View file

@ -1,11 +1,6 @@
# -*- coding: utf-8 -*-
try:
# python 2.x
from mock import patch, call
except ImportError:
# python 3.x
from unittest.mock import patch, call # pylint: disable=no-name-in-module, import-error
from unittest.mock import patch, call
from gitlint.tests.base import BaseTestCase
from gitlint.git import GitContext
@ -16,15 +11,15 @@ class GitContextTests(BaseTestCase):
# Expected special_args passed to 'sh'
expected_sh_special_args = {
'_tty_out': False,
'_cwd': u"fåke/path"
'_cwd': "fåke/path"
}
@patch('gitlint.git.sh')
def test_gitcontext(self, sh):
sh.git.side_effect = [
u"#", # git config --get core.commentchar
u"\nfoöbar\n"
"#", # git config --get core.commentchar
"\nfoöbar\n"
]
expected_calls = [
@ -32,58 +27,58 @@ class GitContextTests(BaseTestCase):
call("rev-parse", "--abbrev-ref", "HEAD", **self.expected_sh_special_args)
]
context = GitContext(u"fåke/path")
context = GitContext("fåke/path")
self.assertEqual(sh.git.mock_calls, [])
# gitcontext.comment_branch
self.assertEqual(context.commentchar, u"#")
self.assertEqual(context.commentchar, "#")
self.assertEqual(sh.git.mock_calls, expected_calls[0:1])
# gitcontext.current_branch
self.assertEqual(context.current_branch, u"foöbar")
self.assertEqual(context.current_branch, "foöbar")
self.assertEqual(sh.git.mock_calls, expected_calls)
@patch('gitlint.git.sh')
def test_gitcontext_equality(self, sh):
sh.git.side_effect = [
u"û\n", # context1: git config --get core.commentchar
u"û\n", # context2: git config --get core.commentchar
u"my-brånch\n", # context1: git rev-parse --abbrev-ref HEAD
u"my-brånch\n", # context2: git rev-parse --abbrev-ref HEAD
"û\n", # context1: git config --get core.commentchar
"û\n", # context2: git config --get core.commentchar
"my-brånch\n", # context1: git rev-parse --abbrev-ref HEAD
"my-brånch\n", # context2: git rev-parse --abbrev-ref HEAD
]
context1 = GitContext(u"fåke/path")
context1.commits = [u"fōo", u"bår"] # we don't need real commits to check for equality
context1 = GitContext("fåke/path")
context1.commits = ["fōo", "bår"] # we don't need real commits to check for equality
context2 = GitContext(u"fåke/path")
context2.commits = [u"fōo", u"bår"]
context2 = GitContext("fåke/path")
context2.commits = ["fōo", "bår"]
self.assertEqual(context1, context2)
# INEQUALITY
# Different commits
context2.commits = [u"hür", u"dür"]
context2.commits = ["hür", "dür"]
self.assertNotEqual(context1, context2)
# Different repository_path
context2.commits = context1.commits
context2.repository_path = u"ōther/path"
context2.repository_path = "ōther/path"
self.assertNotEqual(context1, context2)
# Different comment_char
context3 = GitContext(u"fåke/path")
context3.commits = [u"fōo", u"bår"]
context3 = GitContext("fåke/path")
context3.commits = ["fōo", "bår"]
sh.git.side_effect = ([
u"ç\n", # context3: git config --get core.commentchar
u"my-brånch\n" # context3: git rev-parse --abbrev-ref HEAD
"ç\n", # context3: git config --get core.commentchar
"my-brånch\n" # context3: git rev-parse --abbrev-ref HEAD
])
self.assertNotEqual(context1, context3)
# Different current_branch
context4 = GitContext(u"fåke/path")
context4.commits = [u"fōo", u"bår"]
context4 = GitContext("fåke/path")
context4.commits = ["fōo", "bår"]
sh.git.side_effect = ([
u"û\n", # context4: git config --get core.commentchar
u"different-brånch\n" # context4: git rev-parse --abbrev-ref HEAD
"û\n", # context4: git config --get core.commentchar
"different-brånch\n" # context4: git rev-parse --abbrev-ref HEAD
])
self.assertNotEqual(context1, context4)

View file

@ -8,65 +8,65 @@ class BodyRuleTests(BaseTestCase):
rule = rules.BodyMaxLineLength()
# assert no error
violation = rule.validate(u"å" * 80, None)
violation = rule.validate("å" * 80, None)
self.assertIsNone(violation)
# assert error on line length > 80
expected_violation = rules.RuleViolation("B1", "Line exceeds max length (81>80)", u"å" * 81)
violations = rule.validate(u"å" * 81, None)
expected_violation = rules.RuleViolation("B1", "Line exceeds max length (81>80)", "å" * 81)
violations = rule.validate("å" * 81, None)
self.assertListEqual(violations, [expected_violation])
# set line length to 120, and check no violation on length 73
rule = rules.BodyMaxLineLength({'line-length': 120})
violations = rule.validate(u"å" * 73, None)
violations = rule.validate("å" * 73, None)
self.assertIsNone(violations)
# assert raise on 121
expected_violation = rules.RuleViolation("B1", "Line exceeds max length (121>120)", u"å" * 121)
violations = rule.validate(u"å" * 121, None)
expected_violation = rules.RuleViolation("B1", "Line exceeds max length (121>120)", "å" * 121)
violations = rule.validate("å" * 121, None)
self.assertListEqual(violations, [expected_violation])
def test_trailing_whitespace(self):
rule = rules.BodyTrailingWhitespace()
# assert no error
violations = rule.validate(u"å", None)
violations = rule.validate("å", None)
self.assertIsNone(violations)
# trailing space
expected_violation = rules.RuleViolation("B2", "Line has trailing whitespace", u"å ")
violations = rule.validate(u"å ", None)
expected_violation = rules.RuleViolation("B2", "Line has trailing whitespace", "å ")
violations = rule.validate("å ", None)
self.assertListEqual(violations, [expected_violation])
# trailing tab
expected_violation = rules.RuleViolation("B2", "Line has trailing whitespace", u"å\t")
violations = rule.validate(u"å\t", None)
expected_violation = rules.RuleViolation("B2", "Line has trailing whitespace", "å\t")
violations = rule.validate("å\t", None)
self.assertListEqual(violations, [expected_violation])
def test_hard_tabs(self):
rule = rules.BodyHardTab()
# assert no error
violations = rule.validate(u"This is ã test", None)
violations = rule.validate("This is ã test", None)
self.assertIsNone(violations)
# contains hard tab
expected_violation = rules.RuleViolation("B3", "Line contains hard tab characters (\\t)", u"This is å\ttest")
violations = rule.validate(u"This is å\ttest", None)
expected_violation = rules.RuleViolation("B3", "Line contains hard tab characters (\\t)", "This is å\ttest")
violations = rule.validate("This is å\ttest", None)
self.assertListEqual(violations, [expected_violation])
def test_body_first_line_empty(self):
rule = rules.BodyFirstLineEmpty()
# assert no error
commit = self.gitcommit(u"Tïtle\n\nThis is the secōnd body line")
commit = self.gitcommit("Tïtle\n\nThis is the secōnd body line")
violations = rule.validate(commit)
self.assertIsNone(violations)
# second line not empty
expected_violation = rules.RuleViolation("B4", "Second line is not empty", u"nöt empty", 2)
expected_violation = rules.RuleViolation("B4", "Second line is not empty", "nöt empty", 2)
commit = self.gitcommit(u"Tïtle\nnöt empty\nThis is the secönd body line")
commit = self.gitcommit("Tïtle\nnöt empty\nThis is the secönd body line")
violations = rule.validate(commit)
self.assertListEqual(violations, [expected_violation])
@ -80,34 +80,34 @@ class BodyRuleTests(BaseTestCase):
self.assertIsNone(violations)
# assert no error - no body
commit = self.gitcommit(u"Tïtle\n")
commit = self.gitcommit("Tïtle\n")
violations = rule.validate(commit)
self.assertIsNone(violations)
# body is too short
expected_violation = rules.RuleViolation("B5", "Body message is too short (8<20)", u"töoshort", 3)
expected_violation = rules.RuleViolation("B5", "Body message is too short (8<20)", "töoshort", 3)
commit = self.gitcommit(u"Tïtle\n\ntöoshort\n")
commit = self.gitcommit("Tïtle\n\ntöoshort\n")
violations = rule.validate(commit)
self.assertListEqual(violations, [expected_violation])
# assert error - short across multiple lines
expected_violation = rules.RuleViolation("B5", "Body message is too short (11<20)", u"secöndthïrd", 3)
commit = self.gitcommit(u"Tïtle\n\nsecönd\nthïrd\n")
expected_violation = rules.RuleViolation("B5", "Body message is too short (11<20)", "secöndthïrd", 3)
commit = self.gitcommit("Tïtle\n\nsecönd\nthïrd\n")
violations = rule.validate(commit)
self.assertListEqual(violations, [expected_violation])
# set line length to 120, and check violation on length 21
expected_violation = rules.RuleViolation("B5", "Body message is too short (21<120)", u"å" * 21, 3)
expected_violation = rules.RuleViolation("B5", "Body message is too short (21<120)", "å" * 21, 3)
rule = rules.BodyMinLength({'min-length': 120})
commit = self.gitcommit(u"Title\n\n%s\n" % (u"å" * 21))
commit = self.gitcommit("Title\n\n%s\n" % ("å" * 21))
violations = rule.validate(commit)
self.assertListEqual(violations, [expected_violation])
# Make sure we don't get the error if the body-length is exactly the min-length
rule = rules.BodyMinLength({'min-length': 8})
commit = self.gitcommit(u"Tïtle\n\n%s\n" % (u"å" * 8))
commit = self.gitcommit("Tïtle\n\n%s\n" % ("å" * 8))
violations = rule.validate(commit)
self.assertIsNone(violations)
@ -115,14 +115,14 @@ class BodyRuleTests(BaseTestCase):
rule = rules.BodyMissing()
# assert no error - body is present
commit = self.gitcommit(u"Tïtle\n\nThis ïs the first body line\n")
commit = self.gitcommit("Tïtle\n\nThis ïs the first body line\n")
violations = rule.validate(commit)
self.assertIsNone(violations)
# body is too short
expected_violation = rules.RuleViolation("B6", "Body message is missing", None, 3)
commit = self.gitcommit(u"Tïtle\n")
commit = self.gitcommit("Tïtle\n")
violations = rule.validate(commit)
self.assertListEqual(violations, [expected_violation])
@ -130,7 +130,7 @@ class BodyRuleTests(BaseTestCase):
rule = rules.BodyMissing()
# assert no error - merge commit
commit = self.gitcommit(u"Merge: Tïtle\n")
commit = self.gitcommit("Merge: Tïtle\n")
violations = rule.validate(commit)
self.assertIsNone(violations)
@ -144,37 +144,37 @@ class BodyRuleTests(BaseTestCase):
rule = rules.BodyChangedFileMention()
# assert no error when no files have changed and no files need to be mentioned
commit = self.gitcommit(u"This is a test\n\nHere is a mention of föo/test.py")
commit = self.gitcommit("This is a test\n\nHere is a mention of föo/test.py")
violations = rule.validate(commit)
self.assertIsNone(violations)
# assert no error when no files have changed but certain files need to be mentioned on change
rule = rules.BodyChangedFileMention({'files': u"bar.txt,föo/test.py"})
commit = self.gitcommit(u"This is a test\n\nHere is a mention of föo/test.py")
rule = rules.BodyChangedFileMention({'files': "bar.txt,föo/test.py"})
commit = self.gitcommit("This is a test\n\nHere is a mention of föo/test.py")
violations = rule.validate(commit)
self.assertIsNone(violations)
# assert no error if a file has changed and is mentioned
commit = self.gitcommit(u"This is a test\n\nHere is a mention of föo/test.py", [u"föo/test.py"])
commit = self.gitcommit("This is a test\n\nHere is a mention of föo/test.py", ["föo/test.py"])
violations = rule.validate(commit)
self.assertIsNone(violations)
# assert no error if multiple files have changed and are mentioned
commit_msg = u"This is a test\n\nHere is a mention of föo/test.py\nAnd here is a mention of bar.txt"
commit = self.gitcommit(commit_msg, [u"föo/test.py", "bar.txt"])
commit_msg = "This is a test\n\nHere is a mention of föo/test.py\nAnd here is a mention of bar.txt"
commit = self.gitcommit(commit_msg, ["föo/test.py", "bar.txt"])
violations = rule.validate(commit)
self.assertIsNone(violations)
# assert error if file has changed and is not mentioned
commit_msg = u"This is a test\n\nHere is å mention of\nAnd here is a mention of bar.txt"
commit = self.gitcommit(commit_msg, [u"föo/test.py", "bar.txt"])
commit_msg = "This is a test\n\nHere is å mention of\nAnd here is a mention of bar.txt"
commit = self.gitcommit(commit_msg, ["föo/test.py", "bar.txt"])
violations = rule.validate(commit)
expected_violation = rules.RuleViolation("B7", u"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)
# assert multiple errors if multiple files habe changed and are not mentioned
commit_msg = u"This is å test\n\nHere is a mention of\nAnd here is a mention of"
commit = self.gitcommit(commit_msg, [u"föo/test.py", "bar.txt"])
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)
expected_violation_2 = rules.RuleViolation("B7", "Body does not mention changed file 'bar.txt'", None, 4)
self.assertEqual([expected_violation_2, expected_violation], violations)
@ -182,7 +182,7 @@ class BodyRuleTests(BaseTestCase):
def test_body_match_regex(self):
# We intentionally add 2 newlines at the end of our commit message as that's how git will pass the
# message. This way we also test that the rule strips off the last line.
commit = self.gitcommit(u"US1234: åbc\nIgnored\nBödy\nFöo\nMy-Commit-Tag: föo\n\n")
commit = self.gitcommit("US1234: åbc\nIgnored\nBödy\nFöo\nMy-Commit-Tag: föo\n\n")
# assert no violation on default regex (=everything allowed)
rule = rules.BodyRegexMatches()
@ -191,25 +191,25 @@ class BodyRuleTests(BaseTestCase):
# assert no violation on matching regex
# (also note that first body line - in between title and rest of body - is ignored)
rule = rules.BodyRegexMatches({'regex': u"^Bödy(.*)"})
rule = rules.BodyRegexMatches({'regex': "^Bödy(.*)"})
violations = rule.validate(commit)
self.assertIsNone(violations)
# assert we can do end matching (and last empty line is ignored)
# (also note that first body line - in between title and rest of body - is ignored)
rule = rules.BodyRegexMatches({'regex': u"My-Commit-Tag: föo$"})
rule = rules.BodyRegexMatches({'regex': "My-Commit-Tag: föo$"})
violations = rule.validate(commit)
self.assertIsNone(violations)
# common use-case: matching that a given line is present
rule = rules.BodyRegexMatches({'regex': u"(.*)Föo(.*)"})
rule = rules.BodyRegexMatches({'regex': "(.*)Föo(.*)"})
violations = rule.validate(commit)
self.assertIsNone(violations)
# assert violation on non-matching body
rule = rules.BodyRegexMatches({'regex': u"^Tëst(.*)Foo"})
rule = rules.BodyRegexMatches({'regex': "^Tëst(.*)Foo"})
violations = rule.validate(commit)
expected_violation = rules.RuleViolation("B8", u"Body does not match regex (^Tëst(.*)Foo)", None, 6)
expected_violation = rules.RuleViolation("B8", "Body does not match regex (^Tëst(.*)Foo)", None, 6)
self.assertListEqual(violations, [expected_violation])
# assert no violation on None regex
@ -218,7 +218,7 @@ class BodyRuleTests(BaseTestCase):
self.assertIsNone(violations)
# Assert no issues when there's no body or a weird body variation
bodies = [u"åbc", u"åbc\n", u"åbc\nföo\n", u"åbc\n\n", u"åbc\nföo\nblå", u"åbc\nföo\nblå\n"]
bodies = ["åbc", "åbc\n", "åbc\nföo\n", "åbc\n\n", "åbc\nföo\nblå", "åbc\nföo\nblå\n"]
for body in bodies:
commit = self.gitcommit(body)
rule = rules.BodyRegexMatches({'regex': ".*"})

View file

@ -6,7 +6,7 @@ from gitlint.config import LintConfig
class ConfigurationRuleTests(BaseTestCase):
def test_ignore_by_title(self):
commit = self.gitcommit(u"Releäse\n\nThis is the secōnd body line")
commit = self.gitcommit("Releäse\n\nThis is the secōnd body line")
# No regex specified -> Config shouldn't be changed
rule = rules.IgnoreByTitle()
@ -16,29 +16,29 @@ class ConfigurationRuleTests(BaseTestCase):
self.assert_logged([]) # nothing logged -> nothing ignored
# Matching regex -> expect config to ignore all rules
rule = rules.IgnoreByTitle({"regex": u"^Releäse(.*)"})
rule = rules.IgnoreByTitle({"regex": "^Releäse(.*)"})
expected_config = LintConfig()
expected_config.ignore = "all"
rule.apply(config, commit)
self.assertEqual(config, expected_config)
expected_log_message = u"DEBUG: gitlint.rules Ignoring commit because of rule 'I1': " + \
u"Commit title 'Releäse' matches the regex '^Releäse(.*)', ignoring rules: all"
expected_log_message = "DEBUG: gitlint.rules Ignoring commit because of rule 'I1': " + \
"Commit title 'Releäse' matches the regex '^Releäse(.*)', ignoring rules: all"
self.assert_log_contains(expected_log_message)
# Matching regex with specific ignore
rule = rules.IgnoreByTitle({"regex": u"^Releäse(.*)",
rule = rules.IgnoreByTitle({"regex": "^Releäse(.*)",
"ignore": "T1,B2"})
expected_config = LintConfig()
expected_config.ignore = "T1,B2"
rule.apply(config, commit)
self.assertEqual(config, expected_config)
expected_log_message = u"DEBUG: gitlint.rules Ignoring commit because of rule 'I1': " + \
u"Commit title 'Releäse' matches the regex '^Releäse(.*)', ignoring rules: T1,B2"
expected_log_message = "DEBUG: gitlint.rules Ignoring commit because of rule 'I1': " + \
"Commit title 'Releäse' matches the regex '^Releäse(.*)', ignoring rules: T1,B2"
def test_ignore_by_body(self):
commit = self.gitcommit(u"Tïtle\n\nThis is\n a relëase body\n line")
commit = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line")
# No regex specified -> Config shouldn't be changed
rule = rules.IgnoreByBody()
@ -48,32 +48,32 @@ class ConfigurationRuleTests(BaseTestCase):
self.assert_logged([]) # nothing logged -> nothing ignored
# Matching regex -> expect config to ignore all rules
rule = rules.IgnoreByBody({"regex": u"(.*)relëase(.*)"})
rule = rules.IgnoreByBody({"regex": "(.*)relëase(.*)"})
expected_config = LintConfig()
expected_config.ignore = "all"
rule.apply(config, commit)
self.assertEqual(config, expected_config)
expected_log_message = u"DEBUG: gitlint.rules Ignoring commit because of rule 'I2': " + \
u"Commit message line ' a relëase body' matches the regex '(.*)relëase(.*)'," + \
u" ignoring rules: all"
expected_log_message = "DEBUG: gitlint.rules Ignoring commit because of rule 'I2': " + \
"Commit message line ' a relëase body' matches the regex '(.*)relëase(.*)'," + \
" ignoring rules: all"
self.assert_log_contains(expected_log_message)
# Matching regex with specific ignore
rule = rules.IgnoreByBody({"regex": u"(.*)relëase(.*)",
rule = rules.IgnoreByBody({"regex": "(.*)relëase(.*)",
"ignore": "T1,B2"})
expected_config = LintConfig()
expected_config.ignore = "T1,B2"
rule.apply(config, commit)
self.assertEqual(config, expected_config)
expected_log_message = u"DEBUG: gitlint.rules Ignoring commit because of rule 'I2': " + \
u"Commit message line ' a relëase body' matches the regex '(.*)relëase(.*)', ignoring rules: T1,B2"
expected_log_message = "DEBUG: gitlint.rules Ignoring commit because of rule 'I2': " + \
"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_body_lines(self):
commit1 = self.gitcommit(u"Tïtle\n\nThis is\n a relëase body\n line")
commit2 = self.gitcommit(u"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")
# no regex specified, nothing should have happened:
# commit and config should remain identical, log should be empty
@ -85,22 +85,22 @@ class ConfigurationRuleTests(BaseTestCase):
self.assert_logged([])
# Matching regex
rule = rules.IgnoreBodyLines({"regex": u"(.*)relëase(.*)"})
rule = rules.IgnoreBodyLines({"regex": "(.*)relëase(.*)"})
config = LintConfig()
rule.apply(config, commit1)
# Our modified commit should be identical to a commit that doesn't contain the specific line
expected_commit = self.gitcommit(u"Tïtle\n\nThis is\n line")
expected_commit = self.gitcommit("Tïtle\n\nThis is\n line")
# The original message isn't touched by this rule, this way we always have a way to reference back to it,
# so assert it's not modified by setting it to the same as commit1
expected_commit.message.original = commit1.message.original
self.assertEqual(commit1, expected_commit)
self.assertEqual(config, LintConfig()) # config shouldn't have been modified
self.assert_log_contains(u"DEBUG: gitlint.rules Ignoring line ' a relëase body' because it " +
u"matches '(.*)relëase(.*)'")
self.assert_log_contains("DEBUG: gitlint.rules Ignoring line ' a relëase body' because it " +
"matches '(.*)relëase(.*)'")
# Non-Matching regex: no changes expected
commit1 = self.gitcommit(u"Tïtle\n\nThis is\n a relëase body\n line")
rule = rules.IgnoreBodyLines({"regex": u"(.*)föobar(.*)"})
commit1 = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line")
rule = rules.IgnoreBodyLines({"regex": "(.*)föobar(.*)"})
config = LintConfig()
rule.apply(config, commit1)
self.assertEqual(commit1, commit2)

View file

@ -8,25 +8,25 @@ class MetaRuleTests(BaseTestCase):
rule = AuthorValidEmail()
# valid email addresses
valid_email_addresses = [u"föo@bar.com", u"Jöhn.Doe@bar.com", u"jöhn+doe@bar.com", u"jöhn/doe@bar.com",
u"jöhn.doe@subdomain.bar.com"]
valid_email_addresses = ["föo@bar.com", "Jöhn.Doe@bar.com", "jöhn+doe@bar.com", "jöhn/doe@bar.com",
"jöhn.doe@subdomain.bar.com"]
for email in valid_email_addresses:
commit = self.gitcommit(u"", author_email=email)
commit = self.gitcommit("", author_email=email)
violations = rule.validate(commit)
self.assertIsNone(violations)
# No email address (=allowed for now, as gitlint also lints messages passed via stdin that don't have an
# email address)
commit = self.gitcommit(u"")
commit = self.gitcommit("")
violations = rule.validate(commit)
self.assertIsNone(violations)
# Invalid email addresses: no TLD, no domain, no @, space anywhere (=valid but not allowed by gitlint)
invalid_email_addresses = [u"föo@bar", u"JöhnDoe", u"Jöhn Doe", u"Jöhn Doe@foo.com", u" JöhnDoe@foo.com",
u"JöhnDoe@ foo.com", u"JöhnDoe@foo. com", u"JöhnDoe@foo. com", u"@bår.com",
u"föo@.com"]
invalid_email_addresses = ["föo@bar", "JöhnDoe", "Jöhn Doe", "Jöhn Doe@foo.com", " JöhnDoe@foo.com",
"JöhnDoe@ foo.com", "JöhnDoe@foo. com", "JöhnDoe@foo. com", "@bår.com",
"föo@.com"]
for email in invalid_email_addresses:
commit = self.gitcommit(u"", author_email=email)
commit = self.gitcommit("", author_email=email)
violations = rule.validate(commit)
self.assertListEqual(violations,
[RuleViolation("M1", "Author email for commit is invalid", email)])
@ -35,25 +35,25 @@ class MetaRuleTests(BaseTestCase):
# regex=None -> the rule isn't applied
rule = AuthorValidEmail()
rule.options['regex'].set(None)
emailadresses = [u"föo", None, u"hür dür"]
emailadresses = ["föo", None, "hür dür"]
for email in emailadresses:
commit = self.gitcommit(u"", author_email=email)
commit = self.gitcommit("", author_email=email)
violations = rule.validate(commit)
self.assertIsNone(violations)
# Custom domain
rule = AuthorValidEmail({'regex': u"[^@]+@bår.com"})
rule = AuthorValidEmail({'regex': "[^@]+@bår.com"})
valid_email_addresses = [
u"föo@bår.com", u"Jöhn.Doe@bår.com", u"jöhn+doe@bår.com", u"jöhn/doe@bår.com"]
"föo@bår.com", "Jöhn.Doe@bår.com", "jöhn+doe@bår.com", "jöhn/doe@bår.com"]
for email in valid_email_addresses:
commit = self.gitcommit(u"", author_email=email)
commit = self.gitcommit("", author_email=email)
violations = rule.validate(commit)
self.assertIsNone(violations)
# Invalid email addresses
invalid_email_addresses = [u"föo@hur.com"]
invalid_email_addresses = ["föo@hur.com"]
for email in invalid_email_addresses:
commit = self.gitcommit(u"", author_email=email)
commit = self.gitcommit("", author_email=email)
violations = rule.validate(commit)
self.assertListEqual(violations,
[RuleViolation("M1", "Author email for commit is invalid", email)])

View file

@ -10,14 +10,14 @@ class RuleTests(BaseTestCase):
# Ensure rules are not equal if they differ on their attributes
for attr in ["id", "name", "target", "options"]:
rule = Rule()
setattr(rule, attr, u"åbc")
setattr(rule, attr, "åbc")
self.assertNotEqual(Rule(), rule)
def test_rule_log(self):
rule = Rule()
rule.log.debug(u"Tēst message")
self.assert_log_contains(u"DEBUG: gitlint.rules Tēst message")
rule.log.debug("Tēst message")
self.assert_log_contains("DEBUG: gitlint.rules Tēst message")
def test_rule_violation_equality(self):
violation1 = RuleViolation(u"ïd1", u"My messåge", u"My cöntent", 1)
violation1 = RuleViolation("ïd1", "My messåge", "My cöntent", 1)
self.object_equality_test(violation1, ["rule_id", "message", "content", "line_nr"])

View file

@ -9,66 +9,66 @@ class TitleRuleTests(BaseTestCase):
rule = TitleMaxLength()
# assert no error
violation = rule.validate(u"å" * 72, None)
violation = rule.validate("å" * 72, None)
self.assertIsNone(violation)
# assert error on line length > 72
expected_violation = RuleViolation("T1", "Title exceeds max length (73>72)", u"å" * 73)
violations = rule.validate(u"å" * 73, None)
expected_violation = RuleViolation("T1", "Title exceeds max length (73>72)", "å" * 73)
violations = rule.validate("å" * 73, None)
self.assertListEqual(violations, [expected_violation])
# set line length to 120, and check no violation on length 73
rule = TitleMaxLength({'line-length': 120})
violations = rule.validate(u"å" * 73, None)
violations = rule.validate("å" * 73, None)
self.assertIsNone(violations)
# assert raise on 121
expected_violation = RuleViolation("T1", "Title exceeds max length (121>120)", u"å" * 121)
violations = rule.validate(u"å" * 121, None)
expected_violation = RuleViolation("T1", "Title exceeds max length (121>120)", "å" * 121)
violations = rule.validate("å" * 121, None)
self.assertListEqual(violations, [expected_violation])
def test_trailing_whitespace(self):
rule = TitleTrailingWhitespace()
# assert no error
violations = rule.validate(u"å", None)
violations = rule.validate("å", None)
self.assertIsNone(violations)
# trailing space
expected_violation = RuleViolation("T2", "Title has trailing whitespace", u"å ")
violations = rule.validate(u"å ", None)
expected_violation = RuleViolation("T2", "Title has trailing whitespace", "å ")
violations = rule.validate("å ", None)
self.assertListEqual(violations, [expected_violation])
# trailing tab
expected_violation = RuleViolation("T2", "Title has trailing whitespace", u"å\t")
violations = rule.validate(u"å\t", None)
expected_violation = RuleViolation("T2", "Title has trailing whitespace", "å\t")
violations = rule.validate("å\t", None)
self.assertListEqual(violations, [expected_violation])
def test_hard_tabs(self):
rule = TitleHardTab()
# assert no error
violations = rule.validate(u"This is å test", None)
violations = rule.validate("This is å test", None)
self.assertIsNone(violations)
# contains hard tab
expected_violation = RuleViolation("T4", "Title contains hard tab characters (\\t)", u"This is å\ttest")
violations = rule.validate(u"This is å\ttest", None)
expected_violation = RuleViolation("T4", "Title contains hard tab characters (\\t)", "This is å\ttest")
violations = rule.validate("This is å\ttest", None)
self.assertListEqual(violations, [expected_violation])
def test_trailing_punctuation(self):
rule = TitleTrailingPunctuation()
# assert no error
violations = rule.validate(u"This is å test", None)
violations = rule.validate("This is å test", None)
self.assertIsNone(violations)
# assert errors for different punctuations
punctuation = u"?:!.,;"
punctuation = "?:!.,;"
for char in punctuation:
line = u"This is å test" + char # note that make sure to include some unicode!
line = "This is å test" + char # note that make sure to include some unicode!
gitcontext = self.gitcontext(line)
expected_violation = RuleViolation("T3", u"Title has trailing punctuation ({0})".format(char), line)
expected_violation = RuleViolation("T3", f"Title has trailing punctuation ({char})", line)
violations = rule.validate(line, gitcontext)
self.assertListEqual(violations, [expected_violation])
@ -76,40 +76,40 @@ class TitleRuleTests(BaseTestCase):
rule = TitleMustNotContainWord()
# no violations
violations = rule.validate(u"This is å test", None)
violations = rule.validate("This is å test", None)
self.assertIsNone(violations)
# no violation if WIP occurs inside a wor
violations = rule.validate(u"This is å wiping test", None)
violations = rule.validate("This is å wiping test", None)
self.assertIsNone(violations)
# match literally
violations = rule.validate(u"WIP This is å test", None)
violations = rule.validate("WIP This is å test", None)
expected_violation = RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
u"WIP This is å test")
"WIP This is å test")
self.assertListEqual(violations, [expected_violation])
# match case insensitive
violations = rule.validate(u"wip This is å test", None)
violations = rule.validate("wip This is å test", None)
expected_violation = RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
u"wip This is å test")
"wip This is å test")
self.assertListEqual(violations, [expected_violation])
# match if there is a colon after the word
violations = rule.validate(u"WIP:This is å test", None)
violations = rule.validate("WIP:This is å test", None)
expected_violation = RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
u"WIP:This is å test")
"WIP:This is å test")
self.assertListEqual(violations, [expected_violation])
# match multiple words
rule = TitleMustNotContainWord({'words': u"wip,test,å"})
violations = rule.validate(u"WIP:This is å test", None)
rule = TitleMustNotContainWord({'words': "wip,test,å"})
violations = rule.validate("WIP:This is å test", None)
expected_violation = RuleViolation("T5", "Title contains the word 'wip' (case-insensitive)",
u"WIP:This is å test")
"WIP:This is å test")
expected_violation2 = RuleViolation("T5", "Title contains the word 'test' (case-insensitive)",
u"WIP:This is å test")
expected_violation3 = RuleViolation("T5", u"Title contains the word 'å' (case-insensitive)",
u"WIP:This is å test")
"WIP:This is å test")
expected_violation3 = RuleViolation("T5", "Title contains the word 'å' (case-insensitive)",
"WIP:This is å test")
self.assertListEqual(violations, [expected_violation, expected_violation2, expected_violation3])
def test_leading_whitespace(self):
@ -130,12 +130,12 @@ class TitleRuleTests(BaseTestCase):
self.assertListEqual(violations, [expected_violation])
# unicode test
expected_violation = RuleViolation("T6", "Title has leading whitespace", u"")
violations = rule.validate(u"", None)
expected_violation = RuleViolation("T6", "Title has leading whitespace", "")
violations = rule.validate("", None)
self.assertListEqual(violations, [expected_violation])
def test_regex_matches(self):
commit = self.gitcommit(u"US1234: åbc\n")
commit = self.gitcommit("US1234: åbc\n")
# assert no violation on default regex (=everything allowed)
rule = TitleRegexMatches()
@ -143,41 +143,41 @@ class TitleRuleTests(BaseTestCase):
self.assertIsNone(violations)
# assert no violation on matching regex
rule = TitleRegexMatches({'regex': u"^US[0-9]*: å"})
rule = TitleRegexMatches({'regex': "^US[0-9]*: å"})
violations = rule.validate(commit.message.title, commit)
self.assertIsNone(violations)
# assert violation when no matching regex
rule = TitleRegexMatches({'regex': u"^UÅ[0-9]*"})
rule = TitleRegexMatches({'regex': "^UÅ[0-9]*"})
violations = rule.validate(commit.message.title, commit)
expected_violation = RuleViolation("T7", u"Title does not match regex (^UÅ[0-9]*)", u"US1234: åbc")
expected_violation = RuleViolation("T7", "Title does not match regex (^UÅ[0-9]*)", "US1234: åbc")
self.assertListEqual(violations, [expected_violation])
def test_min_line_length(self):
rule = TitleMinLength()
# assert no error
violation = rule.validate(u"å" * 72, None)
violation = rule.validate("å" * 72, None)
self.assertIsNone(violation)
# assert error on line length < 5
expected_violation = RuleViolation("T8", "Title is too short (4<5)", u"å" * 4, 1)
violations = rule.validate(u"å" * 4, None)
expected_violation = RuleViolation("T8", "Title is too short (4<5)", "å" * 4, 1)
violations = rule.validate("å" * 4, None)
self.assertListEqual(violations, [expected_violation])
# set line length to 3, and check no violation on length 4
rule = TitleMinLength({'min-length': 3})
violations = rule.validate(u"å" * 4, None)
violations = rule.validate("å" * 4, None)
self.assertIsNone(violations)
# assert no violations on length 3 (this asserts we've implemented a *strict* less than)
rule = TitleMinLength({'min-length': 3})
violations = rule.validate(u"å" * 3, None)
violations = rule.validate("å" * 3, None)
self.assertIsNone(violations)
# assert raise on 2
expected_violation = RuleViolation("T8", "Title is too short (2<3)", u"å" * 2, 1)
violations = rule.validate(u"å" * 2, None)
expected_violation = RuleViolation("T8", "Title is too short (2<3)", "å" * 2, 1)
violations = rule.validate("å" * 2, None)
self.assertListEqual(violations, [expected_violation])
# assert raise on empty title

View file

@ -6,7 +6,6 @@ import sys
from gitlint.tests.base import BaseTestCase
from gitlint.rule_finder import find_rule_classes, assert_valid_rule_class
from gitlint.rules import UserRuleError
from gitlint.utils import ustr
from gitlint import options, rules
@ -25,7 +24,7 @@ class UserRuleTests(BaseTestCase):
# - Other members of the my_commit_rules module are ignored
# (such as func_should_be_ignored, global_variable_should_be_ignored)
# - Rules are loaded non-recursively (user_rules/import_exception directory is ignored)
self.assertEqual("[<class 'my_commit_rules.MyUserCommitRule'>]", ustr(classes))
self.assertEqual("[<class 'my_commit_rules.MyUserCommitRule'>]", str(classes))
# Assert that we added the new user_rules directory to the system path and modules
self.assertIn(user_rule_path, sys.path)
@ -33,8 +32,8 @@ class UserRuleTests(BaseTestCase):
# Do some basic asserts on our user rule
self.assertEqual(classes[0].id, "UC1")
self.assertEqual(classes[0].name, u"my-üser-commit-rule")
expected_option = options.IntOption('violation-count', 1, u"Number of violåtions to return")
self.assertEqual(classes[0].name, "my-üser-commit-rule")
expected_option = options.IntOption('violation-count', 1, "Number of violåtions to return")
self.assertListEqual(classes[0].options_spec, [expected_option])
self.assertTrue(hasattr(classes[0], "validate"))
@ -42,13 +41,13 @@ class UserRuleTests(BaseTestCase):
# expected result
rule_class = classes[0]()
violations = rule_class.validate("false-commit-object (ignored)")
self.assertListEqual(violations, [rules.RuleViolation("UC1", u"Commit violåtion 1", u"Contënt 1", 1)])
self.assertListEqual(violations, [rules.RuleViolation("UC1", "Commit violåtion 1", "Contënt 1", 1)])
# Have it return more violations
rule_class.options['violation-count'].value = 2
violations = rule_class.validate("false-commit-object (ignored)")
self.assertListEqual(violations, [rules.RuleViolation("UC1", u"Commit violåtion 1", u"Contënt 1", 1),
rules.RuleViolation("UC1", u"Commit violåtion 2", u"Contënt 2", 2)])
self.assertListEqual(violations, [rules.RuleViolation("UC1", "Commit violåtion 1", "Contënt 1", 1),
rules.RuleViolation("UC1", "Commit violåtion 2", "Contënt 2", 2)])
def test_extra_path_specified_by_file(self):
# Test that find_rule_classes can handle an extra path given as a file name instead of a directory
@ -58,7 +57,7 @@ class UserRuleTests(BaseTestCase):
rule_class = classes[0]()
violations = rule_class.validate("false-commit-object (ignored)")
self.assertListEqual(violations, [rules.RuleViolation("UC1", u"Commit violåtion 1", u"Contënt 1", 1)])
self.assertListEqual(violations, [rules.RuleViolation("UC1", "Commit violåtion 1", "Contënt 1", 1)])
def test_rules_from_init_file(self):
# Test that we can import rules that are defined in __init__.py files
@ -68,8 +67,8 @@ class UserRuleTests(BaseTestCase):
classes = find_rule_classes(user_rule_path)
# convert classes to strings and sort them so we can compare them
class_strings = sorted([ustr(clazz) for clazz in classes])
expected = [u"<class 'my_commit_rules.MyUserCommitRule'>", u"<class 'parent_package.InitFileRule'>"]
class_strings = sorted([str(clazz) for clazz in classes])
expected = ["<class 'my_commit_rules.MyUserCommitRule'>", "<class 'parent_package.InitFileRule'>"]
self.assertListEqual(class_strings, expected)
def test_empty_user_classes(self):
@ -92,8 +91,8 @@ class UserRuleTests(BaseTestCase):
find_rule_classes(user_rule_path)
def test_find_rule_classes_nonexisting_path(self):
with self.assertRaisesMessage(UserRuleError, u"Invalid extra-path: föo/bar"):
find_rule_classes(u"föo/bar")
with self.assertRaisesMessage(UserRuleError, "Invalid extra-path: föo/bar"):
find_rule_classes("föo/bar")
def test_assert_valid_rule_class(self):
class MyLineRuleClass(rules.LineRule):
@ -132,7 +131,7 @@ class UserRuleTests(BaseTestCase):
def test_assert_valid_rule_class_negative_parent(self):
# rule class must extend from LineRule or CommitRule
class MyRuleClass(object):
class MyRuleClass:
pass
expected_msg = "User-defined rule class 'MyRuleClass' must extend from gitlint.rules.LineRule, " + \
@ -160,8 +159,9 @@ class UserRuleTests(BaseTestCase):
# Rule ids must not start with one of the reserved id letters
for letter in ["T", "R", "B", "M", "I"]:
MyRuleClass.id = letter + "1"
expected_msg = "The id '{0}' of 'MyRuleClass' is invalid. Gitlint reserves ids starting with R,T,B,M,I"
with self.assertRaisesMessage(UserRuleError, expected_msg.format(letter)):
expected_msg = f"The id '{letter}' of 'MyRuleClass' is invalid. " + \
"Gitlint reserves ids starting with R,T,B,M,I"
with self.assertRaisesMessage(UserRuleError, expected_msg):
assert_valid_rule_class(MyRuleClass)
def test_assert_valid_rule_class_negative_name(self):
@ -186,17 +186,17 @@ class UserRuleTests(BaseTestCase):
class MyRuleClass(parent_class):
id = "UC1"
name = u"my-rüle-class"
name = "my-rüle-class"
# if set, option_spec must be a list of gitlint options
MyRuleClass.options_spec = u"föo"
MyRuleClass.options_spec = "föo"
expected_msg = "The options_spec attribute of user-defined rule class 'MyRuleClass' must be a list " + \
"of gitlint.options.RuleOption"
with self.assertRaisesMessage(UserRuleError, expected_msg):
assert_valid_rule_class(MyRuleClass)
# option_spec is a list, but not of gitlint options
MyRuleClass.options_spec = [u"föo", 123] # pylint: disable=bad-option-value,redefined-variable-type
MyRuleClass.options_spec = ["föo", 123] # pylint: disable=bad-option-value,redefined-variable-type
with self.assertRaisesMessage(UserRuleError, expected_msg):
assert_valid_rule_class(MyRuleClass)
@ -206,14 +206,14 @@ class UserRuleTests(BaseTestCase):
for clazz in baseclasses:
class MyRuleClass(clazz):
id = "UC1"
name = u"my-rüle-class"
name = "my-rüle-class"
with self.assertRaisesMessage(UserRuleError,
"User-defined rule class 'MyRuleClass' must have a 'validate' method"):
assert_valid_rule_class(MyRuleClass)
# validate attribute - not a method
MyRuleClass.validate = u"föo"
MyRuleClass.validate = "föo"
with self.assertRaisesMessage(UserRuleError,
"User-defined rule class 'MyRuleClass' must have a 'validate' method"):
assert_valid_rule_class(MyRuleClass)
@ -221,21 +221,21 @@ class UserRuleTests(BaseTestCase):
def test_assert_valid_rule_class_negative_apply(self):
class MyRuleClass(rules.ConfigurationRule):
id = "UCR1"
name = u"my-rüle-class"
name = "my-rüle-class"
expected_msg = "User-defined Configuration rule class 'MyRuleClass' must have an 'apply' method"
with self.assertRaisesMessage(UserRuleError, expected_msg):
assert_valid_rule_class(MyRuleClass)
# validate attribute - not a method
MyRuleClass.validate = u"föo"
MyRuleClass.validate = "föo"
with self.assertRaisesMessage(UserRuleError, expected_msg):
assert_valid_rule_class(MyRuleClass)
def test_assert_valid_rule_class_negative_target(self):
class MyRuleClass(rules.LineRule):
id = "UC1"
name = u"my-rüle-class"
name = "my-rüle-class"
def validate(self):
pass
@ -247,7 +247,7 @@ class UserRuleTests(BaseTestCase):
assert_valid_rule_class(MyRuleClass)
# invalid target
MyRuleClass.target = u"föo"
MyRuleClass.target = "föo"
with self.assertRaisesMessage(UserRuleError, expected_msg):
assert_valid_rule_class(MyRuleClass)

View file

@ -5,14 +5,14 @@ from gitlint.options import IntOption
class MyUserCommitRule(CommitRule):
name = u"my-üser-commit-rule"
name = "my-üser-commit-rule"
id = "UC1"
options_spec = [IntOption('violation-count', 1, u"Number of violåtions to return")]
options_spec = [IntOption('violation-count', 1, "Number of violåtions to return")]
def validate(self, _commit):
violations = []
for i in range(1, self.options['violation-count'].value + 1):
violations.append(RuleViolation(self.id, u"Commit violåtion %d" % i, u"Contënt %d" % i, i))
violations.append(RuleViolation(self.id, "Commit violåtion %d" % i, "Contënt %d" % i, i))
return violations

View file

@ -5,7 +5,7 @@ from gitlint.rules import CommitRule
class InitFileRule(CommitRule):
name = u"my-init-cömmit-rule"
name = "my-init-cömmit-rule"
id = "UC1"
options_spec = []

View file

@ -4,7 +4,7 @@ from gitlint.rules import CommitRule
class MyUserCommitRule(CommitRule):
name = u"my-user-cömmit-rule"
name = "my-user-cömmit-rule"
id = "UC2"
options_spec = []

View file

@ -16,13 +16,13 @@ class CacheTests(BaseTestCase):
@cache
def foo(self):
self.counter += 1
return u"bår"
return "bår"
@property
@cache(cachekey=u"hür")
@cache(cachekey="hür")
def bar(self):
self.counter += 1
return u"fōo"
return "fōo"
def test_cache(self):
# Init new class with cached properties
@ -31,14 +31,14 @@ class CacheTests(BaseTestCase):
self.assertDictEqual(myclass._cache, {})
# Assert that function is called on first access, cache is set
self.assertEqual(myclass.foo, u"bår")
self.assertEqual(myclass.foo, "bår")
self.assertEqual(myclass.counter, 1)
self.assertDictEqual(myclass._cache, {"foo": u"bår"})
self.assertDictEqual(myclass._cache, {"foo": "bår"})
# After function is not called on subsequent access, cache is still set
self.assertEqual(myclass.foo, u"bår")
self.assertEqual(myclass.foo, "bår")
self.assertEqual(myclass.counter, 1)
self.assertDictEqual(myclass._cache, {"foo": u"bår"})
self.assertDictEqual(myclass._cache, {"foo": "bår"})
def test_cache_custom_key(self):
# Init new class with cached properties
@ -47,11 +47,11 @@ class CacheTests(BaseTestCase):
self.assertDictEqual(myclass._cache, {})
# Assert that function is called on first access, cache is set with custom key
self.assertEqual(myclass.bar, u"fōo")
self.assertEqual(myclass.bar, "fōo")
self.assertEqual(myclass.counter, 1)
self.assertDictEqual(myclass._cache, {u"hür": u"fōo"})
self.assertDictEqual(myclass._cache, {"hür": "fōo"})
# After function is not called on subsequent access, cache is still set
self.assertEqual(myclass.bar, u"fōo")
self.assertEqual(myclass.bar, "fōo")
self.assertEqual(myclass.counter, 1)
self.assertDictEqual(myclass._cache, {u"hür": u"fōo"})
self.assertDictEqual(myclass._cache, {"hür": "fōo"})

View file

@ -1,19 +1,8 @@
# -*- coding: utf-8 -*-
try:
# python 2.x
from StringIO import StringIO
except ImportError:
# python 3.x
from io import StringIO
from io import StringIO
try:
# python 2.x
from mock import patch
except ImportError:
# python 3.x
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
from gitlint.display import Display
from gitlint.config import LintConfig
@ -28,21 +17,21 @@ class DisplayTests(BaseTestCase):
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
# Non exact outputting, should output both v and vv output
with patch('gitlint.display.stdout', new=StringIO()) as stdout:
display.v(u"tëst")
display.vv(u"tëst2")
display.v("tëst")
display.vv("tëst2")
# vvvv should be ignored regardless
display.vvv(u"tëst3.1")
display.vvv(u"tëst3.2", exact=True)
self.assertEqual(u"tëst\ntëst2\n", stdout.getvalue())
display.vvv("tëst3.1")
display.vvv("tëst3.2", exact=True)
self.assertEqual("tëst\ntëst2\n", stdout.getvalue())
# exact outputting, should only output v
with patch('gitlint.display.stdout', new=StringIO()) as stdout:
display.v(u"tëst", exact=True)
display.vv(u"tëst2", exact=True)
display.v("tëst", exact=True)
display.vv("tëst2", exact=True)
# vvvv should be ignored regardless
display.vvv(u"tëst3.1")
display.vvv(u"tëst3.2", exact=True)
self.assertEqual(u"tëst2\n", stdout.getvalue())
display.vvv("tëst3.1")
display.vvv("tëst3.2", exact=True)
self.assertEqual("tëst2\n", stdout.getvalue())
# standard error should be empty throughtout all of this
self.assertEqual('', stderr.getvalue())
@ -54,21 +43,21 @@ class DisplayTests(BaseTestCase):
with patch('gitlint.display.stdout', new=StringIO()) as stdout:
# Non exact outputting, should output both v and vv output
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
display.e(u"tëst")
display.ee(u"tëst2")
display.e("tëst")
display.ee("tëst2")
# vvvv should be ignored regardless
display.eee(u"tëst3.1")
display.eee(u"tëst3.2", exact=True)
self.assertEqual(u"tëst\ntëst2\n", stderr.getvalue())
display.eee("tëst3.1")
display.eee("tëst3.2", exact=True)
self.assertEqual("tëst\ntëst2\n", stderr.getvalue())
# exact outputting, should only output v
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
display.e(u"tëst", exact=True)
display.ee(u"tëst2", exact=True)
display.e("tëst", exact=True)
display.ee("tëst2", exact=True)
# vvvv should be ignored regardless
display.eee(u"tëst3.1")
display.eee(u"tëst3.2", exact=True)
self.assertEqual(u"tëst2\n", stderr.getvalue())
display.eee("tëst3.1")
display.eee("tëst3.2", exact=True)
self.assertEqual("tëst2\n", stderr.getvalue())
# standard output should be empty throughtout all of this
self.assertEqual('', stdout.getvalue())

View file

@ -2,12 +2,7 @@
import os
try:
# python 2.x
from mock import patch, ANY, mock_open
except ImportError:
# python 3.x
from unittest.mock import patch, ANY, mock_open # pylint: disable=no-name-in-module, import-error
from unittest.mock import patch, ANY, mock_open
from gitlint.tests.base import BaseTestCase
from gitlint.config import LintConfig
@ -19,7 +14,7 @@ class HookTests(BaseTestCase):
@patch('gitlint.hooks.git_hooks_dir')
def test_commit_msg_hook_path(self, git_hooks_dir):
git_hooks_dir.return_value = os.path.join(u"/föo", u"bar")
git_hooks_dir.return_value = os.path.join("/föo", "bar")
lint_config = LintConfig()
lint_config.target = self.SAMPLES_DIR
expected_path = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH)
@ -37,8 +32,8 @@ class HookTests(BaseTestCase):
@patch('gitlint.hooks.git_hooks_dir')
def test_install_commit_msg_hook(git_hooks_dir, isdir, path_exists, copy, stat, chmod):
lint_config = LintConfig()
lint_config.target = os.path.join(u"/hür", u"dur")
git_hooks_dir.return_value = os.path.join(u"/föo", u"bar", ".git", "hooks")
lint_config.target = os.path.join("/hür", "dur")
git_hooks_dir.return_value = os.path.join("/föo", "bar", ".git", "hooks")
expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH)
GitHookInstaller.install_commit_msg_hook(lint_config)
isdir.assert_called_with(git_hooks_dir.return_value)
@ -54,11 +49,11 @@ class HookTests(BaseTestCase):
@patch('gitlint.hooks.git_hooks_dir')
def test_install_commit_msg_hook_negative(self, git_hooks_dir, isdir, path_exists, copy):
lint_config = LintConfig()
lint_config.target = os.path.join(u"/hür", u"dur")
git_hooks_dir.return_value = os.path.join(u"/föo", u"bar", ".git", "hooks")
lint_config.target = os.path.join("/hür", "dur")
git_hooks_dir.return_value = os.path.join("/föo", "bar", ".git", "hooks")
# mock that current dir is not a git repo
isdir.return_value = False
expected_msg = u"{0} is not a git repository.".format(lint_config.target)
expected_msg = f"{lint_config.target} is not a git repository."
with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
GitHookInstaller.install_commit_msg_hook(lint_config)
isdir.assert_called_with(git_hooks_dir.return_value)
@ -69,7 +64,7 @@ class HookTests(BaseTestCase):
isdir.return_value = True
path_exists.return_value = True
expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH)
expected_msg = u"There is already a commit-msg hook file present in {0}.\n".format(expected_dst) + \
expected_msg = f"There is already a commit-msg hook file present in {expected_dst}.\n" + \
"gitlint currently does not support appending to an existing commit-msg file."
with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
GitHookInstaller.install_commit_msg_hook(lint_config)
@ -81,8 +76,8 @@ class HookTests(BaseTestCase):
@patch('gitlint.hooks.git_hooks_dir')
def test_uninstall_commit_msg_hook(git_hooks_dir, isdir, path_exists, remove):
lint_config = LintConfig()
git_hooks_dir.return_value = os.path.join(u"/föo", u"bar", ".git", "hooks")
lint_config.target = os.path.join(u"/hür", u"dur")
git_hooks_dir.return_value = os.path.join("/föo", "bar", ".git", "hooks")
lint_config.target = os.path.join("/hür", "dur")
read_data = "#!/bin/sh\n" + GITLINT_HOOK_IDENTIFIER
with patch('gitlint.hooks.io.open', mock_open(read_data=read_data), create=True):
GitHookInstaller.uninstall_commit_msg_hook(lint_config)
@ -99,12 +94,12 @@ class HookTests(BaseTestCase):
@patch('gitlint.hooks.git_hooks_dir')
def test_uninstall_commit_msg_hook_negative(self, git_hooks_dir, isdir, path_exists, remove):
lint_config = LintConfig()
lint_config.target = os.path.join(u"/hür", u"dur")
git_hooks_dir.return_value = os.path.join(u"/föo", u"bar", ".git", "hooks")
lint_config.target = os.path.join("/hür", "dur")
git_hooks_dir.return_value = os.path.join("/föo", "bar", ".git", "hooks")
# mock that the current directory is not a git repo
isdir.return_value = False
expected_msg = u"{0} is not a git repository.".format(lint_config.target)
expected_msg = f"{lint_config.target} is not a git repository."
with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
GitHookInstaller.uninstall_commit_msg_hook(lint_config)
isdir.assert_called_with(git_hooks_dir.return_value)
@ -115,7 +110,7 @@ class HookTests(BaseTestCase):
isdir.return_value = True
path_exists.return_value = False
expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH)
expected_msg = u"There is no commit-msg hook present in {0}.".format(expected_dst)
expected_msg = f"There is no commit-msg hook present in {expected_dst}."
with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
GitHookInstaller.uninstall_commit_msg_hook(lint_config)
isdir.assert_called_with(git_hooks_dir.return_value)
@ -127,7 +122,7 @@ class HookTests(BaseTestCase):
path_exists.return_value = True
read_data = "#!/bin/sh\nfoo"
expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH)
expected_msg = u"The commit-msg hook in {0} was not installed by gitlint ".format(expected_dst) + \
expected_msg = f"The commit-msg hook in {expected_dst} was not installed by gitlint " + \
"(or it was modified).\nUninstallation of 3th party or modified gitlint hooks " + \
"is not supported."
with patch('gitlint.hooks.io.open', mock_open(read_data=read_data), create=True):

View file

@ -1,18 +1,8 @@
# -*- coding: utf-8 -*-
try:
# python 2.x
from StringIO import StringIO
except ImportError:
# python 3.x
from io import StringIO
from io import StringIO
try:
# python 2.x
from mock import patch
except ImportError:
# python 3.x
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
from gitlint.tests.base import BaseTestCase
from gitlint.lint import GitLinter
@ -27,14 +17,14 @@ class LintTests(BaseTestCase):
gitcontext = self.gitcontext(self.get_sample("commit_message/sample1"))
violations = linter.lint(gitcontext.commits[-1])
expected_errors = [RuleViolation("T3", "Title has trailing punctuation (.)",
u"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
u"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
RuleViolation("B1", "Line exceeds max length (135>80)",
"This is the first line of the commit message body and it is meant to test " +
"a line that exceeds the maximum line length of 80 characters.", 3),
RuleViolation("B2", "Line has trailing whitespace", u"This line has a tråiling space. ", 4),
RuleViolation("B2", "Line has trailing whitespace", "This line has a tråiling space. ", 4),
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing tab.\t", 5),
RuleViolation("B3", "Line contains hard tab characters (\\t)",
"This line has a trailing tab.\t", 5)]
@ -46,7 +36,7 @@ class LintTests(BaseTestCase):
gitcontext = self.gitcontext(self.get_sample("commit_message/sample2"))
violations = linter.lint(gitcontext.commits[-1])
expected = [RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
u"Just a title contåining WIP", 1),
"Just a title contåining WIP", 1),
RuleViolation("B6", "Body message is missing", None, 3)]
self.assertListEqual(violations, expected)
@ -56,7 +46,7 @@ class LintTests(BaseTestCase):
gitcontext = self.gitcontext(self.get_sample("commit_message/sample3"))
violations = linter.lint(gitcontext.commits[-1])
title = u" Commit title containing 'WIP', \tleading and tråiling whitespace and longer than 72 characters."
title = " Commit title containing 'WIP', \tleading and tråiling whitespace and longer than 72 characters."
expected = [RuleViolation("T1", "Title exceeds max length (95>72)", title, 1),
RuleViolation("T3", "Title has trailing punctuation (.)", title, 1),
RuleViolation("T4", "Title contains hard tab characters (\\t)", title, 1),
@ -64,12 +54,12 @@ class LintTests(BaseTestCase):
RuleViolation("T6", "Title has leading whitespace", title, 1),
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
RuleViolation("B1", "Line exceeds max length (101>80)",
u"This is the first line is meånt to test a line that exceeds the maximum line " +
"This is the first line is meånt to test a line that exceeds the maximum line " +
"length of 80 characters.", 3),
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing space. ", 4),
RuleViolation("B2", "Line has trailing whitespace", u"This line has a tråiling tab.\t", 5),
RuleViolation("B2", "Line has trailing whitespace", "This line has a tråiling tab.\t", 5),
RuleViolation("B3", "Line contains hard tab characters (\\t)",
u"This line has a tråiling tab.\t", 5)]
"This line has a tråiling tab.\t", 5)]
self.assertListEqual(violations, expected)
@ -90,13 +80,13 @@ class LintTests(BaseTestCase):
linter = GitLinter(config_builder.build())
violations = linter.lint(commit)
title = u" Commit title containing 'WIP', \tleading and tråiling whitespace and longer than 72 characters."
title = " Commit title containing 'WIP', \tleading and tråiling whitespace and longer than 72 characters."
# expect only certain violations because sample5 has a 'gitlint-ignore: T3, T6, body-max-line-length'
expected = [RuleViolation("T1", "Title exceeds max length (95>72)", title, 1),
RuleViolation("T4", "Title contains hard tab characters (\\t)", title, 1),
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", title, 1),
RuleViolation("B4", "Second line is not empty", u"This line should be ëmpty", 2),
RuleViolation("B2", "Line has trailing whitespace", u"This line has a tråiling space. ", 4),
RuleViolation("B4", "Second line is not empty", "This line should be ëmpty", 2),
RuleViolation("B2", "Line has trailing whitespace", "This line has a tråiling space. ", 4),
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing tab.\t", 5),
RuleViolation("B3", "Line contains hard tab characters (\\t)",
"This line has a trailing tab.\t", 5)]
@ -106,11 +96,11 @@ class LintTests(BaseTestCase):
""" Lint sample2 but also add some metadata to the commit so we that gets linted as well """
linter = GitLinter(LintConfig())
gitcontext = self.gitcontext(self.get_sample("commit_message/sample2"))
gitcontext.commits[0].author_email = u"foo bår"
gitcontext.commits[0].author_email = "foo bår"
violations = linter.lint(gitcontext.commits[-1])
expected = [RuleViolation("M1", "Author email for commit is invalid", u"foo bår", None),
expected = [RuleViolation("M1", "Author email for commit is invalid", "foo bår", None),
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
u"Just a title contåining WIP", 1),
"Just a title contåining WIP", 1),
RuleViolation("B6", "Body message is missing", None, 3)]
self.assertListEqual(violations, expected)
@ -123,7 +113,7 @@ class LintTests(BaseTestCase):
expected = [RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
RuleViolation("B3", "Line contains hard tab characters (\\t)",
u"This line has a tråiling tab.\t", 5)]
"This line has a tråiling tab.\t", 5)]
self.assertListEqual(violations, expected)
@ -146,19 +136,19 @@ class LintTests(BaseTestCase):
# Normally we'd expect a B6 violation, but that one is skipped because of the specific ignore set above
expected = [RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
u"Just a title contåining WIP", 1)]
"Just a title contåining WIP", 1)]
self.assertListEqual(violations, expected)
# Test ignoring body lines
lint_config = LintConfig()
linter = GitLinter(lint_config)
lint_config.set_rule_option("I3", "regex", u"(.*)tråiling(.*)")
lint_config.set_rule_option("I3", "regex", "(.*)tråiling(.*)")
violations = linter.lint(self.gitcommit(self.get_sample("commit_message/sample1")))
expected_errors = [RuleViolation("T3", "Title has trailing punctuation (.)",
u"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
u"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
RuleViolation("B1", "Line exceeds max length (135>80)",
"This is the first line of the commit message body and it is meant to test " +
@ -171,7 +161,7 @@ class LintTests(BaseTestCase):
def test_lint_special_commit(self):
for commit_type in ["merge", "revert", "squash", "fixup"]:
commit = self.gitcommit(self.get_sample("commit_message/{0}".format(commit_type)))
commit = self.gitcommit(self.get_sample(f"commit_message/{commit_type}"))
lintconfig = LintConfig()
linter = GitLinter(lintconfig)
violations = linter.lint(commit)
@ -180,7 +170,7 @@ class LintTests(BaseTestCase):
self.assertListEqual(violations, [])
# Check that we do see violations if we disable 'ignore-merge-commits'
setattr(lintconfig, "ignore_{0}_commits".format(commit_type), False)
setattr(lintconfig, f"ignore_{commit_type}_commits", False)
linter = GitLinter(lintconfig)
violations = linter.lint(commit)
self.assertTrue(len(violations) > 0)
@ -195,7 +185,7 @@ class LintTests(BaseTestCase):
self.assertListEqual(violations, [])
# Matching regexes shouldn't be a problem
rule_regexes = [("title-match-regex", u"Tïtle$"), ("body-match-regex", u"Sïgned-Off-By: (.*)$")]
rule_regexes = [("title-match-regex", "Tïtle$"), ("body-match-regex", "Sïgned-Off-By: (.*)$")]
for rule_regex in rule_regexes:
lintconfig.set_rule_option(rule_regex[0], "regex", rule_regex[1])
violations = linter.lint(commit)
@ -203,16 +193,16 @@ class LintTests(BaseTestCase):
# Non-matching regexes should return violations
rule_regexes = [("title-match-regex", ), ("body-match-regex",)]
lintconfig.set_rule_option("title-match-regex", "regex", u"^Tïtle")
lintconfig.set_rule_option("body-match-regex", "regex", u"Sügned-Off-By: (.*)$")
expected_violations = [RuleViolation("T7", u"Title does not match regex (^Tïtle)", u"Normal Commit Tïtle", 1),
RuleViolation("B8", u"Body does not match regex (Sügned-Off-By: (.*)$)", None, 6)]
lintconfig.set_rule_option("title-match-regex", "regex", "^Tïtle")
lintconfig.set_rule_option("body-match-regex", "regex", "Sügned-Off-By: (.*)$")
expected_violations = [RuleViolation("T7", "Title does not match regex (^Tïtle)", "Normal Commit Tïtle", 1),
RuleViolation("B8", "Body does not match regex (Sügned-Off-By: (.*)$)", None, 6)]
violations = linter.lint(commit)
self.assertListEqual(violations, expected_violations)
def test_print_violations(self):
violations = [RuleViolation("RULE_ID_1", u"Error Messåge 1", "Violating Content 1", None),
RuleViolation("RULE_ID_2", "Error Message 2", u"Violåting Content 2", 2)]
violations = [RuleViolation("RULE_ID_1", "Error Messåge 1", "Violating Content 1", None),
RuleViolation("RULE_ID_2", "Error Message 2", "Violåting Content 2", 2)]
linter = GitLinter(LintConfig())
# test output with increasing verbosity
@ -224,54 +214,54 @@ class LintTests(BaseTestCase):
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
linter.config.verbosity = 1
linter.print_violations(violations)
expected = u"-: RULE_ID_1\n2: RULE_ID_2\n"
expected = "-: RULE_ID_1\n2: RULE_ID_2\n"
self.assertEqual(expected, stderr.getvalue())
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
linter.config.verbosity = 2
linter.print_violations(violations)
expected = u"-: RULE_ID_1 Error Messåge 1\n2: RULE_ID_2 Error Message 2\n"
expected = "-: RULE_ID_1 Error Messåge 1\n2: RULE_ID_2 Error Message 2\n"
self.assertEqual(expected, stderr.getvalue())
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
linter.config.verbosity = 3
linter.print_violations(violations)
expected = u"-: RULE_ID_1 Error Messåge 1: \"Violating Content 1\"\n" + \
u"2: RULE_ID_2 Error Message 2: \"Violåting Content 2\"\n"
expected = "-: RULE_ID_1 Error Messåge 1: \"Violating Content 1\"\n" + \
"2: RULE_ID_2 Error Message 2: \"Violåting Content 2\"\n"
self.assertEqual(expected, stderr.getvalue())
def test_named_rules(self):
""" Test that when named rules are present, both them and the original (non-named) rules executed """
lint_config = LintConfig()
for rule_name in [u"my-ïd", u"another-rule-ïd"]:
for rule_name in ["my-ïd", "another-rule-ïd"]:
rule_id = TitleMustNotContainWord.id + ":" + rule_name
lint_config.rules.add_rule(TitleMustNotContainWord, rule_id)
lint_config.set_rule_option(rule_id, "words", [u"Föo"])
lint_config.set_rule_option(rule_id, "words", ["Föo"])
linter = GitLinter(lint_config)
violations = [RuleViolation("T5", u"Title contains the word 'WIP' (case-insensitive)", u"WIP: Föo bar", 1),
RuleViolation(u"T5:another-rule-ïd", u"Title contains the word 'Föo' (case-insensitive)",
u"WIP: Föo bar", 1),
RuleViolation(u"T5:my-ïd", u"Title contains the word 'Föo' (case-insensitive)",
u"WIP: Föo bar", 1)]
self.assertListEqual(violations, linter.lint(self.gitcommit(u"WIP: Föo bar\n\nFoo bår hur dur bla bla")))
violations = [RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", "WIP: Föo bar", 1),
RuleViolation("T5:another-rule-ïd", "Title contains the word 'Föo' (case-insensitive)",
"WIP: Föo bar", 1),
RuleViolation("T5:my-ïd", "Title contains the word 'Föo' (case-insensitive)",
"WIP: Föo bar", 1)]
self.assertListEqual(violations, linter.lint(self.gitcommit("WIP: Föo bar\n\nFoo bår hur dur bla bla")))
def test_ignore_named_rules(self):
""" Test that named rules can be ignored """
# Add named rule to lint config
config_builder = LintConfigBuilder()
rule_id = TitleMustNotContainWord.id + u":my-ïd"
config_builder.set_option(rule_id, "words", [u"Föo"])
rule_id = TitleMustNotContainWord.id + ":my-ïd"
config_builder.set_option(rule_id, "words", ["Föo"])
lint_config = config_builder.build()
linter = GitLinter(lint_config)
commit = self.gitcommit(u"WIP: Föo bar\n\nFoo bår hur dur bla bla")
commit = self.gitcommit("WIP: Föo bar\n\nFoo bår hur dur bla bla")
# By default, we expect both the violations of the regular rule as well as the named rule to show up
violations = [RuleViolation("T5", u"Title contains the word 'WIP' (case-insensitive)", u"WIP: Föo bar", 1),
RuleViolation(u"T5:my-ïd", u"Title contains the word 'Föo' (case-insensitive)",
u"WIP: Föo bar", 1)]
violations = [RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", "WIP: Föo bar", 1),
RuleViolation("T5:my-ïd", "Title contains the word 'Föo' (case-insensitive)",
"WIP: Föo bar", 1)]
self.assertListEqual(violations, linter.lint(commit))
# ignore regular rule: only named rule violations show up
@ -283,5 +273,5 @@ class LintTests(BaseTestCase):
self.assertListEqual(violations[:-1], linter.lint(commit))
# ignore named rule by name: only regular rule violations show up
lint_config.ignore = [TitleMustNotContainWord.name + u":my-ïd"]
lint_config.ignore = [TitleMustNotContainWord.name + ":my-ïd"]
self.assertListEqual(violations[:-1], linter.lint(commit))

View file

@ -9,25 +9,25 @@ from gitlint.options import IntOption, BoolOption, StrOption, ListOption, PathOp
class RuleOptionTests(BaseTestCase):
def test_option_equality(self):
options = {IntOption: 123, StrOption: u"foöbar", BoolOption: False, ListOption: ["a", "b"],
PathOption: ".", RegexOption: u"^foöbar(.*)"}
options = {IntOption: 123, StrOption: "foöbar", BoolOption: False, ListOption: ["a", "b"],
PathOption: ".", RegexOption: "^foöbar(.*)"}
for clazz, val in options.items():
# 2 options are equal if their name, value and description match
option1 = clazz(u"test-öption", val, u"Test Dëscription")
option2 = clazz(u"test-öption", val, u"Test Dëscription")
option1 = clazz("test-öption", val, "Test Dëscription")
option2 = clazz("test-öption", val, "Test Dëscription")
self.assertEqual(option1, option2)
# Not equal: class, name, description, value are different
self.assertNotEqual(option1, IntOption(u"tëst-option1", 123, u"Test Dëscription"))
self.assertNotEqual(option1, StrOption(u"tëst-option1", u"åbc", u"Test Dëscription"))
self.assertNotEqual(option1, StrOption(u"tëst-option", u"åbcd", u"Test Dëscription"))
self.assertNotEqual(option1, StrOption(u"tëst-option", u"åbc", u"Test Dëscription2"))
self.assertNotEqual(option1, IntOption("tëst-option1", 123, "Test Dëscription"))
self.assertNotEqual(option1, StrOption("tëst-option1", "åbc", "Test Dëscription"))
self.assertNotEqual(option1, StrOption("tëst-option", "åbcd", "Test Dëscription"))
self.assertNotEqual(option1, StrOption("tëst-option", "åbc", "Test Dëscription2"))
def test_int_option(self):
# normal behavior
option = IntOption(u"tëst-name", 123, u"Tëst Description")
self.assertEqual(option.name, u"tëst-name")
self.assertEqual(option.description, u"Tëst Description")
option = IntOption("tëst-name", 123, "Tëst Description")
self.assertEqual(option.name, "tëst-name")
self.assertEqual(option.description, "Tëst Description")
self.assertEqual(option.value, 123)
# re-set value
@ -39,12 +39,12 @@ class RuleOptionTests(BaseTestCase):
self.assertEqual(option.value, None)
# error on negative int when not allowed
expected_error = u"Option 'tëst-name' must be a positive integer (current value: '-123')"
expected_error = "Option 'tëst-name' must be a positive integer (current value: '-123')"
with self.assertRaisesMessage(RuleOptionError, expected_error):
option.set(-123)
# error on non-int value
expected_error = u"Option 'tëst-name' must be a positive integer (current value: 'foo')"
expected_error = "Option 'tëst-name' must be a positive integer (current value: 'foo')"
with self.assertRaisesMessage(RuleOptionError, expected_error):
option.set("foo")
@ -54,20 +54,20 @@ class RuleOptionTests(BaseTestCase):
self.assertEqual(option.value, -456)
# error on non-int value when negative int is allowed
expected_error = u"Option 'test-name' must be an integer (current value: 'foo')"
expected_error = "Option 'test-name' must be an integer (current value: 'foo')"
with self.assertRaisesMessage(RuleOptionError, expected_error):
option.set("foo")
def test_str_option(self):
# normal behavior
option = StrOption(u"tëst-name", u"föo", u"Tëst Description")
self.assertEqual(option.name, u"tëst-name")
self.assertEqual(option.description, u"Tëst Description")
self.assertEqual(option.value, u"föo")
option = StrOption("tëst-name", "föo", "Tëst Description")
self.assertEqual(option.name, "tëst-name")
self.assertEqual(option.description, "Tëst Description")
self.assertEqual(option.value, "föo")
# re-set value
option.set(u"bår")
self.assertEqual(option.value, u"bår")
option.set("bår")
self.assertEqual(option.value, "bår")
# conversion to str
option.set(123)
@ -83,9 +83,9 @@ class RuleOptionTests(BaseTestCase):
def test_boolean_option(self):
# normal behavior
option = BoolOption(u"tëst-name", "true", u"Tëst Description")
self.assertEqual(option.name, u"tëst-name")
self.assertEqual(option.description, u"Tëst Description")
option = BoolOption("tëst-name", "true", "Tëst Description")
self.assertEqual(option.name, "tëst-name")
self.assertEqual(option.description, "Tëst Description")
self.assertEqual(option.value, True)
# re-set value
@ -97,25 +97,25 @@ class RuleOptionTests(BaseTestCase):
self.assertEqual(option.value, True)
# error on incorrect value
incorrect_values = [1, -1, "foo", u"bår", ["foo"], {'foo': "bar"}, None]
incorrect_values = [1, -1, "foo", "bår", ["foo"], {'foo': "bar"}, None]
for value in incorrect_values:
with self.assertRaisesMessage(RuleOptionError, u"Option 'tëst-name' must be either 'true' or 'false'"):
with self.assertRaisesMessage(RuleOptionError, "Option 'tëst-name' must be either 'true' or 'false'"):
option.set(value)
def test_list_option(self):
# normal behavior
option = ListOption(u"tëst-name", u"å,b,c,d", u"Tëst Description")
self.assertEqual(option.name, u"tëst-name")
self.assertEqual(option.description, u"Tëst Description")
self.assertListEqual(option.value, [u"å", u"b", u"c", u"d"])
option = ListOption("tëst-name", "å,b,c,d", "Tëst Description")
self.assertEqual(option.name, "tëst-name")
self.assertEqual(option.description, "Tëst Description")
self.assertListEqual(option.value, ["å", "b", "c", "d"])
# re-set value
option.set(u"1,2,3,4")
self.assertListEqual(option.value, [u"1", u"2", u"3", u"4"])
option.set("1,2,3,4")
self.assertListEqual(option.value, ["1", "2", "3", "4"])
# set list
option.set([u"foo", u"bår", u"test"])
self.assertListEqual(option.value, [u"foo", u"bår", u"test"])
option.set(["foo", "bår", "test"])
self.assertListEqual(option.value, ["foo", "bår", "test"])
# None
option.set(None)
@ -134,8 +134,8 @@ class RuleOptionTests(BaseTestCase):
self.assertListEqual(option.value, [])
# trailing comma
option.set(u"ë,f,g,")
self.assertListEqual(option.value, [u"ë", u"f", u"g"])
option.set("ë,f,g,")
self.assertListEqual(option.value, ["ë", "f", "g"])
# leading and trailing whitespace should be trimmed, but only deduped within text
option.set(" abc , def , ghi \t , jkl mno ")
@ -150,11 +150,11 @@ class RuleOptionTests(BaseTestCase):
self.assertListEqual(option.value, ["123"])
def test_path_option(self):
option = PathOption(u"tëst-directory", ".", u"Tëst Description", type=u"dir")
self.assertEqual(option.name, u"tëst-directory")
self.assertEqual(option.description, u"Tëst Description")
option = PathOption("tëst-directory", ".", "Tëst Description", type="dir")
self.assertEqual(option.name, "tëst-directory")
self.assertEqual(option.description, "Tëst Description")
self.assertEqual(option.value, os.getcwd())
self.assertEqual(option.type, u"dir")
self.assertEqual(option.type, "dir")
# re-set value
option.set(self.SAMPLES_DIR)
@ -165,33 +165,32 @@ class RuleOptionTests(BaseTestCase):
self.assertIsNone(option.value)
# set to int
expected = u"Option tëst-directory must be an existing directory (current value: '1234')"
expected = "Option tëst-directory must be an existing directory (current value: '1234')"
with self.assertRaisesMessage(RuleOptionError, expected):
option.set(1234)
# set to non-existing directory
non_existing_path = os.path.join(u"/föo", u"bar")
expected = u"Option tëst-directory must be an existing directory (current value: '{0}')"
with self.assertRaisesMessage(RuleOptionError, expected.format(non_existing_path)):
non_existing_path = os.path.join("/föo", "bar")
expected = f"Option tëst-directory must be an existing directory (current value: '{non_existing_path}')"
with self.assertRaisesMessage(RuleOptionError, expected):
option.set(non_existing_path)
# set to a file, should raise exception since option.type = dir
sample_path = self.get_sample_path(os.path.join("commit_message", "sample1"))
expected = u"Option tëst-directory must be an existing directory (current value: '{0}')".format(sample_path)
expected = f"Option tëst-directory must be an existing directory (current value: '{sample_path}')"
with self.assertRaisesMessage(RuleOptionError, expected):
option.set(sample_path)
# set option.type = file, file should now be accepted, directories not
option.type = u"file"
option.type = "file"
option.set(sample_path)
self.assertEqual(option.value, sample_path)
expected = u"Option tëst-directory must be an existing file (current value: '{0}')".format(
self.get_sample_path())
expected = f"Option tëst-directory must be an existing file (current value: '{self.get_sample_path()}')"
with self.assertRaisesMessage(RuleOptionError, expected):
option.set(self.get_sample_path())
# set option.type = both, files and directories should now be accepted
option.type = u"both"
option.type = "both"
option.set(sample_path)
self.assertEqual(option.value, sample_path)
option.set(self.get_sample_path())
@ -199,27 +198,27 @@ class RuleOptionTests(BaseTestCase):
# Expect exception if path type is invalid
option.type = u'föo'
expected = u"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):
option.set("haha")
def test_regex_option(self):
# normal behavior
option = RegexOption(u"tëst-regex", u"^myrëgex(.*)foo$", u"Tëst Regex Description")
self.assertEqual(option.name, u"tëst-regex")
self.assertEqual(option.description, u"Tëst Regex Description")
self.assertEqual(option.value, re.compile(u"^myrëgex(.*)foo$", re.UNICODE))
option = RegexOption("tëst-regex", "^myrëgex(.*)foo$", "Tëst Regex Description")
self.assertEqual(option.name, "tëst-regex")
self.assertEqual(option.description, "Tëst Regex Description")
self.assertEqual(option.value, re.compile("^myrëgex(.*)foo$", re.UNICODE))
# re-set value
option.set(u"[0-9]föbar.*")
self.assertEqual(option.value, re.compile(u"[0-9]föbar.*", re.UNICODE))
option.set("[0-9]föbar.*")
self.assertEqual(option.value, re.compile("[0-9]föbar.*", re.UNICODE))
# set None
option.set(None)
self.assertIsNone(option.value)
# error on invalid regex
incorrect_values = [u"foo(", 123, -1]
incorrect_values = ["foo(", 123, -1]
for value in incorrect_values:
with self.assertRaisesRegex(RuleOptionError, u"Invalid regular expression"):
with self.assertRaisesRegex(RuleOptionError, "Invalid regular expression"):
option.set(value)

View file

@ -1,15 +1,10 @@
# -*- coding: utf-8 -*-
from unittest.mock import patch
from gitlint import utils
from gitlint.tests.base import BaseTestCase
try:
# python 2.x
from mock import patch
except ImportError:
# python 3.x
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
class UtilsTests(BaseTestCase):
@ -24,7 +19,7 @@ class UtilsTests(BaseTestCase):
self.assertEqual(utils.use_sh_library(), True)
patched_env.get.assert_called_once_with("GITLINT_USE_SH_LIB", None)
for invalid_val in ["0", u"foöbar"]:
for invalid_val in ["0", "foöbar"]:
patched_env.get.reset_mock() # reset mock call count
patched_env.get.return_value = invalid_val
self.assertEqual(utils.use_sh_library(), False, invalid_val)
@ -41,12 +36,12 @@ class UtilsTests(BaseTestCase):
@patch('gitlint.utils.locale')
def test_default_encoding_non_windows(self, mocked_locale):
utils.PLATFORM_IS_WINDOWS = False
mocked_locale.getpreferredencoding.return_value = u"foöbar"
self.assertEqual(utils.getpreferredencoding(), u"foöbar")
mocked_locale.getpreferredencoding.return_value = "foöbar"
self.assertEqual(utils.getpreferredencoding(), "foöbar")
mocked_locale.getpreferredencoding.assert_called_once()
mocked_locale.getpreferredencoding.return_value = False
self.assertEqual(utils.getpreferredencoding(), u"UTF-8")
self.assertEqual(utils.getpreferredencoding(), "UTF-8")
@patch('os.environ')
def test_default_encoding_windows(self, patched_env):
@ -60,23 +55,23 @@ class UtilsTests(BaseTestCase):
patched_env.get.side_effect = mocked_get
# Assert getpreferredencoding reads env vars in order: LC_ALL, LC_CTYPE, LANG
mock_env = {"LC_ALL": u"ASCII", "LC_CTYPE": u"UTF-16", "LANG": u"CP1251"}
self.assertEqual(utils.getpreferredencoding(), u"ASCII")
mock_env = {"LC_CTYPE": u"UTF-16", "LANG": u"CP1251"}
self.assertEqual(utils.getpreferredencoding(), u"UTF-16")
mock_env = {"LANG": u"CP1251"}
self.assertEqual(utils.getpreferredencoding(), u"CP1251")
mock_env = {"LC_ALL": "ASCII", "LC_CTYPE": "UTF-16", "LANG": "CP1251"}
self.assertEqual(utils.getpreferredencoding(), "ASCII")
mock_env = {"LC_CTYPE": "UTF-16", "LANG": "CP1251"}
self.assertEqual(utils.getpreferredencoding(), "UTF-16")
mock_env = {"LANG": "CP1251"}
self.assertEqual(utils.getpreferredencoding(), "CP1251")
# Assert split on dot
mock_env = {"LANG": u"foo.UTF-16"}
self.assertEqual(utils.getpreferredencoding(), u"UTF-16")
mock_env = {"LANG": "foo.UTF-16"}
self.assertEqual(utils.getpreferredencoding(), "UTF-16")
# assert default encoding is UTF-8
mock_env = {}
self.assertEqual(utils.getpreferredencoding(), "UTF-8")
mock_env = {"FOO": u"föo"}
mock_env = {"FOO": "föo"}
self.assertEqual(utils.getpreferredencoding(), "UTF-8")
# assert fallback encoding is UTF-8 in case we set an unavailable encoding
mock_env = {"LC_ALL": u"foo"}
self.assertEqual(utils.getpreferredencoding(), u"UTF-8")
mock_env = {"LC_ALL": "foo"}
self.assertEqual(utils.getpreferredencoding(), "UTF-8")

View file

@ -1,7 +1,6 @@
# pylint: disable=bad-option-value,unidiomatic-typecheck,undefined-variable,no-else-return
import codecs
import platform
import sys
import os
import locale
@ -24,16 +23,6 @@ def platform_is_windows():
PLATFORM_IS_WINDOWS = platform_is_windows()
########################################################################################################################
# IS_PY2
def is_py2():
return sys.version_info[0] == 2
IS_PY2 = is_py2()
########################################################################################################################
# USE_SH_LIB
# Determine whether to use the `sh` library
@ -90,39 +79,3 @@ def getpreferredencoding():
DEFAULT_ENCODING = getpreferredencoding()
########################################################################################################################
# Unicode utility functions
def ustr(obj):
""" Python 2 and 3 utility method that converts an obj to unicode in python 2 and to a str object in python 3"""
if IS_PY2:
# If we are getting a string, then do an explicit decode
# else, just call the unicode method of the object
if type(obj) in [str, basestring]: # pragma: no cover # noqa
return unicode(obj, DEFAULT_ENCODING) # pragma: no cover # noqa
else:
return unicode(obj) # pragma: no cover # noqa
else:
if type(obj) in [bytes]:
return obj.decode(DEFAULT_ENCODING)
else:
return str(obj)
def sstr(obj):
""" Python 2 and 3 utility method that converts an obj to a DEFAULT_ENCODING encoded string in python 2
and to unicode in python 3.
Especially useful for implementing __str__ methods in python 2: http://stackoverflow.com/a/1307210/381010"""
if IS_PY2:
# For lists and tuples in python2, remove unicode string representation characters.
# i.e. ensure lists are printed as ['a', 'b'] and not [u'a', u'b']
if type(obj) in [list]:
return [sstr(item) for item in obj] # pragma: no cover # noqa
elif type(obj) in [tuple]:
return tuple(sstr(item) for item in obj) # pragma: no cover # noqa
return unicode(obj).encode(DEFAULT_ENCODING) # pragma: no cover # noqa
else:
return obj # pragma: no cover

View file

@ -2,6 +2,7 @@ site_name: Gitlint
site_description: Linting for your git commit messages
site_url: http://jorisroovers.github.io/gitlint/
repo_url: https://github.com/jorisroovers/gitlint
edit_uri: edit/main/docs
nav:
- Home: index.md
- Configuration: configuration.md

View file

@ -10,18 +10,13 @@ import sys
import tempfile
from datetime import datetime
from uuid import uuid4
from unittest import TestCase
import arrow
try:
# python 2.x
from unittest2 import TestCase
except ImportError:
# python 3.x
from unittest import TestCase
from qa.shell import git, gitlint, RunningCommand
from qa.utils import DEFAULT_ENCODING, ustr
from qa.utils import DEFAULT_ENCODING
class BaseTestCase(TestCase):
@ -56,13 +51,14 @@ class BaseTestCase(TestCase):
def assertEqualStdout(self, output, expected): # pylint: disable=invalid-name
self.assertIsInstance(output, RunningCommand)
output = ustr(output.stdout)
output = output.stdout.decode(DEFAULT_ENCODING)
output = output.replace('\r', '')
self.assertMultiLineEqual(output, expected)
@classmethod
def generate_temp_path(cls):
return os.path.realpath("/tmp/gitlint-test-{0}".format(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}")
@classmethod
def create_tmp_git_repo(cls):
@ -89,7 +85,7 @@ class BaseTestCase(TestCase):
@staticmethod
def create_file(parent_dir):
""" Creates a file inside a passed directory. Returns filename."""
test_filename = u"test-fïle-" + str(uuid4())
test_filename = "test-fïle-" + str(uuid4())
io.open(os.path.join(parent_dir, test_filename), 'a', encoding=DEFAULT_ENCODING).close()
return test_filename
@ -171,8 +167,8 @@ class BaseTestCase(TestCase):
@staticmethod
def get_system_info_dict():
""" Returns a dict with items related to system values logged by `gitlint --debug` """
expected_gitlint_version = gitlint("--version").replace("gitlint, version ", "").replace("\n", "")
expected_git_version = git("--version").replace("\n", "")
expected_gitlint_version = gitlint("--version").replace("gitlint, version ", "").strip()
expected_git_version = git("--version").strip()
return {'platform': platform.platform(), 'python_version': sys.version,
'git_version': expected_git_version, 'gitlint_version': expected_gitlint_version,
'GITLINT_USE_SH_LIB': BaseTestCase.GITLINT_USE_SH_LIB, 'DEFAULT_ENCODING': DEFAULT_ENCODING}

View file

@ -1,4 +1,4 @@
sh==1.12.14
pytest==4.6.3;
arrow==0.15.5;
sh==1.14.1
pytest==6.1.2;
arrow==0.17.0;
gitlint # no version as you want to test the currently installed version

View file

@ -2,18 +2,17 @@
from gitlint.rules import CommitRule, RuleViolation, ConfigurationRule
from gitlint.options import IntOption, StrOption, ListOption
from gitlint.utils import sstr
class GitContextRule(CommitRule):
""" Rule that tests whether we can correctly access certain gitcontext properties """
name = u"gïtcontext"
name = "gïtcontext"
id = "UC1"
def validate(self, commit):
violations = [
RuleViolation(self.id, u"GitContext.current_branch: {0}".format(commit.context.current_branch), line_nr=1),
RuleViolation(self.id, u"GitContext.commentchar: {0}".format(commit.context.commentchar), line_nr=1)
RuleViolation(self.id, f"GitContext.current_branch: {commit.context.current_branch}", line_nr=1),
RuleViolation(self.id, f"GitContext.commentchar: {commit.context.commentchar}", line_nr=1)
]
return violations
@ -21,13 +20,13 @@ class GitContextRule(CommitRule):
class GitCommitRule(CommitRule):
""" Rule that tests whether we can correctly access certain commit properties """
name = u"gïtcommit"
name = "gïtcommit"
id = "UC2"
def validate(self, commit):
violations = [
RuleViolation(self.id, u"GitCommit.branches: {0}".format(sstr(commit.branches)), line_nr=1),
RuleViolation(self.id, u"GitCommit.custom_prop: {0}".format(commit.custom_prop), line_nr=1),
RuleViolation(self.id, f"GitCommit.branches: {commit.branches}", line_nr=1),
RuleViolation(self.id, f"GitCommit.custom_prop: {commit.custom_prop}", line_nr=1),
]
return violations
@ -35,16 +34,16 @@ class GitCommitRule(CommitRule):
class GitlintConfigurationRule(ConfigurationRule):
""" Rule that tests whether we can correctly access the config as well as modify the commit message """
name = u"cönfigrule"
name = "cönfigrule"
id = "UC3"
def apply(self, config, commit):
# We add a line to the commit message body that pulls a value from config, this proves we can modify the body
# and read the config contents
commit.message.body.append("{0} ".format(config.target)) # trailing whitespace deliberate to trigger violation
commit.message.body.append(f"{config.target} ") # trailing whitespace deliberate to trigger violation
# We set a custom property that we access in CommitRule, to prove we can add extra properties to the commit
commit.custom_prop = u"foöbar"
commit.custom_prop = "foöbar"
# We also ignore some extra rules, proving that we can modify the config
config.ignore.append("B4")
@ -52,18 +51,18 @@ class GitlintConfigurationRule(ConfigurationRule):
class ConfigurableCommitRule(CommitRule):
""" Rule that tests that we can add configuration to user-defined rules """
name = u"configürable"
name = "configürable"
id = "UC4"
options_spec = [IntOption(u"int-öption", 2, u"int-öption description"),
StrOption(u"str-öption", u"föo", u"int-öption description"),
ListOption(u"list-öption", [u"foo", u"bar"], u"list-öption description")]
options_spec = [IntOption("int-öption", 2, "int-öption description"),
StrOption("str-öption", "föo", "int-öption description"),
ListOption("list-öption", ["foo", "bar"], "list-öption description")]
def validate(self, _):
violations = [
RuleViolation(self.id, u"int-öption: {0}".format(self.options[u'int-öption'].value), line_nr=1),
RuleViolation(self.id, u"str-öption: {0}".format(self.options[u'str-öption'].value), line_nr=1),
RuleViolation(self.id, u"list-öption: {0}".format(sstr(self.options[u'list-öption'].value)), line_nr=1),
RuleViolation(self.id, f"int-öption: {self.options[u'int-öption'].value}", line_nr=1),
RuleViolation(self.id, f"str-öption: {self.options[u'str-öption'].value}", line_nr=1),
RuleViolation(self.id, f"list-öption: {self.options[u'list-öption'].value}", line_nr=1),
]
return violations

View file

@ -3,10 +3,11 @@
# on gitlint internals for our integration testing framework.
import subprocess
from qa.utils import ustr, USE_SH_LIB, IS_PY2
from qa.utils import USE_SH_LIB, DEFAULT_ENCODING
if USE_SH_LIB:
from sh import git, echo, gitlint # pylint: disable=unused-import,no-name-in-module,import-error
gitlint = gitlint.bake(_unify_ttys=True, _tty_in=True) # pylint: disable=invalid-name
# import exceptions separately, this makes it a little easier to mock them out in the unit tests
from sh import CommandNotFound, ErrorReturnCode, RunningCommand # pylint: disable=import-error
@ -16,7 +17,7 @@ else:
""" Exception indicating a command was not found during execution """
pass
class RunningCommand(object):
class RunningCommand:
pass
class ShResult(RunningCommand):
@ -27,7 +28,7 @@ else:
self.full_cmd = full_cmd
# TODO(jorisroovers): The 'sh' library by default will merge stdout and stderr. We mimic this behavior
# for now until we fully remove the 'sh' library.
self.stdout = stdout + ustr(stderr)
self.stdout = stdout + stderr.decode(DEFAULT_ENCODING)
self.stderr = stderr
self.exit_code = exitcode
@ -55,14 +56,9 @@ else:
# a non-zero exit code -> just return the entire result
if hasattr(result, 'exit_code') and result.exit_code > 0:
return result
return ustr(result)
return str(result)
def _exec(*args, **kwargs):
if IS_PY2:
no_command_error = OSError # noqa pylint: disable=undefined-variable,invalid-name
else:
no_command_error = FileNotFoundError # noqa pylint: disable=undefined-variable
pipe = subprocess.PIPE
popen_kwargs = {'stdout': pipe, 'stderr': pipe, 'shell': kwargs.get('_tty_out', False)}
if '_cwd' in kwargs:
@ -73,11 +69,11 @@ else:
try:
p = subprocess.Popen(args, **popen_kwargs)
result = p.communicate()
except no_command_error:
raise CommandNotFound
except FileNotFoundError as exc:
raise CommandNotFound from exc
exit_code = p.returncode
stdout = ustr(result[0])
stdout = result[0].decode(DEFAULT_ENCODING)
stderr = result[1] # 'sh' does not decode the stderr bytes to unicode
full_cmd = '' if args is None else ' '.join(args)

View file

@ -6,7 +6,6 @@ import arrow
from qa.shell import echo, git, gitlint
from qa.base import BaseTestCase
from qa.utils import sstr
class CommitsTests(BaseTestCase):
@ -16,10 +15,10 @@ class CommitsTests(BaseTestCase):
def test_successful(self):
""" Test linting multiple commits without violations """
git("checkout", "-b", "test-branch-commits-base", _cwd=self.tmp_git_repo)
self.create_simple_commit(u"Sïmple title\n\nSimple bödy describing the commit")
self.create_simple_commit("Sïmple title\n\nSimple bödy describing the commit")
git("checkout", "-b", "test-branch-commits", _cwd=self.tmp_git_repo)
self.create_simple_commit(u"Sïmple title2\n\nSimple bödy describing the commit2")
self.create_simple_commit(u"Sïmple title3\n\nSimple bödy describing the commit3")
self.create_simple_commit("Sïmple title2\n\nSimple bödy describing the commit2")
self.create_simple_commit("Sïmple title3\n\nSimple bödy describing the commit3")
output = gitlint("--commits", "test-branch-commits-base...test-branch-commits",
_cwd=self.tmp_git_repo, _tty_in=True)
self.assertEqualStdout(output, "")
@ -27,12 +26,12 @@ class CommitsTests(BaseTestCase):
def test_violations(self):
""" Test linting multiple commits with violations """
git("checkout", "-b", "test-branch-commits-violations-base", _cwd=self.tmp_git_repo)
self.create_simple_commit(u"Sïmple title.\n")
self.create_simple_commit("Sïmple title.\n")
git("checkout", "-b", "test-branch-commits-violations", _cwd=self.tmp_git_repo)
self.create_simple_commit(u"Sïmple title2.\n")
self.create_simple_commit("Sïmple title2.\n")
commit_sha1 = self.get_last_commit_hash()[:10]
self.create_simple_commit(u"Sïmple title3.\n")
self.create_simple_commit("Sïmple title3.\n")
commit_sha2 = self.get_last_commit_hash()[:10]
output = gitlint("--commits", "test-branch-commits-violations-base...test-branch-commits-violations",
_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[4])
@ -43,14 +42,14 @@ class CommitsTests(BaseTestCase):
def test_lint_single_commit(self):
""" Tests `gitlint --commits <sha>` """
self.create_simple_commit(u"Sïmple title.\n")
self.create_simple_commit(u"Sïmple title2.\n")
self.create_simple_commit("Sïmple title.\n")
self.create_simple_commit("Sïmple title2.\n")
commit_sha = self.get_last_commit_hash()
refspec = "{0}^...{0}".format(commit_sha)
self.create_simple_commit(u"Sïmple title3.\n")
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 = (u"1: T3 Title has trailing punctuation (.): \"Sïmple title2.\"\n" +
u"3: B6 Body message is missing\n")
expected = ("1: T3 Title has trailing punctuation (.): \"Sïmple title2.\"\n" +
"3: B6 Body message is missing\n")
self.assertEqual(output.exit_code, 2)
self.assertEqualStdout(output, expected)
@ -61,7 +60,7 @@ class CommitsTests(BaseTestCase):
echo "WIP: Pïpe test." | gitlint --staged --debug
"""
# Create a commit first, before we stage changes. This ensures the repo is properly initialized.
self.create_simple_commit(u"Sïmple title.\n")
self.create_simple_commit("Sïmple title.\n")
# Add some files, stage them: they should show up in the debug output as changed file
filename1 = self.create_file(self.tmp_git_repo)
@ -69,12 +68,12 @@ class CommitsTests(BaseTestCase):
filename2 = self.create_file(self.tmp_git_repo)
git("add", filename2, _cwd=self.tmp_git_repo)
output = gitlint(echo(u"WIP: Pïpe test."), "--staged", "--debug",
output = gitlint(echo("WIP: Pïpe test."), "--staged", "--debug",
_cwd=self.tmp_git_repo, _tty_in=False, _err_to_out=True, _ok_code=[3])
# Determine variable parts of expected output
expected_kwargs = self.get_debug_vars_last_commit()
expected_kwargs.update({'changed_files': sstr(sorted([filename1, filename2]))})
expected_kwargs.update({'changed_files': sorted([filename1, filename2])})
# It's not really possible to determine the "Date: ..." line that is part of the debug output as this date
# is not taken from git but instead generated by gitlint itself. As a workaround, we extract the date from the
@ -95,7 +94,7 @@ class CommitsTests(BaseTestCase):
gitlint --msg-filename /tmp/my-commit-msg --staged --debug
"""
# Create a commit first, before we stage changes. This ensures the repo is properly initialized.
self.create_simple_commit(u"Sïmple title.\n")
self.create_simple_commit("Sïmple title.\n")
# Add some files, stage them: they should show up in the debug output as changed file
filename1 = self.create_file(self.tmp_git_repo)
@ -103,14 +102,14 @@ class CommitsTests(BaseTestCase):
filename2 = self.create_file(self.tmp_git_repo)
git("add", filename2, _cwd=self.tmp_git_repo)
tmp_commit_msg_file = self.create_tmpfile(u"WIP: from fïle test.")
tmp_commit_msg_file = self.create_tmpfile("WIP: from fïle test.")
output = gitlint("--msg-filename", tmp_commit_msg_file, "--staged", "--debug",
_cwd=self.tmp_git_repo, _tty_in=False, _err_to_out=True, _ok_code=[3])
# Determine variable parts of expected output
expected_kwargs = self.get_debug_vars_last_commit()
expected_kwargs.update({'changed_files': sstr(sorted([filename1, filename2]))})
expected_kwargs.update({'changed_files': sorted([filename1, filename2])})
# It's not really possible to determine the "Date: ..." line that is part of the debug output as this date
# is not taken from git but instead generated by gitlint itself. As a workaround, we extract the date from the
@ -128,9 +127,9 @@ class CommitsTests(BaseTestCase):
def test_lint_head(self):
""" Testing whether we can also recognize special refs like 'HEAD' """
tmp_git_repo = self.create_tmp_git_repo()
self.create_simple_commit(u"Sïmple title.\n\nSimple bödy describing the commit", git_repo=tmp_git_repo)
self.create_simple_commit(u"Sïmple title", git_repo=tmp_git_repo)
self.create_simple_commit(u"WIP: 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)
self.create_simple_commit("Sïmple title", git_repo=tmp_git_repo)
self.create_simple_commit("WIP: Sïmple title\n\nSimple bödy describing the commit", git_repo=tmp_git_repo)
output = gitlint("--commits", "HEAD", _cwd=tmp_git_repo, _tty_in=True, _ok_code=[3])
revlist = git("rev-list", "HEAD", _tty_in=True, _cwd=tmp_git_repo).split()
@ -143,14 +142,14 @@ class CommitsTests(BaseTestCase):
""" Tests multiple commits of which some rules get igonored because of ignore-* rules """
# Create repo and some commits
tmp_git_repo = self.create_tmp_git_repo()
self.create_simple_commit(u"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)
# Normally, this commit will give T3 (trailing-punctuation), T5 (WIP) and B5 (bod-too-short) violations
# But in this case only B5 because T3 and T5 are being ignored because of config
self.create_simple_commit(u"Release: WIP tïtle.\n\nShort", git_repo=tmp_git_repo)
self.create_simple_commit("Release: WIP tïtle.\n\nShort", git_repo=tmp_git_repo)
# In the following 2 commits, the T3 violations are as normal
self.create_simple_commit(
u"Sïmple WIP title3.\n\nThis is \ta relëase commit\nMore info", git_repo=tmp_git_repo)
self.create_simple_commit(u"Sïmple title4.\n\nSimple bödy describing the commit4", git_repo=tmp_git_repo)
"Sïmple WIP title3.\n\nThis is \ta relëase commit\nMore info", git_repo=tmp_git_repo)
self.create_simple_commit("Sïmple title4.\n\nSimple bödy describing the commit4", git_repo=tmp_git_repo)
revlist = git("rev-list", "HEAD", _tty_in=True, _cwd=tmp_git_repo).split()
config_path = self.get_sample_path("config/ignore-release-commits")

View file

@ -5,30 +5,30 @@ import re
from qa.shell import gitlint
from qa.base import BaseTestCase
from qa.utils import sstr, ustr
from qa.utils import DEFAULT_ENCODING
class ConfigTests(BaseTestCase):
""" Integration tests for gitlint configuration and configuration precedence. """
def test_ignore_by_id(self):
self.create_simple_commit(u"WIP: Thïs is a title.\nContënt on the second line")
self.create_simple_commit("WIP: Thïs is a title.\nContënt on the second line")
output = gitlint("--ignore", "T5,B4", _tty_in=True, _cwd=self.tmp_git_repo, _ok_code=[1])
expected = u"1: T3 Title has trailing punctuation (.): \"WIP: Thïs is a title.\"\n"
expected = "1: T3 Title has trailing punctuation (.): \"WIP: Thïs is a title.\"\n"
self.assertEqualStdout(output, expected)
def test_ignore_by_name(self):
self.create_simple_commit(u"WIP: Thïs is a title.\nContënt on the second line")
self.create_simple_commit("WIP: Thïs is a title.\nContënt on the second line")
output = gitlint("--ignore", "title-must-not-contain-word,body-first-line-empty",
_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1])
expected = u"1: T3 Title has trailing punctuation (.): \"WIP: Thïs is a title.\"\n"
expected = "1: T3 Title has trailing punctuation (.): \"WIP: Thïs is a title.\"\n"
self.assertEqualStdout(output, expected)
def test_verbosity(self):
self.create_simple_commit(u"WIP: Thïs is a title.\nContënt on the second line")
self.create_simple_commit("WIP: Thïs is a title.\nContënt on the second line")
output = gitlint("-v", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3])
expected = u"1: T3\n1: T5\n2: B4\n"
expected = "1: T3\n1: T5\n2: B4\n"
self.assertEqualStdout(output, expected)
output = gitlint("-vv", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3])
@ -42,12 +42,12 @@ class ConfigTests(BaseTestCase):
self.assertEqualStdout(output, "")
def test_set_rule_option(self):
self.create_simple_commit(u"This ïs a title.")
self.create_simple_commit("This ïs a title.")
output = gitlint("-c", "title-max-length.line-length=5", _tty_in=True, _cwd=self.tmp_git_repo, _ok_code=[3])
self.assertEqualStdout(output, self.get_expected("test_config/test_set_rule_option_1"))
def test_config_from_file(self):
commit_msg = u"WIP: Thïs is a title thåt is a bit longer.\nContent on the second line\n" + \
commit_msg = "WIP: Thïs is a title thåt is a bit longer.\nContent on the second line\n" + \
"This line of the body is here because we need it"
self.create_simple_commit(commit_msg)
config_path = self.get_sample_path("config/gitlintconfig")
@ -58,14 +58,14 @@ class ConfigTests(BaseTestCase):
# Test both on existing and new repo (we've had a bug in the past that was unique to empty repos)
repos = [self.tmp_git_repo, self.create_tmp_git_repo()]
for target_repo in repos:
commit_msg = u"WIP: Thïs is a title thåt is a bit longer.\nContent on the second line\n" + \
commit_msg = "WIP: Thïs is a title thåt is a bit longer.\nContent on the second line\n" + \
"This line of the body is here because we need it"
filename = self.create_simple_commit(commit_msg, git_repo=target_repo)
config_path = self.get_sample_path("config/gitlintconfig")
output = gitlint("--config", config_path, "--debug", _cwd=target_repo, _tty_in=True, _ok_code=[5])
expected_kwargs = self.get_debug_vars_last_commit(git_repo=target_repo)
expected_kwargs.update({'config_path': config_path, 'changed_files': sstr([filename])})
expected_kwargs.update({'config_path': config_path, 'changed_files': [filename]})
self.assertEqualStdout(output, self.get_expected("test_config/test_config_from_file_debug_1",
expected_kwargs))
@ -75,7 +75,7 @@ class ConfigTests(BaseTestCase):
# We invoke gitlint, configuring it via env variables, we can check whether gitlint picks these up correctly
# by comparing the debug output with what we'd expect
target_repo = self.create_tmp_git_repo()
commit_msg = u"WIP: Thïs is a title thåt is a bit longer.\nContent on the second line\n" + \
commit_msg = "WIP: Thïs is a title thåt is a bit longer.\nContent on the second line\n" + \
"This line of the body is here because we need it"
filename = self.create_simple_commit(commit_msg, git_repo=target_repo)
env = self.create_environment({"GITLINT_DEBUG": "1", "GITLINT_VERBOSITY": "2",
@ -84,12 +84,12 @@ class ConfigTests(BaseTestCase):
"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)
expected_kwargs.update({'changed_files': sstr([filename])})
expected_kwargs.update({'changed_files': [filename]})
self.assertEqualStdout(output, self.get_expected("test_config/test_config_from_env_1", expected_kwargs))
# For some env variables, we need a separate test ast they are mutually exclusive with the ones tested above
tmp_commit_msg_file = self.create_tmpfile(u"WIP: msg-fïlename test.")
tmp_commit_msg_file = self.create_tmpfile("WIP: msg-fïlename test.")
env = self.create_environment({"GITLINT_DEBUG": "1", "GITLINT_TARGET": target_repo,
"GITLINT_SILENT": "1", "GITLINT_STAGED": "1"})
@ -99,7 +99,7 @@ class ConfigTests(BaseTestCase):
# Extract date from actual output to insert it into the expected output
# We have to do this since there's no way for us to deterministically know that date otherwise
p = re.compile("Date: (.*)\n", re.UNICODE | re.MULTILINE)
result = p.search(ustr(output.stdout))
result = p.search(output.stdout.decode(DEFAULT_ENCODING))
date = result.group(1).strip()
expected_kwargs.update({"date": date})

View file

@ -8,19 +8,19 @@ class ContribRuleTests(BaseTestCase):
""" Integration tests for contrib rules."""
def test_contrib_rules(self):
self.create_simple_commit(u"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",
_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[4])
self.assertEqualStdout(output, self.get_expected("test_contrib/test_contrib_rules_1"))
def test_contrib_rules_with_config(self):
self.create_simple_commit(u"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",
"-c", u"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])
self.assertEqualStdout(output, self.get_expected("test_contrib/test_contrib_rules_with_config_1"))
def test_invalid_contrib_rules(self):
self.create_simple_commit("WIP: test")
output = gitlint("--contrib", u"föobar,CC1", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[255])
self.assertEqualStdout(output, u"Config Error: No contrib rule with id or name 'föobar' found.\n")
output = gitlint("--contrib", "föobar,CC1", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[255])
self.assertEqualStdout(output, "Config Error: No contrib rule with id or name 'föobar' found.\n")

View file

@ -12,7 +12,7 @@ class IntegrationTests(BaseTestCase):
def test_successful(self):
# Test for STDIN with and without a TTY attached
self.create_simple_commit(u"Sïmple title\n\nSimple bödy describing the commit")
self.create_simple_commit("Sïmple title\n\nSimple bödy describing the commit")
output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _err_to_out=True)
self.assertEqualStdout(output, "")
@ -22,26 +22,26 @@ class IntegrationTests(BaseTestCase):
# Different commentchar (Note: tried setting this to a special unicode char, but git doesn't like that)
git("config", "--add", "core.commentchar", "$", _cwd=self.tmp_git_repo)
self.create_simple_commit(u"Sïmple title\n\nSimple bödy describing the commit\n$after commentchar\t ignored")
self.create_simple_commit("Sïmple title\n\nSimple bödy describing the commit\n$after commentchar\t ignored")
output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _err_to_out=True)
self.assertEqualStdout(output, "")
def test_successful_merge_commit(self):
# Create branch on master
self.create_simple_commit(u"Cömmit on master\n\nSimple bödy")
self.create_simple_commit("Cömmit on master\n\nSimple bödy")
# Create test branch, add a commit and determine the commit hash
git("checkout", "-b", "test-branch", _cwd=self.tmp_git_repo)
git("checkout", "test-branch", _cwd=self.tmp_git_repo)
commit_title = u"Commit on test-brånch with a pretty long title that will cause issues when merging"
self.create_simple_commit(u"{0}\n\nSïmple body".format(commit_title))
commit_title = "Commit on test-brånch with a pretty long title that will cause issues when merging"
self.create_simple_commit(f"{commit_title}\n\nSïmple body")
hash = self.get_last_commit_hash()
# Checkout master and merge the commit
# We explicitly set the title of the merge commit to the title of the previous commit as this or similar
# behavior is what many tools do that handle merges (like github, gerrit, etc).
git("checkout", "master", _cwd=self.tmp_git_repo)
git("merge", "--no-ff", "-m", u"Merge '{0}'".format(commit_title), hash, _cwd=self.tmp_git_repo)
git("merge", "--no-ff", "-m", f"Merge '{commit_title}'", hash, _cwd=self.tmp_git_repo)
# Run gitlint and assert output is empty
output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True)
@ -50,14 +50,13 @@ class IntegrationTests(BaseTestCase):
# Assert that we do see the error if we disable the ignore-merge-commits option
output = gitlint("-c", "general.ignore-merge-commits=false", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1])
self.assertEqual(output.exit_code, 1)
self.assertEqualStdout(output,
u"1: T1 Title exceeds max length (90>72): \"Merge '{0}'\"\n".format(commit_title))
self.assertEqualStdout(output, f"1: T1 Title exceeds max length (90>72): \"Merge '{commit_title}'\"\n")
def test_fixup_commit(self):
# Create a normal commit and assert that it has a violation
test_filename = self.create_simple_commit(u"Cömmit on WIP master\n\nSimple bödy that is long enough")
test_filename = self.create_simple_commit("Cömmit on WIP master\n\nSimple bödy that is long enough")
output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1])
expected = u"1: T5 Title contains the word 'WIP' (case-insensitive): \"Cömmit on WIP master\"\n"
expected = "1: T5 Title contains the word 'WIP' (case-insensitive): \"Cömmit on WIP master\"\n"
self.assertEqualStdout(output, expected)
# Make a small modification to the commit and commit it using fixup commit
@ -66,7 +65,7 @@ class IntegrationTests(BaseTestCase):
# https://stackoverflow.com/questions/22392377/
# error-writing-a-file-with-file-write-in-python-unicodeencodeerror
# So just keeping it simple - ASCII will here
fh.write(u"Appending some stuff\n")
fh.write("Appending some stuff\n")
git("add", test_filename, _cwd=self.tmp_git_repo)
@ -79,13 +78,13 @@ class IntegrationTests(BaseTestCase):
# Make sure that if we set the ignore-fixup-commits option to false that we do still see the violations
output = gitlint("-c", "general.ignore-fixup-commits=false", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2])
expected = u"1: T5 Title contains the word 'WIP' (case-insensitive): \"fixup! Cömmit on WIP master\"\n" + \
u"3: B6 Body message is missing\n"
expected = "1: T5 Title contains the word 'WIP' (case-insensitive): \"fixup! Cömmit on WIP master\"\n" + \
"3: B6 Body message is missing\n"
self.assertEqualStdout(output, expected)
def test_revert_commit(self):
self.create_simple_commit(u"WIP: Cömmit on master.\n\nSimple bödy")
self.create_simple_commit("WIP: Cömmit on master.\n\nSimple bödy")
hash = self.get_last_commit_hash()
git("revert", hash, _cwd=self.tmp_git_repo)
@ -97,14 +96,14 @@ class IntegrationTests(BaseTestCase):
output = gitlint("-c", "general.ignore-revert-commits=false",
_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1])
self.assertEqual(output.exit_code, 1)
expected = u"1: T5 Title contains the word 'WIP' (case-insensitive): \"Revert \"WIP: Cömmit on master.\"\"\n"
expected = "1: T5 Title contains the word 'WIP' (case-insensitive): \"Revert \"WIP: Cömmit on master.\"\"\n"
self.assertEqualStdout(output, expected)
def test_squash_commit(self):
# Create a normal commit and assert that it has a violation
test_filename = self.create_simple_commit(u"Cömmit on WIP master\n\nSimple bödy that is long enough")
test_filename = self.create_simple_commit("Cömmit on WIP master\n\nSimple bödy that is long enough")
output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[1])
expected = u"1: T5 Title contains the word 'WIP' (case-insensitive): \"Cömmit on WIP master\"\n"
expected = "1: T5 Title contains the word 'WIP' (case-insensitive): \"Cömmit on WIP master\"\n"
self.assertEqualStdout(output, expected)
# Make a small modification to the commit and commit it using squash commit
@ -113,11 +112,11 @@ class IntegrationTests(BaseTestCase):
# https://stackoverflow.com/questions/22392377/
# error-writing-a-file-with-file-write-in-python-unicodeencodeerror
# So just keeping it simple - ASCII will here
fh.write(u"Appending some stuff\n")
fh.write("Appending some stuff\n")
git("add", test_filename, _cwd=self.tmp_git_repo)
git("commit", "--squash", self.get_last_commit_hash(), "-m", u"Töo short body", _cwd=self.tmp_git_repo)
git("commit", "--squash", self.get_last_commit_hash(), "-m", "Töo short body", _cwd=self.tmp_git_repo)
# Assert that gitlint does not show an error for the fixup commit
output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True)
@ -127,25 +126,25 @@ class IntegrationTests(BaseTestCase):
# Make sure that if we set the ignore-squash-commits option to false that we do still see the violations
output = gitlint("-c", "general.ignore-squash-commits=false",
_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2])
expected = u"1: T5 Title contains the word 'WIP' (case-insensitive): \"squash! Cömmit on WIP master\"\n" + \
u"3: B5 Body message is too short (14<20): \"Töo short body\"\n"
expected = "1: T5 Title contains the word 'WIP' (case-insensitive): \"squash! Cömmit on WIP master\"\n" + \
"3: B5 Body message is too short (14<20): \"Töo short body\"\n"
self.assertEqualStdout(output, expected)
def test_violations(self):
commit_msg = u"WIP: This ïs a title.\nContent on the sëcond line"
commit_msg = "WIP: This ïs a title.\nContent on the sëcond line"
self.create_simple_commit(commit_msg)
output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3])
self.assertEqualStdout(output, self.get_expected("test_gitlint/test_violations_1"))
def test_msg_filename(self):
tmp_commit_msg_file = self.create_tmpfile(u"WIP: msg-fïlename test.")
tmp_commit_msg_file = self.create_tmpfile("WIP: msg-fïlename test.")
output = gitlint("--msg-filename", tmp_commit_msg_file, _tty_in=True, _ok_code=[3])
self.assertEqualStdout(output, self.get_expected("test_gitlint/test_msg_filename_1"))
def test_msg_filename_no_tty(self):
""" Make sure --msg-filename option also works with no TTY attached """
tmp_commit_msg_file = self.create_tmpfile(u"WIP: msg-fïlename NO TTY test.")
tmp_commit_msg_file = self.create_tmpfile("WIP: msg-fïlename NO TTY test.")
# We need to set _err_to_out explicitly for sh to merge stdout and stderr output in case there's
# no TTY attached to STDIN
@ -159,24 +158,24 @@ class IntegrationTests(BaseTestCase):
def test_no_git_name_set(self):
""" Ensure we print out a helpful message if user.name is not set """
tmp_commit_msg_file = self.create_tmpfile(u"WIP: msg-fïlename NO name test.")
tmp_commit_msg_file = self.create_tmpfile("WIP: msg-fïlename NO name test.")
# Name is checked before email so this isn't strictly
# necessary but seems good for consistency.
env = self.create_tmp_git_config(u"[user]\n email = test-emåil@foo.com\n")
env = self.create_tmp_git_config("[user]\n email = test-emåil@foo.com\n")
output = gitlint("--staged", "--msg-filename", tmp_commit_msg_file,
_ok_code=[self.GIT_CONTEXT_ERROR_CODE],
_env=env)
expected = u"Missing git configuration: please set user.name\n"
expected = "Missing git configuration: please set user.name\n"
self.assertEqualStdout(output, expected)
def test_no_git_email_set(self):
""" Ensure we print out a helpful message if user.email is not set """
tmp_commit_msg_file = self.create_tmpfile(u"WIP: msg-fïlename NO email test.")
env = self.create_tmp_git_config(u"[user]\n name = test åuthor\n")
tmp_commit_msg_file = self.create_tmpfile("WIP: msg-fïlename NO email test.")
env = self.create_tmp_git_config("[user]\n name = test åuthor\n")
output = gitlint("--staged", "--msg-filename", tmp_commit_msg_file,
_ok_code=[self.GIT_CONTEXT_ERROR_CODE],
_env=env)
expected = u"Missing git configuration: please set user.email\n"
expected = "Missing git configuration: please set user.email\n"
self.assertEqualStdout(output, expected)
def test_git_errors(self):
@ -184,10 +183,10 @@ class IntegrationTests(BaseTestCase):
empty_git_repo = self.create_tmp_git_repo()
output = gitlint(_cwd=empty_git_repo, _tty_in=True, _ok_code=[self.GIT_CONTEXT_ERROR_CODE])
expected = u"Current branch has no commits. Gitlint requires at least one commit to function.\n"
expected = "Current branch has no commits. Gitlint requires at least one commit to function.\n"
self.assertEqualStdout(output, expected)
# Repo has no commits: caused by `git rev-parse`
output = gitlint(echo(u"WIP: Pïpe test."), "--staged", _cwd=empty_git_repo, _tty_in=False,
output = gitlint(echo("WIP: Pïpe test."), "--staged", _cwd=empty_git_repo, _tty_in=False,
_err_to_out=True, _ok_code=[self.GIT_CONTEXT_ERROR_CODE])
self.assertEqualStdout(output, expected)

View file

@ -24,18 +24,18 @@ class HookTests(BaseTestCase):
# The '--staged' flag used in the commit-msg hook fetches additional information from the underlying
# git repo which means there already needs to be a commit in the repo
# (as gitlint --staged doesn't work against empty repos)
self.create_simple_commit(u"Commït Title\n\nCommit Body explaining commit.")
self.create_simple_commit("Commït Title\n\nCommit Body explaining commit.")
# install git commit-msg hook and assert output
output_installed = gitlint("install-hook", _cwd=self.tmp_git_repo)
expected_installed = u"Successfully installed gitlint commit-msg hook in %s/.git/hooks/commit-msg\n" % \
expected_installed = "Successfully installed gitlint commit-msg hook in %s/.git/hooks/commit-msg\n" % \
self.tmp_git_repo
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 = u"Successfully uninstalled gitlint commit-msg hook from %s/.git/hooks/commit-msg\n" % \
expected_uninstalled = "Successfully uninstalled gitlint commit-msg hook from %s/.git/hooks/commit-msg\n" % \
self.tmp_git_repo
self.assertEqualStdout(output_uninstalled, expected_uninstalled)
@ -50,24 +50,24 @@ class HookTests(BaseTestCase):
# Answer 'yes' to question to keep violating commit-msg
if "Your commit message contains the above violations" in line:
response = self.responses[self.response_index]
stdin.put("{0}\n".format(response))
stdin.put(f"{response}\n")
self.response_index = (self.response_index + 1) % len(self.responses)
def test_commit_hook_no_violations(self):
test_filename = self.create_simple_commit(u"This ïs a title\n\nBody contënt that should work",
test_filename = self.create_simple_commit("This ïs a title\n\nBody contënt that should work",
out=self._interact, tty_in=True)
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",
u"[master %s] This ïs a title\n" % short_hash,
"[master %s] This ïs a title\n" % short_hash,
" 1 file changed, 0 insertions(+), 0 deletions(-)\n",
u" create mode 100644 %s\n" % test_filename]
" create mode 100644 %s\n" % test_filename]
self.assertListEqual(expected_output, self.githook_output)
def test_commit_hook_continue(self):
self.responses = ["y"]
test_filename = self.create_simple_commit(u"WIP: This ïs a title.\nContënt on the second line",
test_filename = self.create_simple_commit("WIP: This ïs a title.\nContënt on the second line",
out=self._interact, tty_in=True)
# Determine short commit-msg hash, needed to determine expected output
@ -76,10 +76,10 @@ 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)] " +
u"[master %s] WIP: This ïs a title. Contënt on the second line\n"
"[master %s] WIP: This ïs a title. Contënt on the second line\n"
% short_hash,
" 1 file changed, 0 insertions(+), 0 deletions(-)\n",
u" create mode 100644 %s\n" % test_filename]
" create mode 100644 %s\n" % test_filename]
assert len(self.githook_output) == len(expected_output)
for output, expected in zip(self.githook_output, expected_output):
@ -89,7 +89,7 @@ class HookTests(BaseTestCase):
def test_commit_hook_abort(self):
self.responses = ["n"]
test_filename = self.create_simple_commit(u"WIP: This ïs a title.\nContënt on the second line",
test_filename = self.create_simple_commit("WIP: This ïs a title.\nContënt on the second line",
out=self._interact, ok_code=1, tty_in=True)
git("rm", "-f", test_filename, _cwd=self.tmp_git_repo)
@ -101,8 +101,8 @@ class HookTests(BaseTestCase):
"Commit aborted.\n",
"Your commit message: \n",
"-----------------------------------------------\n",
u"WIP: This ïs a title.\n",
u"Contënt on the second line\n",
"WIP: This ïs a title.\n",
"Contënt on the second line\n",
"-----------------------------------------------\n"]
self.assertListEqual(expected_output, self.githook_output)
@ -110,7 +110,7 @@ class HookTests(BaseTestCase):
def test_commit_hook_edit(self):
self.responses = ["e", "y"]
env = {"EDITOR": ":"}
test_filename = self.create_simple_commit(u"WIP: This ïs a title.\nContënt on the second line",
test_filename = self.create_simple_commit("WIP: This ïs a title.\nContënt on the second line",
out=self._interact, env=env, tty_in=True)
git("rm", "-f", test_filename, _cwd=self.tmp_git_repo)
@ -124,9 +124,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)] " +
u"[master %s] WIP: This ïs a title. Contënt on the second line\n" % short_hash,
"[master %s] WIP: This ïs a title. Contënt on the second line\n" % short_hash,
" 1 file changed, 0 insertions(+), 0 deletions(-)\n",
u" create mode 100644 %s\n" % test_filename]
" create mode 100644 %s\n" % test_filename]
assert len(self.githook_output) == len(expected_output)
for output, expected in zip(self.githook_output, expected_output):
@ -147,7 +147,7 @@ class HookTests(BaseTestCase):
```
"""
tmp_git_repo = self.create_tmp_git_repo()
self.create_simple_commit(u"Simple title\n\nContënt in the body", git_repo=tmp_git_repo)
self.create_simple_commit("Simple title\n\nContënt in the body", git_repo=tmp_git_repo)
worktree_dir = self.generate_temp_path()
self.tmp_git_repos.append(worktree_dir) # make sure we clean up the worktree afterwards
@ -156,10 +156,10 @@ class HookTests(BaseTestCase):
output_installed = gitlint("install-hook", _cwd=worktree_dir)
expected_hook_path = os.path.join(tmp_git_repo, ".git", "hooks", "commit-msg")
expected_msg = "Successfully installed gitlint commit-msg hook in {0}\n".format(expected_hook_path)
expected_msg = f"Successfully installed gitlint commit-msg hook in {expected_hook_path}\r\n"
self.assertEqual(output_installed, expected_msg)
output_uninstalled = gitlint("uninstall-hook", _cwd=worktree_dir)
expected_hook_path = os.path.join(tmp_git_repo, ".git", "hooks", "commit-msg")
expected_msg = "Successfully uninstalled gitlint commit-msg hook from {0}\n".format(expected_hook_path)
expected_msg = f"Successfully uninstalled gitlint commit-msg hook from {expected_hook_path}\r\n"
self.assertEqual(output_uninstalled, expected_msg)

View file

@ -7,14 +7,14 @@ class NamedRuleTests(BaseTestCase):
""" Integration tests for named rules."""
def test_named_rule(self):
commit_msg = u"WIP: thåt dûr bår\n\nSïmple commit body"
commit_msg = "WIP: thåt dûr bår\n\nSïmple commit body"
self.create_simple_commit(commit_msg)
config_path = self.get_sample_path("config/named-rules")
output = gitlint("--config", config_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5])
self.assertEqualStdout(output, self.get_expected("test_named_rules/test_named_rule_1"))
def test_named_user_rule(self):
commit_msg = u"Normal cömmit title\n\nSïmple commit message body"
commit_msg = "Normal cömmit title\n\nSïmple commit message body"
self.create_simple_commit(commit_msg)
config_path = self.get_sample_path("config/named-user-rules")
extra_path = self.get_sample_path("user_rules/extra")

View file

@ -4,7 +4,7 @@ import io
import subprocess
from qa.shell import echo, gitlint
from qa.base import BaseTestCase
from qa.utils import ustr, DEFAULT_ENCODING
from qa.utils import DEFAULT_ENCODING
class StdInTests(BaseTestCase):
@ -18,7 +18,7 @@ class StdInTests(BaseTestCase):
# NOTE: There is no use in testing this with _tty_in=True, because if you pipe something into a command
# there never is a TTY connected to stdin (per definition). We're setting _tty_in=False here to be explicit
# but note that this is always true when piping something into a command.
output = gitlint(echo(u"WIP: Pïpe test."),
output = gitlint(echo("WIP: Pïpe test."),
_cwd=self.tmp_git_repo, _tty_in=False, _err_to_out=True, _ok_code=[3])
self.assertEqualStdout(output, self.get_expected("test_stdin/test_stdin_pipe_1"))
@ -28,7 +28,7 @@ class StdInTests(BaseTestCase):
This is the equivalent of doing:
$ echo -n "" | gitlint
"""
commit_msg = u"WIP: This ïs a title.\nContent on the sëcond line"
commit_msg = "WIP: This ïs a title.\nContent on the sëcond line"
self.create_simple_commit(commit_msg)
# We need to set _err_to_out explicitly for sh to merge stdout and stderr output in case there's
@ -36,21 +36,21 @@ class StdInTests(BaseTestCase):
# http://amoffat.github.io/sh/sections/special_arguments.html?highlight=_tty_in#err-to-out
output = gitlint(echo("-n", ""), _cwd=self.tmp_git_repo, _tty_in=False, _err_to_out=True, _ok_code=[3])
self.assertEqual(ustr(output), self.get_expected("test_stdin/test_stdin_pipe_empty_1"))
self.assertEqual(output, self.get_expected("test_stdin/test_stdin_pipe_empty_1"))
def test_stdin_file(self):
""" Test the scenario where STDIN is a regular file (stat.S_ISREG = True)
This is the equivalent of doing:
$ gitlint < myfile
"""
tmp_commit_msg_file = self.create_tmpfile(u"WIP: STDIN ïs a file test.")
tmp_commit_msg_file = self.create_tmpfile("WIP: STDIN ïs a file test.")
with io.open(tmp_commit_msg_file, encoding=DEFAULT_ENCODING) as file_handle:
# 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(u"gitlint", stdin=file_handle, cwd=self.tmp_git_repo,
p = subprocess.Popen("gitlint", stdin=file_handle, cwd=self.tmp_git_repo,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output, _ = p.communicate()
self.assertEqual(ustr(output), 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

@ -10,7 +10,7 @@ class UserDefinedRuleTests(BaseTestCase):
def test_user_defined_rules_examples1(self):
""" Test the user defined rules in the top-level `examples/` directory """
extra_path = self.get_example_path()
commit_msg = u"WIP: Thi$ is å title\nContent on the second line"
commit_msg = "WIP: Thi$ is å title\nContent on the second line"
self.create_simple_commit(commit_msg)
output = gitlint("--extra-path", extra_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5])
self.assertEqualStdout(output, self.get_expected("test_user_defined/test_user_defined_rules_examples_1"))
@ -18,7 +18,7 @@ class UserDefinedRuleTests(BaseTestCase):
def test_user_defined_rules_examples2(self):
""" Test the user defined rules in the top-level `examples/` directory """
extra_path = self.get_example_path()
commit_msg = u"Release: Thi$ is å title\nContent on the second line\n$This line is ignored \nThis isn't\t\n"
commit_msg = "Release: Thi$ is å title\nContent on the second line\n$This line is ignored \nThis isn't\t\n"
self.create_simple_commit(commit_msg)
output = gitlint("--extra-path", extra_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[4])
self.assertEqualStdout(output, self.get_expected("test_user_defined/test_user_defined_rules_examples_2"))
@ -26,7 +26,7 @@ class UserDefinedRuleTests(BaseTestCase):
def test_user_defined_rules_examples_with_config(self):
""" Test the user defined rules in the top-level `examples/` directory """
extra_path = self.get_example_path()
commit_msg = u"WIP: Thi$ is å title\nContent on the second line"
commit_msg = "WIP: Thi$ is å title\nContent on the second line"
self.create_simple_commit(commit_msg)
output = gitlint("--extra-path", extra_path, "-c", "body-max-line-count.max-line-count=1",
_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[6])
@ -35,7 +35,7 @@ class UserDefinedRuleTests(BaseTestCase):
def test_user_defined_rules_extra(self):
extra_path = self.get_sample_path("user_rules/extra")
commit_msg = u"WIP: Thi$ is å title\nContent on the second line"
commit_msg = "WIP: Thi$ is å title\nContent on the second line"
self.create_simple_commit(commit_msg)
output = gitlint("--extra-path", extra_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[9])
self.assertEqualStdout(output, self.get_expected("test_user_defined/test_user_defined_rules_extra_1",

View file

@ -1,6 +1,5 @@
# pylint: disable=bad-option-value,unidiomatic-typecheck,undefined-variable,no-else-return
import platform
import sys
import os
import locale
@ -15,16 +14,6 @@ def platform_is_windows():
PLATFORM_IS_WINDOWS = platform_is_windows()
########################################################################################################################
# IS_PY2
def is_py2():
return sys.version_info[0] == 2
IS_PY2 = is_py2()
########################################################################################################################
# USE_SH_LIB
# Determine whether to use the `sh` library
@ -71,41 +60,3 @@ def getpreferredencoding():
DEFAULT_ENCODING = getpreferredencoding()
########################################################################################################################
# Unicode utility functions
def ustr(obj):
""" Python 2 and 3 utility method that converts an obj to unicode in python 2 and to a str object in python 3"""
if IS_PY2:
# If we are getting a string, then do an explicit decode
# else, just call the unicode method of the object
if type(obj) in [str, basestring]: # pragma: no cover # noqa
return unicode(obj, DEFAULT_ENCODING) # pragma: no cover # noqa
else:
return unicode(obj) # pragma: no cover # noqa
else:
if type(obj) in [bytes]:
return obj.decode(DEFAULT_ENCODING)
else:
return str(obj)
def sstr(obj):
""" Python 2 and 3 utility method that converts an obj to a DEFAULT_ENCODING encoded string in python 2
and to unicode in python 3.
Especially useful for implementing __str__ methods in python 2: http://stackoverflow.com/a/1307210/381010"""
if IS_PY2:
# For lists and tuples in python2, remove unicode string representation characters.
# i.e. ensure lists are printed as ['a', 'b'] and not [u'a', u'b']
if type(obj) in [list]:
return [sstr(item) for item in obj] # pragma: no cover # noqa
elif type(obj) in [tuple]:
return tuple(sstr(item) for item in obj) # pragma: no cover # noqa
return unicode(obj).encode(DEFAULT_ENCODING) # pragma: no cover # noqa
else:
return obj # pragma: no cover
########################################################################################################################

View file

@ -1,5 +1,5 @@
setuptools
wheel==0.33.4
Click==7.0
sh==1.12.14; sys_platform != 'win32' # sh is not supported on windows
arrow==0.15.5;
wheel==0.35.1
Click==7.1.2
sh==1.14.1; sys_platform != 'win32' # sh is not supported on windows
arrow==0.17.0

View file

@ -13,7 +13,7 @@ help(){
echo " -b, --build Run build tests"
echo " -a, --all Run all tests and checks (unit, integration, pep8, git)"
echo " -e, --envs [ENV1],[ENV2] Run tests against specified python environments"
echo " (envs: 27,35,36,37,pypy2,pypy35)."
echo " (envs: 36,37,38,39,pypy37)."
echo " Also works for integration, pep8 and lint tests."
echo " -C, --container Run the specified command in the container for the --envs specified"
echo " --all-env Run all tests against all python environments"
@ -261,9 +261,7 @@ install_virtualenv(){
# For pypy: custom path + fetch from the web if not installed (=distro agnostic)
if [[ $version == *"pypy"* ]]; then
pypy_download_mirror="https://downloads.python.org/pypy"
if [[ $version == *"pypy2"* ]]; then
pypy_full_version="pypy2.7-v7.3.2-linux64"
elif [[ $version == *"pypy36"* ]]; then
if [[ $version == *"pypy36"* ]]; then
pypy_full_version="pypy3.6-v7.3.2-linux64"
elif [[ $version == *"pypy37"* ]]; then
pypy_full_version="pypy3.7-v7.3.2-linux64"
@ -365,7 +363,7 @@ uninstall_container(){
assert_specific_env(){
if [ -z "$1" ] || [ "$1" == "default" ]; then
fatal "ERROR: Please specify one or more valid python environments using --envs: 27,35,36,37,pypy2,pypy35"
fatal "ERROR: Please specify one or more valid python environments using --envs: 36,37,38,39,pypy37"
exit 1
fi
}
@ -461,7 +459,7 @@ exit_code=0
# If the users specified 'all', then just replace $envs with the list of all envs
if [ "$envs" == "all" ]; then
envs="27,35,36,37,38,39,pypy2,pypy35"
envs="36,37,38,39,pypy37"
fi
original_envs="$envs"
envs=$(echo "$envs" | tr ',' '\n') # Split the env list on comma so we can loop through it

View file

@ -7,13 +7,6 @@ import os
import platform
import sys
# There is an issue with building python packages in a shared vagrant directory because of how setuptools works
# in python < 2.7.9. We solve this by deleting the filesystem hardlinking capability during build.
# See: http://stackoverflow.com/a/22147112/381010
try:
del os.link
except:
pass # Not all OSes (e.g. windows) support os.link
description = "Git commit message linter written in python, checks your commit messages for style."
long_description = """
@ -52,8 +45,6 @@ setup(
"Development Status :: 5 - Production/Stable",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
@ -66,18 +57,23 @@ setup(
"Topic :: Software Development :: Testing",
"License :: OSI Approved :: MIT License"
],
python_requires=">=3.6",
install_requires=[
'Click==7.0',
'arrow==0.15.5',
'Click==7.1.2',
'arrow==0.17.0',
],
extras_require={
':sys_platform != "win32"': [
'sh==1.12.14',
'sh==1.14.1',
],
},
keywords='gitlint git lint',
author='Joris Roovers',
url='https://github.com/jorisroovers/gitlint',
url='https://jorisroovers.github.io/gitlint',
project_urls={
'Documentation': 'https://jorisroovers.github.io/gitlint',
'Source': 'https://github.com/jorisroovers/gitlint',
},
license='MIT',
package_data={
'gitlint': ['files/*']
@ -93,8 +89,7 @@ setup(
# Print a red deprecation warning for python < 3.6 users
if sys.version_info[:2] < (3, 6):
msg = "\033[31mDEPRECATION: You're using a python version that has reached end-of-life. " + \
"Gitlint does not support Python < 3.5 or < 2.7, and will be dropping support for " + \
"Python 2.7 and 3.5 in the next release. " + \
"Gitlint does not support Python < 3.6" + \
"Please upgrade your Python to 3.6 or above.\033[0m"
print(msg)

View file

@ -1,10 +1,8 @@
unittest2==1.1.0; python_version <= '2.7'
flake8==3.7.9
coverage==4.5.3
flake8==3.8.4
coverage==5.3
python-coveralls==2.9.2
radon==4.1.0
mock==3.0.5 # mock 4.x no longer supports Python 2.7
pytest==4.6.3; # pytest 5.x no longer supports Python 2.7
pylint==1.9.4; python_version == '2.7'
pylint==2.3.1; python_version >= '3.4'
radon==4.3.2
flake8-polyfill==1.0.2 # Required when installing both flake8 and radon>=4.3.1
pytest==6.1.2;
pylint==2.6.0;
-e .