summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2020-11-03 06:07:45 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2020-11-03 06:07:45 +0000
commit5f208e04c159791e668031a7fa83f98724ec8d24 (patch)
tree4b58b42fd2a91a14871010e2dd39369a839ae383
parentAdding upstream version 0.13.1. (diff)
downloadgitlint-5f208e04c159791e668031a7fa83f98724ec8d24.tar.xz
gitlint-5f208e04c159791e668031a7fa83f98724ec8d24.zip
Adding upstream version 0.14.0.upstream/0.14.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.github/PULL_REQUEST_TEMPLATE.md13
-rw-r--r--.github/workflows/checks.yml44
-rw-r--r--.gitignore4
-rw-r--r--CHANGELOG.md120
-rw-r--r--Dockerfile9
-rw-r--r--README.md2
-rw-r--r--Vagrantfile5
-rw-r--r--doc-requirements.txt2
-rw-r--r--docs/configuration.md296
-rw-r--r--docs/contrib_rules.md12
-rw-r--r--docs/contributing.md55
-rw-r--r--docs/extra.js5
-rw-r--r--docs/index.md176
-rw-r--r--docs/rules.md244
-rw-r--r--docs/user_defined_rules.md264
-rw-r--r--examples/commit-message-117
-rw-r--r--examples/my_commit_rules.py10
-rw-r--r--examples/my_configuration_rules.py72
-rw-r--r--examples/my_line_rules.py9
-rw-r--r--gitlint/__init__.py2
-rw-r--r--gitlint/cli.py144
-rw-r--r--gitlint/config.py65
-rw-r--r--gitlint/contrib/rules/conventional_commit.py2
-rw-r--r--gitlint/display.py5
-rw-r--r--gitlint/files/commit-msg78
-rw-r--r--gitlint/files/gitlint25
-rw-r--r--gitlint/git.py34
-rw-r--r--gitlint/options.py37
-rw-r--r--gitlint/rule_finder.py41
-rw-r--r--gitlint/rules.py131
-rw-r--r--gitlint/shell.py18
-rw-r--r--gitlint/tests/base.py41
-rw-r--r--gitlint/tests/cli/test_cli.py78
-rw-r--r--gitlint/tests/cli/test_cli_hooks.py172
-rw-r--r--gitlint/tests/config/test_config.py62
-rw-r--r--gitlint/tests/config/test_config_builder.py81
-rw-r--r--gitlint/tests/config/test_config_precedence.py26
-rw-r--r--gitlint/tests/contrib/rules/__init__.py (renamed from qa/samples/config/contrib-enabled)0
-rw-r--r--gitlint/tests/contrib/rules/test_conventional_commit.py (renamed from gitlint/tests/contrib/test_conventional_commit.py)4
-rw-r--r--gitlint/tests/contrib/rules/test_signedoff_by.py (renamed from gitlint/tests/contrib/test_signedoff_by.py)0
-rw-r--r--gitlint/tests/contrib/test_contrib_rules.py2
-rw-r--r--gitlint/tests/expected/cli/test_cli/test_contrib_1 (renamed from gitlint/tests/expected/test_cli/test_contrib_1)2
-rw-r--r--gitlint/tests/expected/cli/test_cli/test_debug_1 (renamed from gitlint/tests/expected/test_cli/test_debug_1)22
-rw-r--r--gitlint/tests/expected/cli/test_cli/test_input_stream_1 (renamed from gitlint/tests/expected/test_cli/test_input_stream_1)0
-rw-r--r--gitlint/tests/expected/cli/test_cli/test_input_stream_debug_1 (renamed from gitlint/tests/expected/test_cli/test_input_stream_debug_1)0
-rw-r--r--gitlint/tests/expected/cli/test_cli/test_input_stream_debug_2 (renamed from gitlint/tests/expected/test_cli/test_input_stream_debug_2)10
-rw-r--r--gitlint/tests/expected/cli/test_cli/test_lint_multiple_commits_1 (renamed from gitlint/tests/expected/test_cli/test_lint_multiple_commits_1)0
-rw-r--r--gitlint/tests/expected/cli/test_cli/test_lint_multiple_commits_config_1 (renamed from gitlint/tests/expected/test_cli/test_lint_multiple_commits_config_1)0
-rw-r--r--gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_1 (renamed from gitlint/tests/expected/test_cli/test_lint_staged_msg_filename_1)0
-rw-r--r--gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_2 (renamed from gitlint/tests/expected/test_cli/test_lint_staged_msg_filename_2)14
-rw-r--r--gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_1 (renamed from gitlint/tests/expected/test_cli/test_lint_staged_stdin_1)0
-rw-r--r--gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_2 (renamed from gitlint/tests/expected/test_cli/test_lint_staged_stdin_2)14
-rw-r--r--gitlint/tests/expected/cli/test_cli/test_named_rules_14
-rw-r--r--gitlint/tests/expected/cli/test_cli/test_named_rules_282
-rw-r--r--gitlint/tests/expected/cli/test_cli_hooks/test_hook_config_1_stderr2
-rw-r--r--gitlint/tests/expected/cli/test_cli_hooks/test_hook_config_1_stdout5
-rw-r--r--gitlint/tests/expected/cli/test_cli_hooks/test_hook_edit_1_stderr6
-rw-r--r--gitlint/tests/expected/cli/test_cli_hooks/test_hook_edit_1_stdout14
-rw-r--r--gitlint/tests/expected/cli/test_cli_hooks/test_hook_local_commit_1_stderr2
-rw-r--r--gitlint/tests/expected/cli/test_cli_hooks/test_hook_local_commit_1_stdout4
-rw-r--r--gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_1_stderr2
-rw-r--r--gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_1_stdout8
-rw-r--r--gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_tty_1_stderr2
-rw-r--r--gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_tty_1_stdout5
-rw-r--r--gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_no_violations_1_stdout2
-rw-r--r--gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_violations_1_stderr2
-rw-r--r--gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_violations_1_stdout5
-rw-r--r--gitlint/tests/expected/cli/test_cli_hooks/test_hook_yes_1_stderr2
-rw-r--r--gitlint/tests/expected/cli/test_cli_hooks/test_hook_yes_1_stdout4
-rw-r--r--gitlint/tests/git/test_git.py10
-rw-r--r--gitlint/tests/git/test_git_commit.py40
-rw-r--r--gitlint/tests/rules/test_body_rules.py46
-rw-r--r--gitlint/tests/rules/test_configuration_rules.py38
-rw-r--r--gitlint/tests/rules/test_meta_rules.py9
-rw-r--r--gitlint/tests/rules/test_rules.py5
-rw-r--r--gitlint/tests/rules/test_title_rules.py34
-rw-r--r--gitlint/tests/rules/test_user_rules.py137
-rw-r--r--gitlint/tests/samples/commit_message/no-violations6
-rw-r--r--gitlint/tests/samples/config/named-rules8
-rw-r--r--gitlint/tests/test_hooks.py14
-rw-r--r--gitlint/tests/test_lint.py94
-rw-r--r--gitlint/tests/test_options.py122
-rw-r--r--gitlint/tests/test_utils.py20
-rw-r--r--gitlint/utils.py33
-rw-r--r--mkdocs.yml11
-rw-r--r--qa/base.py19
-rw-r--r--qa/expected/test_commits/test_lint_staged_msg_filename_115
-rw-r--r--qa/expected/test_commits/test_lint_staged_stdin_115
-rw-r--r--qa/expected/test_config/test_config_from_env_193
-rw-r--r--qa/expected/test_config/test_config_from_env_283
-rw-r--r--qa/expected/test_config/test_config_from_file_debug_115
-rw-r--r--qa/expected/test_contrib/test_contrib_rules_12
-rw-r--r--qa/expected/test_named_rules/test_named_rule_15
-rw-r--r--qa/expected/test_named_rules/test_named_user_rule_19
-rw-r--r--qa/expected/test_user_defined/test_user_defined_rules_examples_24
-rw-r--r--qa/expected/test_user_defined/test_user_defined_rules_extra_16
-rw-r--r--qa/samples/config/named-rules8
-rw-r--r--qa/samples/config/named-user-rules15
-rw-r--r--qa/samples/user_rules/extra/extra_rules.py52
-rw-r--r--qa/shell.py7
-rw-r--r--qa/test_config.py43
-rw-r--r--qa/test_gitlint.py22
-rw-r--r--qa/test_hooks.py12
-rw-r--r--qa/test_named_rules.py23
-rw-r--r--qa/test_user_defined.py17
-rw-r--r--qa/utils.py18
-rwxr-xr-xrun_tests.sh37
-rw-r--r--setup.py17
-rw-r--r--tools/windows/create-test-repo.bat20
109 files changed, 2964 insertions, 830 deletions
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..db7f144
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,13 @@
+<!--- THIS IS A COMMENT BLOCK, REMOVE IT BEFORE SUBMITTING YOUR PR
+
+Thank you for your interest in gitlint and putting in the effort to create a PR!
+
+A few quick notes:
+
+- It's really just me (https://github.com/jorisroovers) maintaining gitlint, and I do so in a hobby capacity. More recently it has become harder for me to find time to maintain gitlint on a regular basis, which in practice means that it might take me a while (sometimes months) to get back to you. Rest assured though, I absolutely look at all PRs as soon as they come in - I just tend to only "work" on gitlint a few times a year.
+- Similarly, after your code is merged, it typically still takes months before I do another release that contains your code. Please don't let that deter you from contributing - I appreciate your patience and understanding!
+- Please review: http://jorisroovers.github.io/gitlint/contributing/
+
+-->
+
+Enter your PR details here
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 348fb47..710245c 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -1,19 +1,27 @@
name: Tests and Checks
-on: [push]
+on: [push, pull_request]
jobs:
checks:
runs-on: "ubuntu-latest"
strategy:
matrix:
- python-version: [2.7, 3.5, 3.6, 3.7, 3.8, pypy2, pypy3]
+ python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, pypy2, pypy3]
os: ["macos-latest", "ubuntu-latest"]
steps:
- uses: actions/checkout@v2
+ with:
+ ref: ${{ github.event.pull_request.head.sha }} # Checkout pull request HEAD commit instead of merge commit
+
+ # Because gitlint is a tool that uses git itself under the hood, we remove git tracking from the checked out
+ # code by temporarily renaming the .git directory.
+ # This is to ensure that the tests don't have a dependency on the version control of gitlint itself.
+ - name: Temporarily remove git version control from code
+ run: mv .git ._git
- name: Setup python
- uses: actions/setup-python@v1
+ uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
@@ -57,24 +65,36 @@ jobs:
# with:
# github-token: ${{ secrets.GITHUB_TOKEN }}
+ # Re-add git version control so we can run gitlint on itself.
+ - name: Re-add git version control to code
+ run: mv ._git .git
+
- name: Gitlint check
- run: ./run_tests.sh -g
+ run: ./run_tests.sh -g --debug
windows-checks:
runs-on: windows-latest
strategy:
matrix:
- python-version: [2.7, 3.5]
+ python-version: [2.7, 3.6]
steps:
- uses: actions/checkout@v2
-
+ with:
+ ref: ${{ github.event.pull_request.head.sha }} # Checkout pull request HEAD commit instead of merge commit
+
+ # Because gitlint is a tool that uses git itself under the hood, we remove git tracking from the checked out
+ # code by temporarily renaming the .git directory.
+ # This is to ensure that the tests don't have a dependency on the version control of gitlint itself.
+ - name: Temporarily remove git version control from code
+ run: Rename-Item .git ._git
+
- name: Setup python
- uses: actions/setup-python@v1
+ uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: "Upgrade pip on Python 3"
- if: matrix.python-version == '3.5'
+ if: matrix.python-version == '3.6'
run: python -m pip install --upgrade pip
- name: Install requirements
@@ -88,8 +108,8 @@ jobs:
- name: Tests (sanity)
run: tools\windows\run_tests.bat "gitlint\tests\cli\test_cli.py::CLITests::test_lint"
- - name: Tests (ignore test_cli.py)
- run: pytest --ignore gitlint\tests\cli\test_cli.py -rw -s gitlint
+ - name: Tests (ignore cli\*)
+ run: pytest --ignore gitlint\tests\cli -rw -s gitlint
- name: Tests (test_cli.py only - continue-on-error:true)
run: tools\windows\run_tests.bat "gitlint\tests\cli\test_cli.py"
@@ -109,5 +129,9 @@ jobs:
- name: PyLint
run: pylint gitlint qa --rcfile=".pylintrc" -r n
+ # Re-add git version control so we can run gitlint on itself.
+ - name: Re-add git version control to code
+ run: Rename-Item ._git .git
+
- name: Gitlint check
run: gitlint --debug
diff --git a/.gitignore b/.gitignore
index c350158..e81c924 100644
--- a/.gitignore
+++ b/.gitignore
@@ -63,6 +63,8 @@ virtualenv
# Vagrant
.vagrant
-
# mkdocs
site/
+
+# pyenv
+.python-version \ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a3991d..509ebc8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,37 @@
# Changelog #
+## 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).
+
+
+- **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.**
+- Python 3.9 support
+- **New Rule**: [title-min-length](http://jorisroovers.github.io/gitlint/rules/#t8-title-min-length) enforces a minimum length on titles (default: 5 chars) ([#138](https://github.com/jorisroovers/gitlint/issues/138))
+- **New Rule**: [body-match-regex](http://jorisroovers.github.io/gitlint/rules/#b8-body-match-regex) allows users to enforce that the commit-msg body matches a given regex ([#130](https://github.com/jorisroovers/gitlint/issues/130))
+- **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))
+- [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).
+- Users can now use `self.log.debug("my message")` for debugging purposes in their user-defined rules. Debug messages will show up when running `gitlint --debug`.
+- **Breaking**: User-defined rule id's can no longer start with 'I', as those are reserved for [built-in gitlint ignore rules](http://jorisroovers.github.io/gitlint/rules/#i1-ignore-by-title).
+- New `RegexOption` rule [option type for use in user-defined rules](http://jorisroovers.github.io/gitlint/user_defined_rules/#options). By using the `RegexOption`, regular expressions are pre-validated at gitlint startup and compiled only once which is much more efficient when linting multiple commits.
+- Bugfixes:
+ - Improved UTF-8 fallback on Windows (ongoing - [#96](https://github.com/jorisroovers/gitlint/issues/96))
+ - Windows users can now use the 'edit' function of the `commit-msg` hook ([#94](https://github.com/jorisroovers/gitlint/issues/94))
+ - Doc update: Users should use `--ulimit nofile=1024` when invoking gitlint using Docker ([#129](https://github.com/jorisroovers/gitlint/issues/129))
+ - The `commit-msg` hook was broken in Ubuntu's gitlint package due to a python/python3 mismatch ([#127](https://github.com/jorisroovers/gitlint/issues/127))
+ - Better error message when no git username is set ([#149](https://github.com/jorisroovers/gitlint/issues/149))
+ - Options can now actually be set to `None` (from code) to make them optional.
+ - Ignore rules no longer have `"None"` as default regex, but an empty regex - effectively disabling them by default (as intended).
+- Contrib Rules:
+ - Added 'ci' and 'build' to conventional commit types ([#135](https://github.com/jorisroovers/gitlint/issues/135))
+- Under-the-hood: minor performance improvements (removed some unnecessary regex matching), test improvements, improved debug logging, CI runs on pull requests, PR request template.
+
## v0.13.1 (2020-02-26)
- Patch to enable `--staged` flag for pre-commit.
@@ -8,8 +40,8 @@
## v0.13.0 (2020-02-25)
- **Behavior Change**: Revert Commits are now recognized and ignored by default ([#99](https://github.com/jorisroovers/gitlint/issues/99))
-- ```--staged``` flag: gitlint can now detect meta-data (such as author details, changed files, etc) of staged/pre-commits. Useful when you use [gitlint's commit-msg hook](https://jorisroovers.github.io/gitlint/#using-gitlint-as-a-commit-msg-hook) or [precommit](https://jorisroovers.github.io/gitlint/#using-gitlint-through-pre-commit) ([#105](https://github.com/jorisroovers/gitlint/issues/105))
-- New branch properties on ```GitCommit``` and ```GitContext```, useful when writing your own user-defined rules: ```commit.branches``` and ```commit.context.current_branch``` ([#108](https://github.com/jorisroovers/gitlint/issues/108))
+- `--staged` flag: gitlint can now detect meta-data (such as author details, changed files, etc) of staged/pre-commits. Useful when you use [gitlint's commit-msg hook](https://jorisroovers.github.io/gitlint/#using-gitlint-as-a-commit-msg-hook) or [precommit](https://jorisroovers.github.io/gitlint/#using-gitlint-through-pre-commit) ([#105](https://github.com/jorisroovers/gitlint/issues/105))
+- New branch properties on `GitCommit` and `GitContext`, useful when writing your own user-defined rules: `commit.branches` and `commit.context.current_branch` ([#108](https://github.com/jorisroovers/gitlint/issues/108))
- Python 3.8 support
- Python 3.4 no longer supported. Python 3.4 has [reached EOL](https://www.python.org/dev/peps/pep-0429/#id4) and an increasing
of gitlint's dependencies have dropped support which makes it hard to maintain.
@@ -30,14 +62,14 @@ Special thanks to all contributors for this release, in particular [@rogalksi](h
- [Contrib Rules](http://jorisroovers.github.io/gitlint/contrib_rules): community-contributed rules that are disabled
by default, but can be enabled through configuration. Contrib rules are meant to augment default gitlint behavior by
providing users with rules for common use-cases without forcing these rules on all gitlint users.
- - **New Contrib Rule**: ```contrib-title-conventional-commits``` enforces the [Conventional Commits](https://www.conventionalcommits.org) spec. Details in our [documentation](http://jorisroovers.github.io/gitlint/contrib_rules/#ct1-contrib-title-conventional-commits).
- - **New Contrib Rule**: ```cc1-contrib-requires-signed-off-by``` ensures that all commit messages contain a ```Sign-Off-By``` line. Details in our [documentation](http://jorisroovers.github.io/gitlint/contrib_rules/#cc1-contrib-requires-signed-off-by).
+ - **New Contrib Rule**: `contrib-title-conventional-commits` enforces the [Conventional Commits](https://www.conventionalcommits.org) spec. Details in our [documentation](http://jorisroovers.github.io/gitlint/contrib_rules/#ct1-contrib-title-conventional-commits).
+ - **New Contrib Rule**: `cc1-contrib-requires-signed-off-by` ensures that all commit messages contain a `Sign-Off-By` line. Details in our [documentation](http://jorisroovers.github.io/gitlint/contrib_rules/#cc1-contrib-requires-signed-off-by).
- If you're interested in adding new Contrib rules to gitlint, please start by reading the
[Contributing](http://jorisroovers.github.io/gitlint/contributing/) page. Thanks for considering!
- *Experimental (!)* Windows support: Basic functionality is working, but there are still caveats. For more details, please refer to [#20](https://github.com/jorisroovers/gitlint/issues/20) and the [open issues related to Windows](https://github.com/jorisroovers/gitlint/issues?q=is%3Aissue+is%3Aopen+label%3Awindows).
- Python 3.3 no longer supported. Python 3.4 is likely to follow in a future release as it has [reached EOL](https://www.python.org/dev/peps/pep-0429/#id4) as well.
- PyPy 3.5 support
-- Support for ```--ignore-stdin``` command-line flag to ignore any text send via stdin. ([#56](https://github.com/jorisroovers/gitlint/issues/56), [#89](https://github.com/jorisroovers/gitlint/issues/89))
+- Support for `--ignore-stdin` command-line flag to ignore any text send via stdin. ([#56](https://github.com/jorisroovers/gitlint/issues/56), [#89](https://github.com/jorisroovers/gitlint/issues/89))
- Bugfixes:
- [#68: Can't use install-hooks in with git worktree](https://github.com/jorisroovers/gitlint/issues/68)
- [#59: gitlint failed with configured commentchar](https://github.com/jorisroovers/gitlint/issues/59)
@@ -63,16 +95,16 @@ python version to 2.7 or 3.3+. Future versions of gitlint are likely to drop sup
Full Changelog:
-- **New Rule**: ```ignore-by-title``` allows users to
+- **New Rule**: `ignore-by-title` allows users to
[ignore certain commits](http://jorisroovers.github.io/gitlint/#ignoring-commits) by matching a regex against
a commit message title. ([#54](https://github.com/jorisroovers/gitlint/issues/54), [#57](https://github.com/jorisroovers/gitlint/issues/57)).
-- **New Rule**: ```ignore-by-body``` allows users to
+- **New Rule**: `ignore-by-body` allows users to
[ignore certain commits](http://jorisroovers.github.io/gitlint/#ignoring-commits) by matching a regex against
a line in a commit message body.
- Gitlint now supports [pre-commit.com](https://pre-commit.com).
[Details in our documentation](http://jorisroovers.github.io/gitlint/#using-gitlint-through-pre-commit)
([#62](https://github.com/jorisroovers/gitlint/issues/62)).
-- Gitlint now has a ```--msg-filename``` commandline flag that allows you to specify the commit message to lint via
+- Gitlint now has a `--msg-filename` commandline flag that allows you to specify the commit message to lint via
a file ([#39](https://github.com/jorisroovers/gitlint/issues/39)).
- Gitlint will now be silent by default when a specified commit range is empty ([#46](https://github.com/jorisroovers/gitlint/issues/46)).
- Gitlint can now be installed on MacOS by brew via the [homebrew-devops](https://github.com/rockyluke/homebrew-devops) tap. To get the latest version of gitlint, always use pip for installation.
@@ -83,18 +115,18 @@ a line in a commit message body.
- Fix for custom git comment characters ([#48](https://github.com/jorisroovers/gitlint/issues/48)).
## v0.9.0 (2017-12-03) ##
-The 0.9.0 release adds a new default ```author-valid-email``` rule, important bugfixes and special case handling.
+The 0.9.0 release adds a new default `author-valid-email` rule, important bugfixes and special case handling.
Special thanks to [joshholl](https://github.com/joshholl), [ron8mcr](https://github.com/ron8mcr),
[omarkohl](https://github.com/omarkohl), [domo141](https://github.com/domo141), [nud](https://github.com/nud)
and [AlexMooney](https://github.com/AlexMooney) for their contributions.
-- New Rule: ```author-valid-email``` enforces a valid author email address. Details can be found in the
+- New Rule: `author-valid-email` enforces a valid author email address. Details can be found in the
[Rules section of the documentation](http://jorisroovers.github.io/gitlint/rules/#m1-author-valid-email).
-- **Breaking change**: The ```--commits``` commandline flag now strictly follows the refspec format as interpreted
- by the [```git rev-list <refspec>```](https://git-scm.com/docs/git-rev-list) command. This means
- that linting a single commit using ```gitlint --commits <SHA>``` won't work anymore. Instead, for single commits,
- users now need to specificy ```gitlint --commits <SHA>^...<SHA>```. On the upside, this change also means
- that gitlint will now understand all refspec formatters, including ```gitlint --commits HEAD``` to lint all commits
+- **Breaking change**: The `--commits` commandline flag now strictly follows the refspec format as interpreted
+ by the [`git rev-list <refspec>`](https://git-scm.com/docs/git-rev-list) command. This means
+ that linting a single commit using `gitlint --commits <SHA>` won't work anymore. Instead, for single commits,
+ users now need to specificy `gitlint --commits <SHA>^...<SHA>`. On the upside, this change also means
+ that gitlint will now understand all refspec formatters, including `gitlint --commits HEAD` to lint all commits
in the repository. This fixes [#23](https://github.com/jorisroovers/gitlint/issues/23).
- **Breaking change**: Gitlint now always falls back on trying to read a git message from a local git repository, only
reading a commit message from STDIN if one is passed. Before, gitlint only read from the local git repository when
@@ -105,7 +137,7 @@ and [AlexMooney](https://github.com/AlexMooney) for their contributions.
[ignore squash and fixup commits](http://jorisroovers.github.io/gitlint/#merge-fixup-and-squash-commits)
(fix for [#33: fixup messages should not trigger a gitlint violation](https://github.com/jorisroovers/gitlint/issues/33))
- Support for custom comment characters ([#34](https://github.com/jorisroovers/gitlint/issues/34))
-- Support for [```git commit --cleanup=scissors```](https://git-scm.com/docs/git-commit#git-commit---cleanupltmodegt)
+- Support for [`git commit --cleanup=scissors`](https://git-scm.com/docs/git-commit#git-commit---cleanupltmodegt)
([#34](https://github.com/jorisroovers/gitlint/issues/34))
- Bugfix: [#37: Prevent Commas in text fields from breaking git log printing](https://github.com/jorisroovers/gitlint/issues/37)
- Debug output improvements
@@ -115,14 +147,14 @@ and [AlexMooney](https://github.com/AlexMooney) for their contributions.
The 0.8.2 release brings minor improvements, bugfixes and some under-the-hood changes. Special thanks to
[tommyip](https://github.com/tommyip) for his contributions.
-- ```--extra-path``` now also accepts a file path (in the past only directory paths where accepted).
+- `--extra-path` now also accepts a file path (in the past only directory paths where accepted).
Thanks to [tommyip](https://github.com/tommyip) for implementing this!
-- gitlint will now show more information when using the ```--debug``` flag. This is initial work and will continue to
+- gitlint will now show more information when using the `--debug` flag. This is initial work and will continue to
be improved upon in later releases.
- Bugfixes:
- [#24: --commits doesn't take commit specific config into account](https://github.com/jorisroovers/gitlint/issues/24)
- [#27: --commits returns the wrong exit code](https://github.com/jorisroovers/gitlint/issues/27)
-- Development: better unit and integration test coverage for ```--commits```
+- Development: better unit and integration test coverage for `--commits`
## v0.8.1 (2017-03-16) ##
@@ -151,8 +183,8 @@ The 0.8.0 release is a significant release that has been in the works for a long
[define your own custom rules](http://jorisroovers.github.io/gitlint/user_defined_rules/)
if you want to extend gitlint's functionality.
- Pypy2 support!
-- Debug output improvements: Gitlint will now print your active configuration when using ```--debug```
-- The ```general.target``` option can now also be set via ```-c``` flags or a ```.gitlint``` file
+- Debug output improvements: Gitlint will now print your active configuration when using `--debug`
+- The `general.target` option can now also be set via `-c` flags or a `.gitlint` file
- Bugfixes:
- Various important fixes related to configuration precedence
- [#17: Body MinLength is not working properly](https://github.com/jorisroovers/gitlint/issues/17).
@@ -162,15 +194,15 @@ The 0.8.0 release is a significant release that has been in the works for a long
- Development:
- Pylint compliance for all supported python versions
- Updated dependencies to latest versions
- - Various ```run_tests.sh``` improvements for developer convenience
+ - Various `run_tests.sh` improvements for developer convenience
## v0.7.1 (2016-06-18) ##
Bugfixes:
-- **Behavior Change**: gitlint no longer prints the file path by default when using a ```.gitlint``` file. The path
-will still be printed when using the new ```--debug``` flag. Special thanks to [Slipcon](https://github.com/slipcon)
+- **Behavior Change**: gitlint no longer prints the file path by default when using a `.gitlint` file. The path
+will still be printed when using the new `--debug` flag. Special thanks to [Slipcon](https://github.com/slipcon)
for submitting this.
-- Gitlint now prints a correct violation message for the ```title-match-regex``` rule. Special thanks to
+- Gitlint now prints a correct violation message for the `title-match-regex` rule. Special thanks to
[Slipcon](https://github.com/slipcon) for submitting this.
- Gitlint is now better at parsing commit messages cross-platform by taking platform specific line endings into account
- Minor documentation improvements
@@ -187,7 +219,7 @@ requests.
- **Behavior change**: merge commits are now ignored by default. The rationale is that the original commits
should already be linted and that many merge commits don't pass gitlint checks by default
(e.g. exceeding title length or empty body is very common). This behavior can be overwritten by setting the
- general option ```ignore-merge-commit=false```.
+ general option `ignore-merge-commit=false`.
- Bugfixes and enhancements:
- [#7: Hook compatibility with SourceTree](https://github.com/jorisroovers/gitlint/issues/7)
- [#8: Illegal option -e](https://github.com/jorisroovers/gitlint/issues/8)
@@ -200,32 +232,32 @@ requests.
## v0.6.1 (2015-11-22) ##
-- Fix: ```install-hook``` and ```generate-config``` commands not working when gitlint is installed from pypi.
+- Fix: `install-hook` and `generate-config` commands not working when gitlint is installed from pypi.
## v0.6.0 (2015-11-22) ##
- Python 3 (3.3+) support!
- All documentation is now hosted on [http://jorisroovers.github.io/gitlint/]()
-- New ```generate-config``` command generates a sample gitlint config file
-- New ```--target``` flag allows users to lint different directories than the current working directory
+- New `generate-config` command generates a sample gitlint config file
+- New `--target` flag allows users to lint different directories than the current working directory
- **Breaking change**: exit code behavior has changed. More details in the
[Exit codes section of the documentation](http://jorisroovers.github.io/gitlint/#exit-codes).
-- **Breaking change**: ```--install-hook``` and ```--uninstall-hook``` have been renamed to ```install-hook``` and
- ```uninstall-hook``` respectively to better express that they are commands instead of options.
+- **Breaking change**: `--install-hook` and `--uninstall-hook` have been renamed to `install-hook` and
+ `uninstall-hook` respectively to better express that they are commands instead of options.
- Better error handling when gitlint is executed in a directory that is not a git repository or
when git is not installed.
- The git commit message hook now uses pretty colored output
-- Fix: ```--config``` option no longer accepts directories as value
+- Fix: `--config` option no longer accepts directories as value
- Development: unit tests are now ran using py.test
## v0.5.0 (2015-10-04) ##
-- New Rule: ```title-match-regex```. Details can be found in the
+- New Rule: `title-match-regex`. Details can be found in the
[Rules section of the documentation](http://jorisroovers.github.io/gitlint/rules/).
-- Uninstall previously installed gitlint git commit hooks using: ```gitlint --uninstall-hook```
-- Ignore rules on a per commit basis by adding e.g.: ```gitlint-ignore: T1, body-hard-tab``` to your git commit message.
- Use ```gitlint-ignore: all``` to disable gitlint all together for a specific commit.
-- ```body-is-missing``` will now automatically be disabled for merge commits (use the ```ignore-merge-commit: false```
+- Uninstall previously installed gitlint git commit hooks using: `gitlint --uninstall-hook`
+- Ignore rules on a per commit basis by adding e.g.: `gitlint-ignore: T1, body-hard-tab` to your git commit message.
+ Use `gitlint-ignore: all` to disable gitlint all together for a specific commit.
+- `body-is-missing` will now automatically be disabled for merge commits (use the `ignore-merge-commit: false`
option to disable this behavior)
- Violations are now sorted by line number first and then by rule id (previously the order of violations on the
same line was arbitrary).
@@ -236,16 +268,16 @@ requests.
## v0.4.0 (2015-09-19) ##
-- New rules: ```body-is-missing```, ```body-min-length```, ```title-leading-whitespace```,
- ```body-changed-file-mention```. Details can be found in the
+- New rules: `body-is-missing`, `body-min-length`, `title-leading-whitespace`,
+ `body-changed-file-mention`. Details can be found in the
[Rules section of the documentation](http://jorisroovers.github.io/gitlint/rules/).
-- The git ```commit-msg``` hook now allows you to keep or discard the commit when it fails gitlint validation
+- The git `commit-msg` hook now allows you to keep or discard the commit when it fails gitlint validation
- gitlint is now also released as a [python wheel](http://pythonwheels.com/) on pypi.
- Internal: rule classes now have access to a gitcontext containing body the commit message and the files changed in the
last commit.
## v0.3.0 (2015-09-11) ##
-- ```title-must-not-contain-word``` now has a ```words``` option that can be used to specify which words should not
+- `title-must-not-contain-word` now has a `words` option that can be used to specify which words should not
occur in the title
- gitlint violations are now printed to the stderr instead of stdout
- Various minor bugfixes
@@ -255,17 +287,17 @@ requests.
## v0.2.0 (2015-09-10) ##
- Rules can now have their behavior configured through options.
- For example, the ```title-max-length``` rule now has a ```line-length``` option.
+ For example, the `title-max-length` rule now has a `line-length` option.
- Under-the-hood: The codebase now has a basic level of unit test coverage, increasing overall quality assurance
## v0.1.1 (2015-09-08) ##
-- Bugfix: added missing ```sh``` dependency
+- Bugfix: added missing `sh` dependency
## v0.1.0 (2015-09-08) ##
- Initial gitlint release
- Initial set of rules: title-max-length, title-trailing-whitespace, title-trailing-punctuation , title-hard-tab,
title-must-not-contain-word, body-max-line-length, body-trailing-whitespace, body-hard-tab
-- General gitlint configuration through a ```gitlint``` file
+- General gitlint configuration through a `gitlint` file
- Silent and verbose mode
- Vagrantfile for easy development
- gitlint is available on [pypi](https://pypi.python.org/pypi/gitlint)
diff --git a/Dockerfile b/Dockerfile
index b66bb71..9f93206 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,12 +1,15 @@
# User-facing Dockerfile. For development, see Dockerfile.dev and ./run_tests.sh -h
# To lint your current working directory:
-# docker run -v $(pwd):/repo jorisroovers/gitlint
+# docker run --ulimit nofile=1024 -v $(pwd):/repo jorisroovers/gitlint
# With arguments:
-# docker run -v $(pwd):/repo jorisroovers/gitlint --debug --ignore T1
+# docker --ulimit nofile=1024 run -v $(pwd):/repo jorisroovers/gitlint --debug --ignore T1
-FROM python:3.8-alpine
+# NOTE: --ulimit is required to work around a limitation in Docker
+# Details: https://github.com/jorisroovers/gitlint/issues/129
+
+FROM python:3.9-alpine
ARG GITLINT_VERSION
RUN apk add git
diff --git a/README.md b/README.md
index 81f2ac9..43da74c 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ Git commit message linter written in python (for Linux and Mac, experimental on
## Contributing ##
All contributions are welcome and very much appreciated!
-**I'm looking for contributors that are interested in taking a more active co-maintainer role as it's becoming increasingly difficult for me to find time to maintain gitlint. Please open a PR if you're interested - Thanks!**
+**I'm [looking for contributors](https://github.com/jorisroovers/gitlint/issues/134) that are interested in taking a more active co-maintainer role as it's becoming increasingly difficult for me to find time to maintain gitlint. Please open a PR if you're interested - Thanks!**
See [jorisroovers.github.io/gitlint/contributing](http://jorisroovers.github.io/gitlint/contributing) for details on
how to get started - it's easy!
diff --git a/Vagrantfile b/Vagrantfile
index 2a26aab..7684c1a 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -7,9 +7,10 @@ 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
-sudo apt-get install -y --allow-unauthenticated python3.8-distutils # Needed to work around python3.8+virtualenv issue
+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.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 build-essential libssl-dev libffi-dev # for rebuilding cryptography (required for pypy2)
sudo apt-get purge -y python3-virtualenv
sudo pip3 install virtualenv
diff --git a/doc-requirements.txt b/doc-requirements.txt
index baf208d..53dbf05 100644
--- a/doc-requirements.txt
+++ b/doc-requirements.txt
@@ -1 +1 @@
-mkdocs==1.0.4 \ No newline at end of file
+mkdocs==1.1.2 \ No newline at end of file
diff --git a/docs/configuration.md b/docs/configuration.md
index 641b361..78224c1 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -1,21 +1,21 @@
# Configuration
Gitlint can be configured through different means.
-# Config files #
-You can modify gitlint's behavior by adding a ```.gitlint``` file to your git repository.
+## The .gitlint file
+You can modify gitlint's behavior by adding a `.gitlint` file to your git repository.
-Generate a default ```.gitlint``` config file by running:
-```bash
+Generate a default `.gitlint` config file by running:
+```sh
gitlint generate-config
```
You can also use a different config file like so:
-```bash
+```sh
gitlint --config myconfigfile.ini
```
-The block below shows a sample ```.gitlint``` file. Details about rule config options can be found on the
-[Rules](rules.md) page, details about the ```[general]``` section can be found in the
+The block below shows a sample `.gitlint` file. Details about rule config options can be found on the
+[Rules](rules.md) page, details about the `[general]` section can be found in the
[General Configuration](configuration.md#general-configuration) section of this page.
```ini
@@ -25,7 +25,7 @@ The block below shows a sample ```.gitlint``` file. Details about rule config op
# one rule and each key in it is an option for that specific rule.
#
# Rules and sections can be referenced by their full name or by id. For example
-# section "[body-max-line-length]" could be written as "[B1]". Full section names are
+# section "[body-max-line-length]" could also be written as "[B1]". Full section names are
# used in here for clarity.
# Rule reference documentation: http://jorisroovers.github.io/gitlint/rules/
#
@@ -68,6 +68,11 @@ extra-path=examples/
[title-max-length]
line-length=80
+# Conversely, you can also enforce minimal length of a title with the
+# "title-min-length" rule:
+[title-min-length]
+min-length=5
+
[title-must-not-contain-word]
# Comma-separated list of words that should not occur in the title. Matching is case
# insensitive. It's fine if the keyword occurs as part of a larger word (so "WIPING"
@@ -99,10 +104,15 @@ ignore-merge-commits=false
# it in the commit message.
files=gitlint/rules.py,README.md
+[body-match-regex]
+# python-style regex that the commit-msg body must match.
+# E.g. body must end in My-Commit-Tag: foo
+regex=My-Commit-Tag: foo$
+
[author-valid-email]
# python like regex (https://docs.python.org/2/library/re.html) that the
# commit author email address should be matched to
-# 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]
@@ -123,6 +133,11 @@ ignore=T1,body-min-length
# Use 'all' to ignore all rules
ignore=T1,body-min-length
+[ignore-body-lines]
+# Ignore certain lines in a commit body that match a regex.
+# E.g. Ignore all lines that start with 'Co-Authored-By'
+regex=^Co-Authored-By
+
# This is a contrib rule - a community contributed rule. These are disabled by default.
# You need to explicitly enable them one-by-one by adding them to the "contrib" option
# under [general] section above.
@@ -131,20 +146,20 @@ ignore=T1,body-min-length
types = bugfix,user-story,epic
```
-# Commandline config #
+## Commandline config
-You can also use one or more ```-c``` flags like so:
+You can also use one or more `-c` flags like so:
```
$ gitlint -c general.verbosity=2 -c title-max-length.line-length=80 -c B1.line-length=100
```
-The generic config flag format is ```-c <rule>.<option>=<value>``` and supports all the same rules and options which
-you can also use in a ```.gitlint``` config file.
+The generic config flag format is `-c <rule>.<option>=<value>` and supports all the same rules and options which
+you can also use in a `.gitlint` config file.
-# Commit specific config #
+## Commit specific config
You can also configure gitlint by adding specific lines to your commit message.
-For now, we only support ignoring commits by adding ```gitlint-ignore: all``` to the commit
+For now, we only support ignoring commits by adding `gitlint-ignore: all` to the commit
message like so:
```
@@ -154,7 +169,7 @@ I want gitlint to ignore this entire commit message.
gitlint-ignore: all
```
-```gitlint-ignore: all``` can occur on any line, as long as it is at the start of the line.
+`gitlint-ignore: all` can occur on any line, as long as it is at the start of the line.
You can also specify specific rules to be ignored as follows:
```
@@ -166,44 +181,46 @@ gitlint-ignore: T1, body-hard-tab
-# Configuration precedence #
+## Configuration precedence
gitlint configuration is applied in the following order of precedence:
-1. Commit specific config (e.g.: ```gitlint-ignore: all``` in the commit message)
+1. Commit specific config (e.g.: `gitlint-ignore: all` in the commit message)
2. Configuration Rules (e.g.: [ignore-by-title](/rules/#i1-ignore-by-title))
-3. Commandline convenience flags (e.g.: ```-vv```, ```--silent```, ```--ignore```)
-4. Commandline configuration flags (e.g.: ```-c title-max-length=123```)
-5. Configuration file (local ```.gitlint``` file, or file specified using ```-C```/```--config```)
-6. Default gitlint config
+3. Commandline convenience flags (e.g.: `-vv`, `--silent`, `--ignore`)
+4. Environment variables (e.g.: `GITLINT_VERBOSITY=3`)
+5. Commandline configuration flags (e.g.: `-c title-max-length=123`)
+6. Configuration file (local `.gitlint` file, or file specified using `-C`/`--config`)
+7. Default gitlint config
-# General Options
+## General Options
Below we outline all configuration options that modify gitlint's overall behavior. These options can be specified
-using commandline flags or in ```[general]``` section in a ```.gitlint``` configuration file.
+using commandline flags or in `[general]` section in a `.gitlint` configuration file.
-## silent
+### silent
Enable silent mode (no output). Use [exit](index.md#exit-codes) code to determine result.
-Default value | gitlint version | commandline flag
----------------|------------------|-------------------
- false | >= 0.1.0 | ```--silent```
+Default value | gitlint version | commandline flag | environment variable
+---------------|------------------|-------------------|-----------------------
+`False` | >= 0.1.0 | `--silent` | `GITLINT_SILENT`
-### Examples
+#### Examples
```sh
# CLI
gitlint --silent
+GITLINT_SILENT=1 gitlint # using env variable
```
-## verbosity
+### verbosity
Amount of output gitlint will show when printing errors.
-Default value | gitlint version | commandline flag
----------------|------------------|-------------------
- 3 | >= 0.1.0 | `-v`
+Default value | gitlint version | commandline flag | environment variable
+---------------|------------------|-------------------|-----------------------
+3 | >= 0.1.0 | `-v` | `GITLINT_VERBOSITY`
-### Examples
+#### Examples
```sh
# CLI
gitlint -vvv # default (level 3)
@@ -212,221 +229,230 @@ gitlint -v # even less (level 1)
gitlint --silent # no output (level 0)
gitlint -c general.verbosity=1 # Set specific level
gitlint -c general.verbosity=0 # Same as --silent
+GITLINT_VERBOSITY=2 gitlint # using env variable
```
```ini
-.gitlint
+# .gitlint
[general]
verbosity=2
```
-## ignore-merge-commits
+### ignore
-Whether or not to ignore merge commits.
+Comma separated list of rules to ignore (by name or id).
-Default value | gitlint version | commandline flag
----------------|------------------|-------------------
- true | >= 0.7.0 | Not Available
+Default value | gitlint version | commandline flag | environment variable
+---------------------------|------------------|-------------------|-----------------------
+ [] (=empty list) | >= 0.1.0 | `--ignore` | `GITLINT_IGNORE`
-### Examples
+#### Examples
```sh
# CLI
-gitlint -c general.ignore-merge-commits=false
+gitlint --ignore=body-min-length # ignore single rule
+gitlint --ignore=T1,body-min-length # ignore multiple rule
+gitlint -c general.ignore=T1,body-min-length # different way of doing the same
+GITLINT_IGNORE=T1,body-min-length gitlint # using env variable
```
```ini
#.gitlint
[general]
-ignore-merge-commits=false
+ignore=T1,body-min-length
```
-## ignore-revert-commits
+### debug
-Whether or not to ignore revert commits.
+Enable debugging output.
-Default value | gitlint version | commandline flag
----------------|------------------|-------------------
- true | >= 0.13.0 | Not Available
+Default value | gitlint version | commandline flag | environment variable
+---------------|------------------|-------------------|-----------------------
+ false | >= 0.7.1 | `--debug` | `GITLINT_DEBUG`
-### Examples
+#### Examples
```sh
# CLI
-gitlint -c general.ignore-revert-commits=false
-```
-```ini
-#.gitlint
-[general]
-ignore-revert-commits=false
+gitlint --debug
+GITLINT_DEBUG=1 gitlint # using env variable
+# --debug is special, the following does NOT work
+# gitlint -c general.debug=true
```
-## ignore-fixup-commits
+### target
-Whether or not to ignore [fixup](https://git-scm.com/docs/git-commit#git-commit---fixupltcommitgt) commits.
+Target git repository gitlint should be linting against.
-Default value | gitlint version | commandline flag
----------------|------------------|-------------------
- true | >= 0.9.0 | Not Available
+Default value | gitlint version | commandline flag | environment variable
+---------------------------|------------------|-------------------|-----------------------
+(empty) | >= 0.8.0 | `--target` | `GITLINT_TARGET`
-### Examples
+#### Examples
```sh
# CLI
-gitlint -c general.ignore-fixup-commits=false
+gitlint --target=/home/joe/myrepo/
+gitlint -c general.target=/home/joe/myrepo/ # different way of doing the same
+GITLINT_TARGET=/home/joe/myrepo/ gitlint # using env variable
```
```ini
#.gitlint
[general]
-ignore-fixup-commits=false
+target=/home/joe/myrepo/
```
-## ignore-squash-commits
+### extra-path
-Whether or not to ignore [squash](https://git-scm.com/docs/git-commit#git-commit---squashltcommitgt) commits.
+Path where gitlint looks for [user-defined rules](user_defined_rules.md).
-Default value | gitlint version | commandline flag
----------------|------------------|-------------------
- true | >= 0.9.0 | Not Available
+Default value | gitlint version | commandline flag | environment variable
+---------------------------|------------------|-------------------|-----------------------
+ (empty) | >= 0.8.0 | `--extra-path` | `GITLINT_EXTRA_PATH`
-### Examples
+#### Examples
```sh
# CLI
-gitlint -c general.ignore-squash-commits=false
+gitlint --extra-path=/home/joe/rules/
+gitlint -c general.extra-path=/home/joe/rules/ # different way of doing the same
+GITLINT_EXTRA_PATH=/home/joe/rules/ gitlint # using env variable
```
```ini
#.gitlint
[general]
-ignore-squash-commits=false
+extra-path=/home/joe/rules/
```
-## ignore
+### contrib
-Comma separated list of rules to ignore (by name or id).
+Comma-separated list of [Contrib rules](contrib_rules) to enable (by name or id).
-Default value | gitlint version | commandline flag
----------------------------|------------------|-------------------
- [] (=empty list) | >= 0.1.0 | `--ignore`
+Default value | gitlint version | commandline flag | environment variable
+---------------------------|------------------|-------------------|-----------------------
+ (empty) | >= 0.12.0 | `--contrib` | `GITLINT_CONTRIB`
-### Examples
+#### Examples
```sh
# CLI
-gitlint --ignore=body-min-length # ignore single rule
-gitlint --ignore=T1,body-min-length # ignore multiple rule
-gitlint -c general.ignore=T1,body-min-length # different way of doing the same
+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
```
```ini
#.gitlint
[general]
-ignore=T1,body-min-length
+contrib=contrib-title-conventional-commits,CC1
```
-## debug
+### staged
-Enable debugging output.
+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
----------------|------------------|-------------------
- false | >= 0.7.1 | `--debug`
+Default value | gitlint version | commandline flag | environment variable
+---------------|------------------|-------------------|-----------------------
+ false | >= 0.13.0 | `--staged` | `GITLINT_STAGED`
-### Examples
+#### Examples
```sh
# CLI
-gitlint --debug
-# --debug is special, the following does NOT work
-# gitlint -c general.debug=true
+gitlint --staged
+gitlint -c general.staged=true # different way of doing the same
+GITLINT_STAGED=1 gitlint # using env variable
+```
+```ini
+#.gitlint
+[general]
+staged=true
```
-## target
+### ignore-stdin
-Target git repository gitlint should be linting against.
+Ignore any stdin data. Sometimes useful when running gitlint in a CI server.
-Default value | gitlint version | commandline flag
----------------------------|------------------|-------------------
- (empty) | >= 0.8.0 | `--target`
+Default value | gitlint version | commandline flag | environment variable
+---------------|------------------|-------------------|-----------------------
+ false | >= 0.12.0 | `--ignore-stdin` | `GITLINT_IGNORE_STDIN`
-### Examples
+#### Examples
```sh
# CLI
-gitlint --target=/home/joe/myrepo/
-gitlint -c general.target=/home/joe/myrepo/ # different way of doing the same
+gitlint --ignore-stdin
+gitlint -c general.ignore-stdin=true # different way of doing the same
+GITLINT_IGNORE_STDIN=1 gitlint # using env variable
```
```ini
#.gitlint
[general]
-target=/home/joe/myrepo/
+ignore-stdin=true
```
-## extra-path
+### ignore-merge-commits
-Path where gitlint looks for [user-defined rules](user_defined_rules.md).
+Whether or not to ignore merge commits.
-Default value | gitlint version | commandline flag
----------------------------|------------------|-------------------
- (empty) | >= 0.8.0 | `--extra-path`
+Default value | gitlint version | commandline flag | environment variable
+---------------|------------------|-------------------|-----------------------
+ true | >= 0.7.0 | Not Available | Not Available
-### Examples
+#### Examples
```sh
# CLI
-gitlint --extra-path=/home/joe/rules/
-gitlint -c general.extra-path=/home/joe/rules/ # different way of doing the same
+gitlint -c general.ignore-merge-commits=false
```
```ini
#.gitlint
[general]
-extra-path=/home/joe/rules/
+ignore-merge-commits=false
```
-## contrib
+### ignore-revert-commits
-[Contrib rules](contrib_rules) to enable.
+Whether or not to ignore revert commits.
-Default value | gitlint version | commandline flag
----------------------------|------------------|-------------------
- (empty) | >= 0.12.0 | `--contrib`
+Default value | gitlint version | commandline flag | environment variable
+---------------|------------------|-------------------|-----------------------
+ true | >= 0.13.0 | Not Available | Not Available
-### Examples
+#### Examples
```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 -c general.ignore-revert-commits=false
```
```ini
#.gitlint
[general]
-contrib=contrib-title-conventional-commits,CC1
+ignore-revert-commits=false
```
-## ignore-stdin
-Ignore any stdin data. Sometimes useful when running gitlint in a CI server.
+### ignore-fixup-commits
+
+Whether or not to ignore [fixup](https://git-scm.com/docs/git-commit#git-commit---fixupltcommitgt) commits.
-Default value | gitlint version | commandline flag
----------------|------------------|-------------------
- false | >= 0.12.0 | `--ignore-stdin`
+Default value | gitlint version | commandline flag | environment variable
+---------------|------------------|-------------------|-----------------------
+ true | >= 0.9.0 | Not Available | Not Available
-### Examples
+#### Examples
```sh
# CLI
-gitlint --ignore-stdin
-gitlint -c general.ignore-stdin=true # different way of doing the same
+gitlint -c general.ignore-fixup-commits=false
```
```ini
#.gitlint
[general]
-ignore-stdin=true
+ignore-fixup-commits=false
```
-## staged
+### ignore-squash-commits
-Fetch additional meta-data from the local `repository when manually passing a commit message to gitlint via stdin or ```--commit-msg```.
+Whether or not to ignore [squash](https://git-scm.com/docs/git-commit#git-commit---squashltcommitgt) commits.
-Default value | gitlint version | commandline flag
----------------|------------------|-------------------
- false | >= 0.13.0 | `--staged`
+Default value | gitlint version | commandline flag | environment variable
+---------------|------------------|-------------------|-----------------------
+ true | >= 0.9.0 | Not Available | Not Available
-### Examples
+#### Examples
```sh
# CLI
-gitlint --staged
-gitlint -c general.staged=true # different way of doing the same
+gitlint -c general.ignore-squash-commits=false
```
```ini
#.gitlint
[general]
-staged=true
+ignore-squash-commits=false
``` \ No newline at end of file
diff --git a/docs/contrib_rules.md b/docs/contrib_rules.md
index a4f4f0d..e376fb8 100644
--- a/docs/contrib_rules.md
+++ b/docs/contrib_rules.md
@@ -7,7 +7,7 @@ Contrib rules are meant to augment default gitlint behavior by providing users w
forcing these rules on all gitlint users. This also means that users don't have to
re-implement these commonly used rules themselves as [user-defined](user_defined_rules) rules.
-To enable certain contrib rules, you can use the ```--contrib``` flag.
+To enable certain contrib rules, you can use the `--contrib` flag.
```sh
$ cat examples/commit-message-1 | gitlint --contrib contrib-title-conventional-commits,CC1
1: CC1 Body does not contain a 'Signed-Off-By' line
@@ -20,7 +20,7 @@ $ cat examples/commit-message-1 | gitlint --contrib contrib-title-conventional-c
3: B1 Line exceeds max length (123>80): "Lines typically need to have a max length, meaning that they can't exceed a preset number of characters, usually 80 or 120."
```
-Same thing using a ```.gitlint``` file:
+Same thing using a `.gitlint` file:
```ini
[general]
@@ -36,7 +36,7 @@ types = bugfix,user-story,epic
You can also configure contrib rules using [any of the other ways to configure gitlint](configuration.md).
-# Available Contrib Rules
+## Available Contrib Rules
ID | Name | gitlint version | Description
------|-------------------------------------|------------------ |-------------------------------------------
@@ -53,7 +53,7 @@ CT1 | contrib-title-conventional-commits | >= 0.12.0 | Enforces [C
Name | gitlint version | Default | Description
---------------|--------------------|--------------|----------------------------------
-types | >= 0.12.0 | `fix,feat,chore,docs,style,refactor,perf,test,revert` | Comma separated list of allowed commit types.
+types | >= 0.12.0 | `fix,feat,chore,docs,style,refactor,perf,test,revert,ci,build` | Comma separated list of allowed commit types.
## CC1: contrib-requires-signed-off-by ##
@@ -63,5 +63,5 @@ ID | Name | gitlint version | Description
CC1 | contrib-requires-signed-off-by | >= 0.12.0 | Commit body must contain a `Signed-Off-By` line. This means, a line that starts with the `Signed-Off-By` keyword.
-# Contributing Contrib rules
-We'd love for you to contribute new Contrib rules to gitlint or improve existing ones! Please visit the [Contributing](contributing) page on how to get started. \ No newline at end of file
+## Contributing Contrib rules
+We'd love for you to contribute new Contrib rules to gitlint or improve existing ones! Please visit the [Contributing](contributing) page on how to get started.
diff --git a/docs/contributing.md b/docs/contributing.md
index 0cd6eaf..f0d2d30 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -9,40 +9,47 @@ your interest!
We maintain a [loose roadmap on our wiki](https://github.com/jorisroovers/gitlint/wiki/Roadmap), but
that's open to a lot of change and input.
-# Guidelines
+## Guidelines
When contributing code, please consider all the parts that are typically required:
-- [Unit tests](https://github.com/jorisroovers/gitlint/tree/master/gitlint/tests) (automatically
+- [Unit tests](https://github.com/jorisroovers/gitlint/tree/main/gitlint/tests) (automatically
[enforced by CI](https://github.com/jorisroovers/gitlint/actions)). Please consider writing
new ones for your functionality, not only updating existing ones to make the build pass.
-- [Integration tests](https://github.com/jorisroovers/gitlint/tree/master/qa) (also automatically
+- [Integration tests](https://github.com/jorisroovers/gitlint/tree/main/qa) (also automatically
[enforced by CI](https://github.com/jorisroovers/gitlint/actions)). Again, please consider writing new ones
for your functionality, not only updating existing ones to make the build pass.
-- [Documentation](https://github.com/jorisroovers/gitlint/tree/master/docs)
+- [Documentation](https://github.com/jorisroovers/gitlint/tree/main/docs)
Since we want to maintain a high standard of quality, all of these things will have to be done regardless before code
can make it as part of a release. If you can already include them as part of your PR, it's a huge timesaver for us
and it's likely that your PR will be merged and released a lot sooner. Thanks!
-# Development #
+!!! Important
+ **On the topic of releases**: Gitlint releases typically go out when there's either enough new features and fixes
+ to make it worthwhile or when there's a critical fix for a bug that fundamentally breaks gitlint. While the amount
+ of overhead of doing a release isn't huge, it's also not zero. In practice this means that it might take weeks
+ or months before merged code actually gets released - we know that can be frustrating but please understand it's
+ a well-considered trade-off based on available time.
+
+## Development
There is a Vagrantfile in this repository that can be used for development.
-```bash
+```sh
vagrant up
vagrant ssh
```
Or you can choose to use your local environment:
-```bash
+```sh
virtualenv .venv
pip install -r requirements.txt -r test-requirements.txt -r doc-requirements.txt
python setup.py develop
```
To run tests:
-```bash
+```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 --no-coverage # run unit tests without test coverage
@@ -54,16 +61,14 @@ To run tests:
./run_tests.sh --git # inception: run gitlint against itself
./run_tests.sh --lint # run pylint checks
./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 and pypy2.
+The `Vagrantfile` comes with `virtualenv`s for python 2.7, 3.5, 3.6, 3.7, 3.8, 3.9 and pypy2.
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,35,pypy2 # Run the unit tests against Python 2.7, Python 3.5 and Pypy2
-./run_tests.sh --envs 27,35 --pep8 # Run pep8 checks against Python 2.7 and Python 3.5 (also works for ```--git```, ```--integration```, ```--pep8```, ```--stats``` and ```--lint```).
+./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 all --all # Run all tests against all environments
./run_tests.sh --all-env --all # Idem: Run all tests against all environments
```
@@ -71,29 +76,29 @@ You can easily run tests against specific python environments by using the follo
!!! important
Gitlint commits and pull requests are gated on all of our tests and checks.
-# Packaging #
+## Packaging
To see the package description in HTML format
-```
+```sh
pip install docutils
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
python setup.py --long-description | rst2html.py > output.html
```
-# Documentation #
+## Documentation
We use [mkdocs](https://www.mkdocs.org/) for generating our documentation from markdown.
To use it, do the following outside of the vagrant box (on your host machine):
-```bash
+```sh
pip install -r doc-requirements.txt # install doc requirements
mkdocs serve
```
Then access the documentation website on your host machine on [http://localhost:8000]().
-# Tools #
-We keep a small set of scripts in the ```tools/``` directory:
+## Tools
+We keep a small set of scripts in the `tools/` directory:
```sh
tools/create-test-repo.sh # Create a test git repo in your /tmp directory
@@ -101,7 +106,7 @@ tools/windows/create-test-repo.bat # Windows: create git test repo
tools/windows/run_tests.bat # Windows run unit tests
```
-# Contrib rules
+## Contrib rules
Since gitlint 0.12.0, we support [Contrib rules](../contrib_rules): community contributed rules that are part of gitlint
itself. Thanks for considering to add a new one to gitlint!
@@ -110,13 +115,13 @@ Then, we suggest taking the following approach to add a Contrib rule:
1. **Write your rule as a [user-defined rule](../user_defined_rules)**. In terms of code, Contrib rules are identical to
user-defined rules, they just happen to have their code sit within the gitlint codebase itself.
-2. **Add your user-defined rule to gitlint**. You should put your file(s) in the [gitlint/contrib/rules](https://github.com/jorisroovers/gitlint/tree/master/gitlint/contrib/rules) directory.
-3. **Write unit tests**. The gitlint codebase contains [Contrib rule test files you can copy and modify](https://github.com/jorisroovers/gitlint/tree/master/gitlint/tests/contrib).
-4. **Write documentation**. In particular, you should update the [gitlint/docs/contrib_rules.md](https://github.com/jorisroovers/gitlint/blob/master/docs/contrib_rules.md) file with details on your Contrib rule.
+2. **Add your user-defined rule to gitlint**. You should put your file(s) in the [gitlint/contrib/rules](https://github.com/jorisroovers/gitlint/tree/main/gitlint/contrib/rules) directory.
+3. **Write unit tests**. The gitlint codebase contains [Contrib rule test files you can copy and modify](https://github.com/jorisroovers/gitlint/tree/main/gitlint/tests/contrib/rules).
+4. **Write documentation**. In particular, you should update the [gitlint/docs/contrib_rules.md](https://github.com/jorisroovers/gitlint/blob/main/docs/contrib_rules.md) file with details on your Contrib rule.
5. **Create a Pull Request**: code review typically requires a bit of back and forth. Thanks for your contribution!
-## Contrib rule requirements
+### Contrib rule requirements
If you follow the steps above and follow the existing gitlint conventions wrt naming things, you should already be fairly close to done.
In case you're looking for a slightly more formal spec, here's what gitlint requires of Contrib rules.
diff --git a/docs/extra.js b/docs/extra.js
new file mode 100644
index 0000000..4af1fa4
--- /dev/null
+++ b/docs/extra.js
@@ -0,0 +1,5 @@
+document.addEventListener("DOMContentLoaded", function () {
+ document.querySelectorAll("table").forEach(function (table) {
+ table.classList.add("docutils");
+ });
+}); \ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index 3155b19..c179c9e 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,4 +1,4 @@
-# Intro
+# Introduction
Gitlint is a git commit message linter written in python: it checks your commit messages for style.
Great for use as a [commit-msg git hook](#using-gitlint-as-a-commit-msg-hook) or as part of your gating script in a
@@ -13,7 +13,11 @@ Great for use as a [commit-msg git hook](#using-gitlint-as-a-commit-msg-hook) or
have a look at [fit-commit](https://github.com/m1foley/fit-commit) (Ruby),
[node-commit-msg](https://github.com/clns/node-commit-msg) (Node.js) or [commitlint](http://marionebl.github.io/commitlint) (Node.js).
-## Features ##
+
+!!! 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)**.
+
+## 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).
- **Easily integrated**: Gitlint is designed to work [with your own scripts or CI system](#using-gitlint-in-a-ci-environment).
- **Sane defaults:** Many of gitlint's validations are based on
@@ -24,14 +28,13 @@ useful throughout the years.
- **Easily configurable:** Gitlint has sane defaults, but [you can also easily customize it to your own liking](configuration.md).
- **Community contributed rules**: Conventions that are common but not universal [can be selectively enabled](contrib_rules).
- **User-defined rules:** Want to do more then what gitlint offers out of the box? Write your own [user defined rules](user_defined_rules.md).
- - **Broad python version support:** Gitlint supports python versions 2.7, 3.5+, PyPy2 and PyPy3.5.
- **Full unicode support:** Lint your Russian, Chinese or Emoji commit messages with ease!
- **Production-ready:** Gitlint checks a lot of the boxes you're looking for: actively maintained, high unit test coverage, integration tests,
python code standards (pep8, pylint), good documentation, widely used, proven track record.
-# Getting Started
-## Installation
-```bash
+## Getting Started
+### Installation
+```sh
# Pip is recommended to install the latest version
pip install gitlint
@@ -43,10 +46,12 @@ brew install gitlint
apt-get install gitlint
# Docker: https://hub.docker.com/r/jorisroovers/gitlint
-docker run -v $(pwd):/repo jorisroovers/gitlint
+docker run --ulimit nofile=1024 -v $(pwd):/repo jorisroovers/gitlint
+# NOTE: --ulimit is required to work around a limitation in Docker
+# Details: https://github.com/jorisroovers/gitlint/issues/129
```
-## Usage
+### Usage
```sh
# Check the last commit message
gitlint
@@ -64,7 +69,7 @@ gitlint install-hook
```
Output example:
-```bash
+```sh
$ cat examples/commit-message-2 | gitlint
1: T1 Title exceeds max length (134>80): "This is the title of a commit message that is over 80 characters and contains hard tabs and trailing whitespace and the word wiping "
1: T2 Title has trailing whitespace: "This is the title of a commit message that is over 80 characters and contains hard tabs and trailing whitespace and the word wiping "
@@ -77,11 +82,11 @@ $ cat examples/commit-message-2 | gitlint
!!! note
The returned exit code equals the number of errors found. [Some exit codes are special](index.md#exit-codes).
-# Configuration
+## Configuration
For in-depth documentation of general and rule-specific configuration options, have a look at the [Configuration](configuration.md) and [Rules](rules.md) pages.
-Short example ```.gitlint``` file ([full reference](configuration.md)):
+Short example `.gitlint` file ([full reference](configuration.md)):
```ini
[general]
@@ -103,7 +108,7 @@ line-length=123
Example use of flags:
-```bash
+```sh
# Change gitlint's verbosity.
$ gitlint -v
# Ignore certain rules
@@ -160,13 +165,13 @@ Commands:
```
-# Using gitlint as a commit-msg hook ##
+## Using gitlint as a commit-msg hook
_Introduced in gitlint v0.4.0_
-You can also install gitlint as a git ```commit-msg``` hook so that gitlint checks your commit messages automatically
+You can also install gitlint as a git `commit-msg` hook so that gitlint checks your commit messages automatically
after each commit.
-```bash
+```sh
gitlint install-hook
# To remove the hook
gitlint uninstall-hook
@@ -174,13 +179,13 @@ gitlint uninstall-hook
!!! important
- Gitlint cannot work together with an existing hook. If you already have a ```.git/hooks/commit-msg```
- file in your local repository, gitlint will refuse to install the ```commit-msg``` hook. Gitlint will also only
+ Gitlint cannot work together with an existing hook. If you already have a `.git/hooks/commit-msg`
+ file in your local repository, gitlint will refuse to install the `commit-msg` hook. Gitlint will also only
uninstall unmodified commit-msg hooks that were installed by gitlint.
If you're looking to use gitlint in conjunction with other hooks, you should consider
[using gitlint with pre-commit](#using-gitlint-through-pre-commit).
-# Using gitlint through [pre-commit](https://pre-commit.com)
+## Using gitlint through [pre-commit](https://pre-commit.com)
`gitlint` can be configured as a plugin for the `pre-commit` git hooks
framework. Simply add the configuration to your `.pre-commit-config.yaml`:
@@ -198,10 +203,10 @@ pre-commit install --hook-type commit-msg
```
!!! important
- It's important that you run ```pre-commit install --hook-type commit-msg```, even if you've already used
- ```pre-commit install``` before. ```pre-commit install``` does **not** install commit-msg hooks by default!
+ It's important that you run `pre-commit install --hook-type commit-msg`, even if you've already used
+ `pre-commit install` before. `pre-commit install` does **not** install commit-msg hooks by default!
-To manually trigger gitlint using ```pre-commit``` for your last commit message, use the following command:
+To manually trigger gitlint using `pre-commit` for your last commit message, use the following command:
```sh
pre-commit run gitlint --hook-stage commit-msg --commit-msg-filename .git/COMMIT_EDITMSG
```
@@ -219,8 +224,8 @@ your `.pre-commit-config.yaml` file like so:
args: [--contrib=CT1, --msg-filename]
```
-# Using gitlint in a CI environment ##
-By default, when just running ```gitlint``` without additional parameters, gitlint lints the last commit in the current
+## 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.
This makes it easy to use gitlint in a CI environment (Jenkins, TravisCI, Github Actions, pre-commit, CircleCI, Gitlab, etc).
@@ -229,22 +234,22 @@ In fact, this is exactly what we do ourselves: on every commit,
This will cause the build to fail when we submit a bad commit message.
Alternatively, gitlint will also lint any commit message that you feed it via stdin like so:
-```bash
+```sh
# lint the last commit message
git log -1 --pretty=%B | gitlint
# lint a specific commit: 62c0519
git log -1 --pretty=%B 62c0519 | gitlint
```
-Note that gitlint requires that you specify ```--pretty=%B``` (=only print the log message, not the metadata),
-future versions of gitlint might fix this and not require the ```--pretty``` argument.
+Note that gitlint requires that you specify `--pretty=%B` (=only print the log message, not the metadata),
+future versions of gitlint might fix this and not require the `--pretty` argument.
-## Linting a range of commits ##
+## Linting a range of commits
_Introduced in gitlint v0.9.0 (experimental in v0.8.0)_
-Gitlint allows users to commit a number of commits at once like so:
+Gitlint allows users to lint a number of commits at once like so:
-```bash
+```sh
# Lint a specific commit range:
gitlint --commits "019cf40...d6bc75a"
# You can also use git's special references:
@@ -253,16 +258,14 @@ gitlint --commits "origin..HEAD"
gitlint --commits "019cf40^...019cf40"
```
-The ```--commits``` flag takes a **single** refspec argument or commit range. Basically, any range that is understood
+The `--commits` flag takes a **single** refspec argument or commit range. Basically, any range that is understood
by [git rev-list](https://git-scm.com/docs/git-rev-list) as a single argument will work.
-Prior to v0.8.1 gitlint didn't support this feature. However, older versions of gitlint can still lint a range or set
-of commits at once by creating a simple bash script that pipes the commit messages one by one into gitlint. This
-approach can still be used with newer versions of gitlint in case ```--commits``` doesn't provide the flexibility you
-are looking for.
+For cases where the `--commits` option doesn't provide the flexibility you need, you can always use a simple shell
+script to lint an arbitrary set of commits, like shown in the example below.
-```bash
-#!/bin/bash
+```sh
+#!/bin/sh
for commit in $(git rev-list master); do
commit_msg=$(git log -1 --pretty=%B $commit)
@@ -275,10 +278,10 @@ done
!!! note
One downside to this approach is that you invoke gitlint once per commit vs. once per set of commits.
This means you'll incur the gitlint startup time once per commit, making this approach rather slow if you want to
- lint a large set of commits. Always use ```--commits``` if you can to avoid this performance penalty.
+ lint a large set of commits. Always use `--commits` if you can to avoid this performance penalty.
-# Merge, fixup and squash commits ##
+## Merge, fixup, squash and revert commits
_Introduced in gitlint v0.7.0 (merge), v0.9.0 (fixup, squash) and v0.13.0 (revert)_
**Gitlint ignores merge, revert, fixup and squash commits by default.**
@@ -298,20 +301,20 @@ short-lived and not intended to make it into the final commit history. In additi
(e.g. [title-max-length](rules.md#t1-title-max-length)) which is often undesirable.
In case you *do* want to lint these commit messages, you can disable this behavior by setting the
-general ```ignore-merge-commits```, ```ignore-revert-commits```, ```ignore-fixup-commits``` or
-```ignore-squash-commits``` option to ```false```
+general `ignore-merge-commits`, `ignore-revert-commits`, `ignore-fixup-commits` or
+`ignore-squash-commits` option to `false`
[using one of the various ways to configure gitlint](configuration.md).
-# Ignoring commits ##
+## Ignoring commits
_Introduced in gitlint v0.10.0_
-You can configure gitlint to ignore specific commits.
+You can configure gitlint to ignore specific commits or parts of a commit.
One way to do this, is to by [adding a gitline-ignore line to your commit message](configuration.md#commit-specific-config).
If you have a case where you want to ignore a certain type of commits all-together, you can
use gitlint's *ignore* rules.
-Here's an example gitlint file that configures gitlint to ignore rules ```title-max-length``` and ```body-min-length```
+Here's an example gitlint file that configures gitlint to ignore rules `title-max-length` and `body-min-length`
for all commits with a title starting with *"Release"*.
```ini
@@ -328,13 +331,88 @@ regex=(.*)release(.*)
ignore=all
```
+If you just want to ignore certain lines in a commit, you can do that using the
+[ignore-body-lines](rules.md#i3-ignore-body-lines) rule.
+
+```ini
+# Ignore all lines that start with 'Co-Authored-By'
+[ignore-body-lines]
+regex=^Co-Authored-By
+```
+
+!!! warning
+
+ When ignoring specific lines, gitlint will no longer be aware of them while applying other rules.
+ This can sometimes be confusing for end-users, especially as line numbers of violations will typically no longer
+ match line numbers in the original commit message. Make sure to educate your users accordingly.
+
!!! note
- Right now it's not possible to write user-defined ignore rules to handle more complex use-cases.
- This is however something that we'd like to implement in a future version. If this is something you're interested in
- please let us know by [opening an issue](https://github.com/jorisroovers/gitlint/issues).
+ If you want to implement more complex ignore rules according to your own logic, you can do so using [user-defined
+ configuration rules](user_defined_rules.md#configuration-rules).
+
+## Named Rules
+
+Introduced in gitlint v0.14.0
+
+Named rules allow you to have multiple of the same rules active at the same time, which allows you to
+enforce the same rule multiple times but with different options. Named rules are so-called because they require an
+additional unique identifier (i.e. the rule *name*) during configuration.
+
+!!! warning
+
+ Named rules is an advanced topic. It's easy to make mistakes by defining conflicting instances of the same rule.
+ For example, by defining 2 `body-max-line-length` rules with different `line-length` options, you obviously create
+ a conflicting situation. Gitlint does not do any resolution of such conflicts, it's up to you to make sure
+ any configuration is non-conflicting. So caution advised!
+
+Defining a named rule is easy, for example using your `.gitlint` file:
+
+```ini
+# By adding the following section, you will add a second instance of the
+# title-must-not-contain-word (T5) rule (in addition to the one that is enabled
+# by default) with the name 'extra-words'.
+[title-must-not-contain-word:extra-words]
+words=foo,bar
+
+# So the generic form is
+# [<rule-id-or-name>:<your-chosen-name>]
+# Another example, referencing the rule type by id
+[T5:more-words]
+words=hur,dur
+
+# You can add as many additional rules and you can name them whatever you want
+# The only requirement is that names cannot contain whitespace or colons (:)
+[title-must-not-contain-word:This-Can_Be*Whatever$YouWant]
+words=wonderwoman,batman,power ranger
+```
+
+When executing gitlint, you will see the violations from the default `title-must-not-contain-word (T5)` rule, as well as
+the violations caused by the additional Named Rules.
+
+```sh
+$ gitlint
+1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: foo wonderwoman hur bar"
+1: T5:This-Can_Be*Whatever$YouWant Title contains the word 'wonderwoman' (case-insensitive): "WIP: foo wonderwoman hur bar"
+1: T5:extra-words Title contains the word 'foo' (case-insensitive): "WIP: foo wonderwoman hur bar"
+1: T5:extra-words Title contains the word 'bar' (case-insensitive): "WIP: foo wonderwoman hur bar"
+1: T5:more-words Title contains the word 'hur' (case-insensitive): "WIP: foo wonderwoman hur bar"
+```
+
+Named rules are further treated identical to all other rules in gitlint:
+
+- You can reference them by their full name, when e.g. adding them to your `ignore` configuration
+```ini
+# .gitlint file example
+[general]
+ignore=T5:more-words,title-must-not-contain-word:extra-words
+```
+
+- You can use them to instantiate multiple of the same [user-defined rule](user_defined_rules.md)
+- You can configure them using [any of the ways you can configure regular gitlint rules](configuration.md)
+
-# Exit codes ##
+## Exit codes
Gitlint uses the exit code as a simple way to indicate the number of violations found.
Some exit codes are used to indicate special errors as indicated in the table below.
@@ -346,6 +424,6 @@ to 252.
Exit Code | Description
-----------|------------------------------------------------------------
-253 | Wrong invocation of the ```gitlint``` command.
+253 | Wrong invocation of the `gitlint` command.
254 | Something went wrong when invoking git.
-255 | Invalid gitlint configuration
+255 | Invalid gitlint configuration \ No newline at end of file
diff --git a/docs/rules.md b/docs/rules.md
index 173c5b1..178f962 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -1,9 +1,12 @@
-# Overview #
+# Overview
-The table below shows an overview of all gitlint's built-in rules.
-Note that you can also [write your own user-defined rule](user_defined_rules.md) in case you don't find
+The table below shows an overview of all gitlint's built-in rules, with more specific details further down the page.
+
+Gitlint also has [community **contrib**uted rules](contrib_rules.md) which are not listed here as they're disabled by default.
+
+In addition, you can also [write your own user-defined rule](user_defined_rules.md) in case you don't find
what you're looking for.
-The rest of this page contains details on the available configuration options for each built-in rule.
+
ID | Name | gitlint version | Description
------|-----------------------------|-------------------|-------------------------------------------
@@ -13,7 +16,8 @@ T3 | title-trailing-punctuation | >= 0.1.0 | Title cannot have trai
T4 | title-hard-tab | >= 0.1.0 | Title cannot contain hard tab characters (\t)
T5 | title-must-not-contain-word | >= 0.1.0 | Title cannot contain certain words (default: "WIP")
T6 | title-leading-whitespace | >= 0.4.0 | Title cannot have leading whitespace (space or tab)
-T7 | title-match-regex | >= 0.5.0 | Title must match a given regex (default: .*)
+T7 | title-match-regex | >= 0.5.0 | Title must match a given regex (default: None)
+T8 | title-max-length | >= 0.14.0 | Title length must be &gt; 5 chars.
B1 | body-max-line-length | >= 0.1.0 | Lines in the body must be &lt; 80 chars
B2 | body-trailing-whitespace | >= 0.1.0 | Body cannot have trailing whitespace (space or tab)
B3 | body-hard-tab | >= 0.1.0 | Body cannot contain hard tab characters (\t)
@@ -21,107 +25,181 @@ B4 | body-first-line-empty | >= 0.1.0 | First line of the body
B5 | body-min-length | >= 0.4.0 | Body length must be at least 20 characters
B6 | body-is-missing | >= 0.4.0 | Body message must be specified
B7 | body-changed-file-mention | >= 0.4.0 | Body must contain references to certain files if those files are changed in the last commit
+B8 | body-match-regex | >= 0.14.0 | Title must match a given regex (default: None)
M1 | author-valid-email | >= 0.9.0 | Author email address must be a valid email address
I1 | ignore-by-title | >= 0.10.0 | Ignore a commit based on matching its title
I2 | ignore-by-body | >= 0.10.0 | Ignore a commit based on matching its body
+I3 | ignore-body-lines | >= 0.14.0 | Ignore certain lines in a commit body that match a regex
+
-## T1: title-max-length ##
+## T1: title-max-length
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
T1 | title-max-length | >= 0.1 | Title length must be &lt; 72 chars.
-### Options ###
+### Options
Name | gitlint version | Default | Description
---------------|-----------------|---------|----------------------------------
line-length | >= 0.2 | 72 | Maximum allowed title length
-## T2: title-trailing-whitespace ##
+### Examples
+
+#### .gitlint
+
+```ini
+# Titles should be max 72 chars
+[title-max-length]
+line-length=72
+
+# It's the 21st century, titles can be 120 chars long
+[title-max-length]
+line-length=120
+```
+
+## T2: title-trailing-whitespace
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
T2 | title-trailing-whitespace | >= 0.1 | Title cannot have trailing whitespace (space or tab)
-## T3: title-trailing-punctuation ##
+## T3: title-trailing-punctuation
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
T3 | title-trailing-punctuation | >= 0.1 | Title cannot have trailing punctuation (?:!.,;)
-## T4: title-hard-tab ##
+## T4: title-hard-tab
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
T4 | title-hard-tab | >= 0.1 | Title cannot contain hard tab characters (\t)
-## T5: title-must-not-contain-word ##
+## T5: title-must-not-contain-word
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
T5 | title-must-not-contain-word | >= 0.1 | Title cannot contain certain words (default: "WIP")
-### Options ###
+### Options
Name | gitlint version | Default | Description
---------------|-----------------|---------|----------------------------------
words | >= 0.3 | WIP | Comma-separated list of words that should not be used in the title. Matching is case insensitive
-## T6: title-leading-whitespace ##
+### Examples
+
+#### .gitlint
+
+```ini
+# Ensure the title doesn't contain swear words
+[title-must-not-contain-word]
+words=crap,darn,damn
+```
+
+## T6: title-leading-whitespace
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
T6 | title-leading-whitespace | >= 0.4 | Title cannot have leading whitespace (space or tab)
-## T7: title-match-regex ##
+## T7: title-match-regex
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
T7 | title-match-regex | >= 0.5 | Title must match a given regex (default: .*)
-### Options ###
+### Options
Name | gitlint version | Default | Description
---------------|-----------------|---------|----------------------------------
-regex | >= 0.5 | .* | [Python-style regular expression](https://docs.python.org/3.5/library/re.html) that the title should match.
+regex | >= 0.5 | .* | [Python regex](https://docs.python.org/library/re.html) that the title should match.
+
+### Examples
+
+#### .gitlint
+
+```ini
+# Ensure every title starts with a user-story like US123
+[title-match-regex]
+regex=^US[1-9][0-9]*
+```
-## B1: body-max-line-length ##
+## T8: title-min-length ##
+
+ID | Name | gitlint version | Description
+------|-----------------------------|-----------------|-------------------------------------------
+T1 | title-min-length | >= | Title length must be &gt; 5 chars.
+
+
+### Options
+
+Name | gitlint version | Default | Description
+---------------|-----------------|---------|----------------------------------
+min-length | >= 0.14.0 | 5 | Minimum required title length
+
+### Examples
+
+#### .gitlint
+
+```ini
+# Titles should be min 3 chars
+[title-min-length]
+min-length=3
+```
+
+## B1: body-max-line-length
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
B1 | body-max-line-length | >= 0.1 | Lines in the body must be &lt; 80 chars
-### Options ###
+### Options
Name | gitlint version | Default | Description
---------------|-----------------|---------|----------------------------------
line-length | >= 0.2 | 80 | Maximum allowed line length in the commit message body
-## B2: body-trailing-whitespace ##
+### Examples
+
+#### .gitlint
+
+```ini
+# It's the 21st century, lines can be 120 chars long
+[body-max-line-length]
+line-length=120
+
+# Your tool prefers 72
+[body-max-line-length]
+line-length=72
+```
+
+## B2: body-trailing-whitespace
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
B2 | body-trailing-whitespace | >= 0.1 | Body cannot have trailing whitespace (space or tab)
-## B3: body-hard-tab ##
+## B3: body-hard-tab
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
B3 | body-hard-tab | >= 0.1 | Body cannot contain hard tab characters (\t)
-## B4: body-first-line-empty ##
+## B4: body-first-line-empty
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
B4 | body-first-line-empty | >= 0.1 | First line of the body (second line of commit message) must be empty
-## B5: body-min-length ##
+## B5: body-min-length
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
@@ -133,34 +211,83 @@ Name | gitlint version | Default | Description
---------------|-----------------|---------|----------------------------------
min-length | >= 0.4 | 20 | Minimum number of required characters in body
-## B6: body-is-missing ##
+### Examples
+
+#### .gitlint
+
+```ini
+# You want *something* in every commit body, but doesn't have to be as long as 20 chars.
+[body-min-length]
+min-length=5
+
+# You want a more elaborate message in every commit body
+[body-min-length]
+min-length=100
+```
+
+## B6: body-is-missing
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
B6 | body-is-missing | >= 0.4 | Body message must be specified
-### Options ###
+### Options
Name | gitlint version | Default | Description
----------------------|-----------------|-----------|----------------------------------
ignore-merge-commits | >= 0.4 | true | Whether this rule should be ignored during merge commits. Allowed values: true,false.
-## B7: body-changed-file-mention ##
+## B7: body-changed-file-mention
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
B7 | body-changed-file-mention | >= 0.4 | Body must contain references to certain files if those files are changed in the last commit
-### Options ###
+### Options
Name | gitlint version | Default | Description
----------------------|-----------------|--------------|----------------------------------
files | >= 0.4 | (empty) | Comma-separated list of files that need to an explicit mention in the commit message in case they are changed.
+### Examples
+
+#### .gitlint
+
+```ini
+# 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
+```
+
+## B8: body-match-regex
+ID | Name | gitlint version | Description
+------|-----------------------------|-----------------|-------------------------------------------
+B8 | body-match-regex | >= 0.14 | Body must match a given regex
+
+### Options
+
+Name | gitlint version | Default | Description
+----------------------|-----------------|--------------|----------------------------------
+regex | >= 0.14 | None | [Python regex](https://docs.python.org/library/re.html) that the title should match.
+
+### Examples
+
+#### .gitlint
+
+```ini
+# Ensure the body ends with Reviewed-By: <some value>
+[body-match-regex]
+regex=Reviewed-By:(.*)$
+
+# Ensure body contains the word "Foo" somewhere
+[body-match-regex]
+regex=(*.)Foo(.*)
+```
-## M1: author-valid-email ##
+## M1: author-valid-email
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
@@ -172,30 +299,36 @@ M1 | author-valid-email | >= 0.8.3 | Author email address mus
-### Options ###
+### Options
Name | gitlint version | Default | Description
----------------------|-------------------|------------------------------|----------------------------------
-regex | >= 0.9.0 | ```[^@ ]+@[^@ ]+\.[^@ ]+``` | Regex the commit author email address is matched against
+regex | >= 0.9.0 | `[^@ ]+@[^@ ]+\.[^@ ]+` | [Python regex](https://docs.python.org/library/re.html) the commit author email address is matched against
-!!! note
- An often recurring use-case is to only allow email addresses from a certain domain. The following regular expression achieves this: ```[^@]+@foo.com```
+### Examples
+#### .gitlint
+
+```ini
+# Only allow email addresses from a foo.com domain
+[author-valid-email]
+regex=[^@]+@foo.com
+```
-## I1: ignore-by-title ##
+## I1: ignore-by-title
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
I1 | ignore-by-title | >= 0.10.0 | Ignore a commit based on matching its title.
-### Options ###
+### Options
Name | gitlint version | Default | Description
----------------------|-------------------|------------------------------|----------------------------------
-regex | >= 0.10.0 | None | Regex to match against commit title. On match, the commit will be ignored.
-ignore | >= 0.10.0 | all | Comma-seperated list of rule names or ids to ignore when this rule is matched.
+regex | >= 0.10.0 | None | [Python regex](https://docs.python.org/library/re.html) to match against commit title. On match, the commit will be ignored.
+ignore | >= 0.10.0 | all | Comma-separated list of rule names or ids to ignore when this rule is matched.
### Examples
@@ -211,19 +344,19 @@ ignore=title-max-length,body-min-length
# ignore=all
```
-## I2: ignore-by-body ##
+## I2: ignore-by-body
ID | Name | gitlint version | Description
------|-----------------------------|-----------------|-------------------------------------------
I2 | ignore-by-body | >= 0.10.0 | Ignore a commit based on matching its body.
-### Options ###
+### Options
Name | gitlint version | Default | Description
----------------------|-------------------|------------------------------|----------------------------------
-regex | >= 0.10.0 | None | Regex to match against each line of the body. On match, the commit will be ignored.
-ignore | >= 0.10.0 | all | Comma-seperated list of rule names or ids to ignore when this rule is matched.
+regex | >= 0.10.0 | None | [Python regex](https://docs.python.org/library/re.html) to match against each line of the body. On match, the commit will be ignored.
+ignore | >= 0.10.0 | all | Comma-separated list of rule names or ids to ignore when this rule is matched.
### Examples
@@ -240,4 +373,35 @@ ignore=all
[ignore-by-body]
regex=(.*)release(.*)
ignore=T1,body-min-length,B6
-``` \ No newline at end of file
+```
+
+## I3: ignore-body-lines
+
+ID | Name | gitlint version | Description
+------|-----------------------------|-----------------|-------------------------------------------
+I3 | ignore-body-lines | >= 0.14.0 | Ignore certain lines in a commit body that match a regex.
+
+
+### Options
+
+Name | gitlint version | Default | Description
+----------------------|-------------------|------------------------------|----------------------------------
+regex | >= 0.14.0 | None | [Python regex](https://docs.python.org/library/re.html) to match against each line of the body. On match, that line will be ignored by gitlint (the rest of the body will still be linted).
+
+### Examples
+
+#### .gitlint
+
+```ini
+# Ignore all lines that start with 'Co-Authored-By'
+[ignore-body-lines]
+regex=^Co-Authored-By
+
+# Ignore lines that start with 'Co-Authored-By' or with 'Signed-Off-By'
+[ignore-body-lines]
+regex=(^Co-Authored-By)|(^Signed-Off-By)
+
+# Ignore lines that contain 'foobar'
+[ignore-body-lines]
+regex=(.*)foobar(.*)
+```
diff --git a/docs/user_defined_rules.md b/docs/user_defined_rules.md
index a8a51d5..13ea544 100644
--- a/docs/user_defined_rules.md
+++ b/docs/user_defined_rules.md
@@ -3,22 +3,23 @@ _Introduced in gitlint v0.8.0_
Gitlint supports the concept of **user-defined** rules: the ability for users to write their own custom rules in python.
-In a nutshell, use ```--extra-path /home/joe/myextensions``` to point gitlint to a ```myextensions``` directory where it will search
+In a nutshell, use `--extra-path /home/joe/myextensions` to point gitlint to a `myextensions` directory where it will search
for python files containing gitlint rule classes. You can also specify a single python module, ie
-```--extra-path /home/joe/my_rules.py```.
+`--extra-path /home/joe/my_rules.py`.
-```bash
+```sh
cat examples/commit-message-1 | gitlint --extra-path examples/
# Example output of a user-defined Signed-Off-By rule
1: UC2 Body does not contain a 'Signed-Off-By Line'
# other violations were removed for brevity
```
-The `SignedOffBy` user-defined ```CommitRule``` was discovered by gitlint when it scanned
-[examples/gitlint/my_commit_rules.py](https://github.com/jorisroovers/gitlint/blob/master/examples/my_commit_rules.py),
-which is part of the examples directory that was passed via ```--extra-path```:
+The `SignedOffBy` user-defined `CommitRule` was discovered by gitlint when it scanned
+[examples/gitlint/my_commit_rules.py](https://github.com/jorisroovers/gitlint/blob/main/examples/my_commit_rules.py),
+which is part of the examples directory that was passed via `--extra-path`:
```python
+# -*- coding: utf-8 -*-
from gitlint.rules import CommitRule, RuleViolation
class SignedOffBy(CommitRule):
@@ -35,6 +36,8 @@ class SignedOffBy(CommitRule):
id = "UC2"
def validate(self, commit):
+ self.log.debug("SignedOffBy: This will be visible when running `gitlint --debug`")
+
for line in commit.message.body:
if line.startswith("Signed-Off-By"):
return
@@ -43,12 +46,12 @@ class SignedOffBy(CommitRule):
return [RuleViolation(self.id, msg, line_nr=1)]
```
-As always, ```--extra-path``` can also be set by adding it under the ```[general]``` section in your ```.gitlint``` file or using
+As always, `--extra-path` can also be set by adding it under the `[general]` section in your `.gitlint` file or using
[one of the other ways to configure gitlint](configuration.md).
-If you want to check whether your rules are properly discovered by gitlint, you can use the ```--debug``` flag:
+If you want to check whether your rules are properly discovered by gitlint, you can use the `--debug` flag:
-```bash
+```sh
$ gitlint --debug --extra-path examples/
# [output cut for brevity]
UC1: body-max-line-count
@@ -60,34 +63,35 @@ $ gitlint --debug --extra-path examples/
!!! Note
In most cases it's really the easiest to just copy an example from the
- [examples](https://github.com/jorisroovers/gitlint/tree/master/examples) directory and modify it to your needs.
+ [examples](https://github.com/jorisroovers/gitlint/tree/main/examples) directory and modify it to your needs.
The remainder of this page contains the technical details, mostly for reference.
-# Line and Commit Rules ##
-The ```SignedOffBy``` class above was an example of a user-defined ```CommitRule```. Commit rules are gitlint rules that
+## Line and Commit Rules
+The `SignedOffBy` class above was an example of a user-defined `CommitRule`. Commit rules are gitlint rules that
act on the entire commit at once. Once the rules are discovered, gitlint will automatically take care of applying them
to the entire commit. This happens exactly once per commit.
-A ```CommitRule``` contrasts with a ```LineRule```
-(see e.g.: [examples/my_line_rules.py](https://github.com/jorisroovers/gitlint/blob/master/examples/my_line_rules.py))
-in that a ```CommitRule``` is only applied once on an entire commit while a ```LineRule``` is applied for every line in the commit
-(you can also apply it once to the title using a ```target``` - see the examples section below).
+A `CommitRule` contrasts with a `LineRule`
+(see e.g.: [examples/my_line_rules.py](https://github.com/jorisroovers/gitlint/blob/main/examples/my_line_rules.py))
+in that a `CommitRule` is only applied once on an entire commit while a `LineRule` is applied for every line in the commit
+(you can also apply it once to the title using a `target` - see the examples section below).
The benefit of a commit rule is that it allows commit rules to implement more complex checks that span multiple lines and/or checks
that should only be done once per commit.
-While every ```LineRule``` can be implemented as a ```CommitRule```, it's usually easier and more concise to go with a ```LineRule``` if
+While every `LineRule` can be implemented as a `CommitRule`, it's usually easier and more concise to go with a `LineRule` if
that fits your needs.
-## Examples ##
+### Examples
-In terms of code, writing your own ```CommitRule``` or ```LineRule``` is very similar.
-The only 2 differences between a ```CommitRule``` and a ```LineRule``` are the parameters of the ```validate(...)``` method and the extra
-```target``` attribute that ```LineRule``` requires.
+In terms of code, writing your own `CommitRule` or `LineRule` is very similar.
+The only 2 differences between a `CommitRule` and a `LineRule` are the parameters of the `validate(...)` method and the extra
+`target` attribute that `LineRule` requires.
-Consider the following ```CommitRule``` that can be found in [examples/my_commit_rules.py](https://github.com/jorisroovers/gitlint/blob/master/examples/my_commit_rules.py):
+Consider the following `CommitRule` that can be found in [examples/my_commit_rules.py](https://github.com/jorisroovers/gitlint/blob/main/examples/my_commit_rules.py):
```python
+# -*- coding: utf-8 -*-
from gitlint.rules import CommitRule, RuleViolation
class SignedOffBy(CommitRule):
@@ -104,18 +108,21 @@ class SignedOffBy(CommitRule):
id = "UC2"
def validate(self, commit):
+ self.log.debug("SignedOffBy: This will be visible when running `gitlint --debug`")
+
for line in commit.message.body:
if line.startswith("Signed-Off-By"):
- return []
+ return
- msg = "Body does not contain a 'Signed-Off-By Line'"
+ msg = "Body does not contain a 'Signed-Off-By' line"
return [RuleViolation(self.id, msg, line_nr=1)]
```
-Note the use of the ```name``` and ```id``` class attributes and the ```validate(...)``` method taking a single ```commit``` parameter.
+Note the use of the `name` and `id` class attributes and the `validate(...)` method taking a single `commit` parameter.
-Contrast this with the following ```LineRule``` that can be found in [examples/my_line_rules.py](https://github.com/jorisroovers/gitlint/blob/master/examples/my_line_rules.py):
+Contrast this with the following `LineRule` that can be found in [examples/my_line_rules.py](https://github.com/jorisroovers/gitlint/blob/main/examples/my_line_rules.py):
```python
+# -*- coding: utf-8 -*-
from gitlint.rules import LineRule, RuleViolation, CommitMessageTitle
from gitlint.options import ListOption
@@ -138,33 +145,37 @@ class SpecialChars(LineRule):
options_spec = [ListOption('special-chars', ['$', '^', '%', '@', '!', '*', '(', ')'],
"Comma separated list of characters that should not occur in the title")]
- def validate(self, line, commit):
+ def validate(self, line, _commit):
+ self.log.debug("SpecialChars: This will be visible when running `gitlint --debug`")
+
violations = []
- # option values can be accessed via self.options
+ # 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:
- violation = RuleViolation(self.id, "Title contains the special character '{}'".format(char), line)
+ msg = "Title contains the special character '{0}'".format(char)
+ violation = RuleViolation(self.id, msg, line)
violations.append(violation)
return violations
+
```
Note the following 2 differences:
-- **extra ```target``` class attribute**: in this example set to ```CommitMessageTitle``` indicating that this ```LineRule```
-should only be applied once to the commit message title. The alternative value for ```target``` is ```CommitMessageBody```,
+- **extra `target` class attribute**: in this example set to `CommitMessageTitle` indicating that this `LineRule`
+should only be applied once to the commit message title. The alternative value for `target` is `CommitMessageBody`,
in which case gitlint will apply
your rule to **every** line in the commit message body.
-- **```validate(...)``` takes 2 parameters**: Line rules get the ```line``` against which they are applied as the first parameter and
-the ```commit``` object of which the line is part of as second.
+- **`validate(...)` takes 2 parameters**: Line rules get the `line` against which they are applied as the first parameter and
+the `commit` object of which the line is part of as second.
-In addition, you probably also noticed the extra ```options_spec``` class attribute which allows you to make your rules configurable.
-Options are not unique to ```LineRule```s, they can also be used by ```CommitRule```s and are further explained in the
+In addition, you probably also noticed the extra `options_spec` class attribute which allows you to make your rules configurable.
+Options are not unique to `LineRule`s, they can also be used by `CommitRule`s and are further explained in the
[Options](user_defined_rules.md#options) section below.
-# The commit object ##
-Both ```CommitRule```s and ```LineRule```s take a ```commit``` object in their ```validate(...)``` methods.
+## The commit object
+Both `CommitRule`s and `LineRule`s take a `commit` object in their `validate(...)` methods.
The table below outlines the various attributes of that commit object that can be used during validation.
@@ -175,14 +186,14 @@ commit.message.original | string | Original commit message as ret
commit.message.full | string | Full commit message, with comments (lines starting with #) removed.
commit.message.title | string | Title/subject of the commit message: the first line
commit.message.body | string[] | List of lines in the body of the commit message (i.e. starting from the second line)
-commit.author_name | string | Name of the author, result of ```git log --pretty=%aN```
-commit.author_email | string | Email of the author, result of ```git log --pretty=%aE```
-commit.date | datetime | Python ```datetime``` object representing the time of commit
+commit.author_name | string | Name of the author, result of `git log --pretty=%aN`
+commit.author_email | string | Email of the author, result of `git log --pretty=%aE`
+commit.date | datetime | Python `datetime` object representing the time of commit
commit.is_merge_commit | boolean | Boolean indicating whether the commit is a merge commit or not.
commit.is_revert_commit | boolean | Boolean indicating whether the commit is a revert commit or not.
commit.is_fixup_commit | boolean | Boolean indicating whether the commit is a fixup commit or not.
commit.is_squash_commit | boolean | Boolean indicating whether the commit is a squash commit or not.
-commit.parents | string[] | List of parent commit ```sha```s (only for merge commits).
+commit.parents | string[] | List of parent commit `sha`s (only for merge commits).
commit.changed_files | string[] | List of files changed in the commit (relative paths).
commit.branches | string[] | List of branch names the commit is part of
commit.context | object | Object pointing to the bigger git context that the commit is part of
@@ -190,17 +201,17 @@ commit.context.current_branch | string | Name of the currently active b
commit.context.repository_path | string | Absolute path pointing to the git repository being linted
commit.context.commits | object[] | List of commits gitlint is acting on, NOT all commits in the repo.
-# Violations ##
-In order to let gitlint know that there is a violation in the commit being linted, users should have the ```validate(...)```
-method in their rules return a list of ```RuleViolation```s.
+## Violations
+In order to let gitlint know that there is a violation in the commit being linted, users should have the `validate(...)`
+method in their rules return a list of `RuleViolation`s.
!!! important
- The ```validate(...)``` method doesn't always need to return a list, you can just skip the return statement in case there are no violations.
+ The `validate(...)` method doesn't always need to return a list, you can just skip the return statement in case there are no violations.
However, in case of a single violation, validate should return a **list** with a single item.
-The ```RuleViolation``` class has the following generic signature:
+The `RuleViolation` class has the following generic signature:
-```
+```python
RuleViolation(rule_id, message, content=None, line_nr=None):
```
With the parameters meaning the following:
@@ -210,9 +221,9 @@ Parameter | Type | Description
rule_id | string | Rule's unique string id
message | string | Short description of the violation
content | string | (optional) the violating part of commit or line
-line_nr | int | (optional) line number in the commit message where the violation occurs. **Automatically set to the correct line number for ```LineRule```s if not set explicitly.**
+line_nr | int | (optional) line number in the commit message where the violation occurs. **Automatically set to the correct line number for `LineRule`s if not set explicitly.**
-A typical ```validate(...)``` implementation for a ```CommitRule``` would then be as follows:
+A typical `validate(...)` implementation for a `CommitRule` would then be as follows:
```python
def validate(self, commit)
for line_nr, line in commit.message.body:
@@ -222,16 +233,17 @@ def validate(self, commit)
return []
```
-The parameters of this ```RuleViolation``` can be directly mapped onto gitlint's output as follows:
+The parameters of this `RuleViolation` can be directly mapped onto gitlint's output as follows:
![How Rule violations map to gitlint output](images/RuleViolation.png)
-# Options ##
+## Options
-In order to make your own rules configurable, you can add an optional ```options_spec``` attribute to your rule class
-(supported for both ```LineRule``` and ```CommitRule```).
+In order to make your own rules configurable, you can add an optional `options_spec` attribute to your rule class
+(supported for both `LineRule` and `CommitRule`).
```python
+# -*- coding: utf-8 -*-
from gitlint.rules import CommitRule, RuleViolation
from gitlint.options import IntOption
@@ -256,57 +268,143 @@ class BodyMaxLineCount(CommitRule):
```
-By using ```options_spec```, you make your option available to be configured through a ```.gitlint``` file
+By using `options_spec`, you make your option available to be configured through a `.gitlint` file
or one of the [other ways to configure gitlint](configuration.md). Gitlint automatically takes care of the parsing and input validation.
-For example, to change the value of the ```max-line-count``` option, add the following to your ```.gitlint``` file:
+For example, to change the value of the `max-line-count` option, add the following to your `.gitlint` file:
```ini
[body-max-line-count]
body-max-line-count=1
```
-As ```options_spec``` is a list, you can obviously have multiple options per rule. The general signature of an option is:
-```Option(name, default_value, description)```.
+As `options_spec` is a list, you can obviously have multiple options per rule. The general signature of an option is:
+`Option(name, default_value, description)`.
-Gitlint supports a variety of different option types, all can be imported from ```gitlint.options```:
+Gitlint supports a variety of different option types, all can be imported from `gitlint.options`:
-Option Class | Use for
-----------------|--------------
-StrOption | Strings
-IntOption | Integers. ```IntOption``` takes an optional ```allow_negative``` parameter if you want to allow negative integers.
-BoolOption | Booleans. Valid values: `true`, `false`. Case-insensitive.
-ListOption | List of strings. Comma separated.
-PathOption | Directory or file path. Takes an optional ```type``` parameter for specifying path type (```file```, ```dir``` (=default) or ```both```).
+Option Class | Use for
+------------------|--------------
+`StrOption ` | Strings
+`IntOption` | Integers. `IntOption` takes an optional `allow_negative` parameter if you want to allow negative integers.
+`BoolOption` | Booleans. Valid values: `true`, `false`. Case-insensitive.
+`ListOption` | List of strings. Comma separated.
+`PathOption` | Directory or file path. Takes an optional `type` parameter for specifying path type (`file`, `dir` (=default) or `both`).
+`RegexOption` | String representing a [Python-style regex](https://docs.python.org/library/re.html) - compiled and validated before rules are applied.
!!! note
Gitlint currently does not support options for all possible types (e.g. float, list of int, etc).
[We could use a hand getting those implemented](contributing.md)!
-# Rule requirements ##
+## Configuration Rules
+
+_Introduced in gitlint v0.14.0_
+
+Configuration rules are special rules that are applied once per commit and *BEFORE* any other rules are run.
+Configuration rules are meant to dynamically change gitlint's configuration and/or the commit that is about to be
+linted.
+A typically use-case for this is when you want to modifying gitlint's behavior for all rules against a commit matching
+specific circumstances.
+
+!!! warning
+ Configuration rules can drastically change the way gitlint behaves and are typically only needed for more advanced
+ use-cases. We recommend you double check:
+
+ 1. Whether gitlint already supports your use-case out-of-the-box (special call-out for [ignore rules](rules.md#i1-ignore-by-title) which allow you to ignore (parts) of your commit message).
+ 2. Whether there's a [Contrib Rule](contrib_rules.md) that implements your use-case.
+ 3. Whether you can implement your use-case using a regular Commit or Line user-defined rule (see above).
+
+
+As with other user-defined rules, the easiest way to get started is by copying [`my_configuration.py` from the examples directory](https://github.com/jorisroovers/gitlint/tree/main/examples/my_configuration_rules.py) and modifying it to fit your need.
+
+```python
+# -*- coding: utf-8 -*-
+from gitlint.rules import ConfigurationRule
+from gitlint.options import IntOption
+
+class ReleaseConfigurationRule(ConfigurationRule):
+ """
+ This rule will modify gitlint's behavior for Release Commits.
+
+ This example might not be the most realistic for a real-world scenario,
+ but is meant to give an overview of what's possible.
+ """
+
+ # A rule MUST have a human friendly name
+ name = "release-configuration-rule"
+
+ # A rule MUST have a *unique* id, we recommend starting with UCR
+ # (for User-defined Configuration-Rule), but this can really be anything.
+ id = "UCR1"
+
+ # A rule MAY have an option_spec if its behavior should be configurable.
+ options_spec = [IntOption('custom-verbosity', 2, "Gitlint verbosity for release commits")]
+
+ def apply(self, config, commit):
+ self.log.debug("ReleaseConfigurationRule: This will be visible when running `gitlint --debug`")
+
+ # If the commit title starts with 'Release', we want to modify
+ # how all subsequent rules interpret that commit
+ if commit.message.title.startswith("Release"):
+
+ # If your Release commit messages are auto-generated, the
+ # body might contain trailing whitespace. Let's ignore that
+ config.ignore.append("body-trailing-whitespace")
+
+ # Similarly, the body lines might exceed 80 chars,
+ # let's set gitlint's limit to 200
+ # To set rule options use:
+ # config.set_rule_option(<rule-name>, <rule-option>, <value>)
+ config.set_rule_option("body-max-line-length", "line-length", 200)
+
+ # For kicks, let's set gitlint's verbosity to 2
+ # To set general options use
+ # config.set_general_option(<general-option>, <value>)
+ config.set_general_option("verbosity", 2)
+ # We can also use custom options to make this configurable
+ config.set_general_option("verbosity", self.options['custom-verbosity'].value)
+
+ # Strip any lines starting with $ from the commit message
+ # (this only affects how gitlint sees your commit message, it does
+ # NOT modify your actual commit in git)
+ commit.message.body = [line for line in commit.message.body if not line.startswith("$")]
+
+ # 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"
+```
+
+For all available properties and methods on the `config` object, have a look at the
+[LintConfig class](https://github.com/jorisroovers/gitlint/blob/main/gitlint/config.py). Please do not use any
+properties or methods starting with an underscore, as those are subject to change.
+
+
+## Rule requirements
As long as you stick with simple rules that are similar to the sample user-defined rules (see the
-[examples](https://github.com/jorisroovers/gitlint/blob/master/examples/my_commit_rules.py) directory), gitlint
+[examples](https://github.com/jorisroovers/gitlint/blob/main/examples/my_commit_rules.py) directory), gitlint
should be able to discover and execute them. While clearly you can run any python code you want in your rules,
you might run into some issues if you don't follow the conventions that gitlint requires.
-While the [rule finding source-code](https://github.com/jorisroovers/gitlint/blob/master/gitlint/rule_finder.py) is the
+While the [rule finding source-code](https://github.com/jorisroovers/gitlint/blob/main/gitlint/rule_finder.py) is the
ultimate source of truth, here are some of the requirements that gitlint enforces.
-## Rule class requirements ###
+### Rule class requirements
-- Rules **must** extend from ```LineRule``` or ```CommitRule```
-- Rule classes **must** have ```id``` and ```name``` string attributes. The ```options_spec``` is optional,
+- Rules **must** extend from `LineRule`, `CommitRule` or `ConfigurationRule`
+- Rule classes **must** have `id` and `name` string attributes. The `options_spec` is optional,
but if set, it **must** be a list of gitlint Options.
-- Rule classes **must** have a ```validate``` method. In case of a ```CommitRule```, ```validate``` **must** take a single ```commit``` parameter.
- In case of ```LineRule```, ```validate``` **must** take ```line``` and ```commit``` as first and second parameters.
-- LineRule classes **must** have a ```target``` class attributes that is set to either ```CommitMessageTitle``` or ```CommitMessageBody```.
-- User Rule id's **cannot** start with ```R```, ```T```, ```B``` or ```M``` as these rule ids are reserved for gitlint itself.
-- Rules **should** have a case-insensitive unique id as only one rule can exist with a given id. While gitlint does not enforce this, having multiple rules with
- the same id might lead to unexpected or undeterministic behavior.
-
-## extra-path requirements ###
-- If ```extra-path``` is a directory, it does **not** need to be a proper python package, i.e. it doesn't require an ```__init__.py``` file.
-- Python files containing user-defined rules must have a ```.py``` extension. Files with a different extension will be ignored.
-- The ```extra-path``` will be searched non-recursively, i.e. all rule classes must be present at the top level ```extra-path``` directory.
-- User rule classes must be defined in the modules that are part of ```extra-path```, rules that are imported from outside the ```extra-path``` will be ignored.
+- `CommitRule` and `LineRule` classes **must** have a `validate` method.
+- In case of a `CommitRule`, `validate` **must** take a single `commit` parameter.
+- In case of `LineRule`, `validate` **must** take `line` and `commit` as first and second parameters.
+- `ConfigurationRule` classes **must** have an `apply` method that take `config` and `commit` as first and second parameters.
+- LineRule classes **must** have a `target` class attributes that is set to either `CommitMessageTitle` or `CommitMessageBody`.
+- User Rule id's **cannot** start with `R`, `T`, `B`, `M` or `I` as these rule ids are reserved for gitlint itself.
+- Rules **should** have a case-insensitive unique id as only one rule can exist with a given id. While gitlint does not
+ enforce this, having multiple rules with the same id might lead to unexpected or undeterministic behavior.
+
+### extra-path requirements
+- If `extra-path` is a directory, it does **not** need to be a proper python package, i.e. it doesn't require an `__init__.py` file.
+- Python files containing user-defined rules must have a `.py` extension. Files with a different extension will be ignored.
+- The `extra-path` will be searched non-recursively, i.e. all rule classes must be present at the top level `extra-path` directory.
+- User rule classes must be defined in the modules that are part of `extra-path`, rules that are imported from outside the `extra-path` will be ignored.
diff --git a/examples/commit-message-11 b/examples/commit-message-11
new file mode 100644
index 0000000..72c7fa8
--- /dev/null
+++ b/examples/commit-message-11
@@ -0,0 +1,7 @@
+Release: Holy Smokes, Batman!
+
+This release contains a bunch of features.
+
+- Here's a description of a feature that exceeds the default maximum line length of 80 characters
+
+$ my-fancy-tool: this line is auto-generated by our release tool and is always too long $ \ No newline at end of file
diff --git a/examples/my_commit_rules.py b/examples/my_commit_rules.py
index e12e02d..c64008f 100644
--- a/examples/my_commit_rules.py
+++ b/examples/my_commit_rules.py
@@ -1,9 +1,13 @@
+# -*- coding: utf-8 -*-
+
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
+
The classes below are examples of user-defined CommitRules. Commit rules are gitlint rules that
act on the entire commit at once. Once the rules are discovered, gitlint will automatically take care of applying them
to the entire commit. This happens exactly once per commit.
@@ -28,6 +32,8 @@ class BodyMaxLineCount(CommitRule):
options_spec = [IntOption('max-line-count', 3, "Maximum body line count")]
def validate(self, commit):
+ self.log.debug("BodyMaxLineCount: This will be visible when running `gitlint --debug`")
+
line_count = len(commit.message.body)
max_line_count = self.options['max-line-count'].value
if line_count > max_line_count:
@@ -47,6 +53,8 @@ class SignedOffBy(CommitRule):
id = "UC2"
def validate(self, commit):
+ self.log.debug("SignedOffBy: This will be visible when running `gitlint --debug`")
+
for line in commit.message.body:
if line.startswith("Signed-Off-By"):
return
@@ -69,6 +77,8 @@ class BranchNamingConventions(CommitRule):
options_spec = [ListOption('branch-prefixes', ["feature/", "hotfix/", "release/"], "Allowed branch prefixes")]
def validate(self, commit):
+ self.log.debug("BranchNamingConventions: This line will be visible when running `gitlint --debug`")
+
violations = []
allowed_branch_prefixes = self.options['branch-prefixes'].value
for branch in commit.branches:
diff --git a/examples/my_configuration_rules.py b/examples/my_configuration_rules.py
new file mode 100644
index 0000000..58de048
--- /dev/null
+++ b/examples/my_configuration_rules.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+
+from gitlint.rules import ConfigurationRule
+from gitlint.options import IntOption
+
+
+"""
+Full details on user-defined rules: https://jorisroovers.com/gitlint/user_defined_rules
+
+The ReleaseConfigurationRule class below is an example of a user-defined ConfigurationRule. Configuration rules are
+gitlint rules that are applied once per commit and BEFORE any other rules are run. Configuration Rules are meant to
+dynamically change gitlint's configuration and/or the commit that is about to be linted. A typically use-case for this
+is modifying the behavior of gitlint's rules based on a commit contents.
+
+Notes:
+- Modifying the commit object DOES NOT modify the actual git commit message in the target repo, only gitlint's copy of
+ it.
+- Modifying the config object only has effect on the commit that is being linted, subsequent commits will not
+ automatically inherit this configuration.
+"""
+
+
+class ReleaseConfigurationRule(ConfigurationRule):
+ """
+ This rule will modify gitlint's behavior for Release Commits.
+
+ This example might not be the most realistic for a real-world scenario,
+ but is meant to give an overview of what's possible.
+ """
+
+ # A rule MUST have a human friendly name
+ name = "release-configuration-rule"
+
+ # A rule MUST have a *unique* id, we recommend starting with UCR
+ # (for User-defined Configuration-Rule), but this can really be anything.
+ id = "UCR1"
+
+ # A rule MAY have an option_spec if its behavior should be configurable.
+ options_spec = [IntOption('custom-verbosity', 2, "Gitlint verbosity for release commits")]
+
+ def apply(self, config, commit):
+ self.log.debug("ReleaseConfigurationRule: This will be visible when running `gitlint --debug`")
+
+ # If the commit title starts with 'Release', we want to modify
+ # how all subsequent rules interpret that commit
+ if commit.message.title.startswith("Release"):
+
+ # If your Release commit messages are auto-generated, the
+ # body might contain trailing whitespace. Let's ignore that
+ config.ignore.append("body-trailing-whitespace")
+
+ # Similarly, the body lines might exceed 80 chars,
+ # let's set gitlint's limit to 200
+ # To set rule options use:
+ # config.set_rule_option(<rule-name>, <rule-option>, <value>)
+ config.set_rule_option("body-max-line-length", "line-length", 200)
+
+ # For kicks, let's set gitlint's verbosity to 2
+ # To set general options use
+ # config.set_general_option(<general-option>, <value>)
+ config.set_general_option("verbosity", 2)
+ # Wwe can also use custom options to make this configurable
+ config.set_general_option("verbosity", self.options['custom-verbosity'].value)
+
+ # Strip any lines starting with $ from the commit message
+ # (this only affects how gitlint sees your commit message, it does
+ # NOT modify your actual commit in git)
+ commit.message.body = [line for line in commit.message.body if not line.startswith("$")]
+
+ # 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"
diff --git a/examples/my_line_rules.py b/examples/my_line_rules.py
index cc69fb9..820024a 100644
--- a/examples/my_line_rules.py
+++ b/examples/my_line_rules.py
@@ -1,7 +1,11 @@
+# -*- coding: utf-8 -*-
+
from gitlint.rules import LineRule, RuleViolation, CommitMessageTitle
from gitlint.options import ListOption
"""
+Full details on user-defined rules: https://jorisroovers.com/gitlint/user_defined_rules
+
The SpecialChars class below is an example of a user-defined LineRule. Line rules are gitlint rules that only act on a
single line at once. Once the rule is discovered, gitlint will automatically take care of applying this rule
against each line of the commit message title or body (whether it is applied to the title or body is determined by the
@@ -35,11 +39,14 @@ class SpecialChars(LineRule):
"Comma separated list of characters that should not occur in the title")]
def validate(self, line, _commit):
+ self.log.debug("SpecialChars: This will be visible when running `gitlint --debug`")
+
violations = []
# 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:
- violation = RuleViolation(self.id, "Title contains the special character '{0}'".format(char), line)
+ msg = "Title contains the special character '{0}'".format(char)
+ violation = RuleViolation(self.id, msg, line)
violations.append(violation)
return violations
diff --git a/gitlint/__init__.py b/gitlint/__init__.py
index 7e0dc0e..9e78220 100644
--- a/gitlint/__init__.py
+++ b/gitlint/__init__.py
@@ -1 +1 @@
-__version__ = "0.13.1"
+__version__ = "0.14.0"
diff --git a/gitlint/cli.py b/gitlint/cli.py
index 4553fda..f284792 100644
--- a/gitlint/cli.py
+++ b/gitlint/cli.py
@@ -19,14 +19,19 @@ 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.utils import ustr, LOG_FORMAT
+from gitlint.shell import shell
+from gitlint.utils import ustr, LOG_FORMAT, IS_PY2
DEFAULT_CONFIG_FILE = ".gitlint"
+# -n: disable swap files. This fixes a vim error on windows (E303: Unable to open swap file for <path>)
+DEFAULT_COMMIT_MSG_EDITOR = "vim -n"
# Since we use the return code to denote the amount of errors, we need to change the default click usage error code
click.UsageError.exit_code = USAGE_ERROR_CODE
-LOG = logging.getLogger(__name__)
+# We don't use logging.getLogger(__main__) here because that will cause DEBUG output to be lost
+# when invoking gitlint as a python module (python -m gitlint.cli)
+LOG = logging.getLogger("gitlint.cli")
class GitLintUsageError(Exception):
@@ -51,6 +56,7 @@ def log_system_info():
LOG.debug("Git version: %s", git_version())
LOG.debug("Gitlint version: %s", gitlint.__version__)
LOG.debug("GITLINT_USE_SH_LIB: %s", os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]"))
+ LOG.debug("DEFAULT_ENCODING: %s", gitlint.utils.DEFAULT_ENCODING)
def build_config( # pylint: disable=too-many-arguments
@@ -164,27 +170,44 @@ def build_git_context(lint_config, msg_filename, refspec):
return GitContext.from_local_repository(lint_config.target, refspec)
+class ContextObj(object):
+ """ 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):
+ self.config = config
+ self.config_builder = config_builder
+ self.refspec = refspec
+ self.msg_filename = msg_filename
+ self.gitcontext = gitcontext
+
+
@click.group(invoke_without_command=True, context_settings={'max_content_width': 120},
epilog="When no COMMAND is specified, gitlint defaults to 'gitlint lint'.")
-@click.option('--target', type=click.Path(exists=True, resolve_path=True, file_okay=False, readable=True),
+@click.option('--target', envvar='GITLINT_TARGET',
+ type=click.Path(exists=True, resolve_path=True, file_okay=False, readable=True),
help="Path of the target git repository. [default: current working directory]")
@click.option('-C', '--config', type=click.Path(exists=True, dir_okay=False, readable=True, resolve_path=True),
help="Config file location [default: {0}]".format(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
-@click.option('--commits', default=None, help="The range of commits to lint. [default: HEAD]")
-@click.option('-e', '--extra-path', help="Path to a directory or python module with extra user-defined rules",
+@click.option('--commits', envvar='GITLINT_COMMITS', default=None, help="The range of commits to lint. [default: HEAD]")
+@click.option('-e', '--extra-path', envvar='GITLINT_EXTRA_PATH',
+ help="Path to a directory or python module with extra user-defined rules",
type=click.Path(exists=True, resolve_path=True, readable=True))
-@click.option('--ignore', default="", help="Ignore rules (comma-separated by id or name).")
-@click.option('--contrib', default="", help="Contrib rules to enable (comma-separated by id or name).")
+@click.option('--ignore', envvar='GITLINT_IGNORE', default="", help="Ignore rules (comma-separated by id or name).")
+@click.option('--contrib', envvar='GITLINT_CONTRIB', default="",
+ help="Contrib rules to enable (comma-separated by id or name).")
@click.option('--msg-filename', type=click.File(), help="Path to a file containing a commit-msg.")
-@click.option('--ignore-stdin', is_flag=True, help="Ignore any stdin data. Useful for running in CI server.")
-@click.option('--staged', is_flag=True, help="Read staged commit meta-info from the local repository.")
-@click.option('-v', '--verbose', count=True, default=0,
+@click.option('--ignore-stdin', envvar='GITLINT_IGNORE_STDIN', is_flag=True,
+ help="Ignore any stdin data. Useful for running in CI server.")
+@click.option('--staged', envvar='GITLINT_STAGED', is_flag=True,
+ help="Read staged commit meta-info from the local repository.")
+@click.option('-v', '--verbose', envvar='GITLINT_VERBOSITY', count=True, default=0,
help="Verbosity, more v's for more verbose output (e.g.: -v, -vv, -vvv). [default: -vvv]", )
-@click.option('-s', '--silent', help="Silent mode (no output). Takes precedence over -v, -vv, -vvv.", is_flag=True)
-@click.option('-d', '--debug', help="Enable debugging output.", is_flag=True)
+@click.option('-s', '--silent', envvar='GITLINT_SILENT', is_flag=True,
+ help="Silent mode (no output). Takes precedence over -v, -vv, -vvv.")
+@click.option('-d', '--debug', envvar='GITLINT_DEBUG', help="Enable debugging output.", is_flag=True)
@click.version_option(version=gitlint.__version__)
@click.pass_context
def cli( # pylint: disable=too-many-arguments
@@ -209,7 +232,7 @@ def cli( # pylint: disable=too-many-arguments
ignore_stdin, staged, verbose, silent, debug)
LOG.debug(u"Configuration\n%s", ustr(config))
- ctx.obj = (config, config_builder, commits, msg_filename)
+ ctx.obj = ContextObj(config, config_builder, commits, msg_filename)
# If no subcommand is specified, then just lint
if ctx.invoked_subcommand is None:
@@ -230,11 +253,14 @@ def cli( # pylint: disable=too-many-arguments
@click.pass_context
def lint(ctx):
""" Lints a git repository [default command] """
- lint_config = ctx.obj[0]
- refspec = ctx.obj[2]
- msg_filename = ctx.obj[3]
+ lint_config = ctx.obj.config
+ refspec = ctx.obj.refspec
+ msg_filename = ctx.obj.msg_filename
gitcontext = build_git_context(lint_config, msg_filename, refspec)
+ # Set gitcontext in the click context, so we can use it in command that are ran after this
+ # in particular, this is used by run-hook
+ ctx.obj.gitcontext = gitcontext
number_of_commits = len(gitcontext.commits)
# Exit if we don't have commits in the specified range. Use a 0 exit code, since a popular use-case is one
@@ -245,7 +271,7 @@ def lint(ctx):
ctx.exit(0)
LOG.debug(u'Linting %d commit(s)', number_of_commits)
- general_config_builder = ctx.obj[1]
+ general_config_builder = ctx.obj.config_builder
last_commit = gitcontext.commits[-1]
# Let's get linting!
@@ -287,9 +313,8 @@ def lint(ctx):
def install_hook(ctx):
""" Install gitlint as a git commit-msg hook. """
try:
- lint_config = ctx.obj[0]
- hooks.GitHookInstaller.install_commit_msg_hook(lint_config)
- hook_path = hooks.GitHookInstaller.commit_msg_hook_path(lint_config)
+ 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))
ctx.exit(0)
except hooks.GitHookInstallerError as e:
@@ -302,9 +327,8 @@ def install_hook(ctx):
def uninstall_hook(ctx):
""" Uninstall gitlint commit-msg hook. """
try:
- lint_config = ctx.obj[0]
- hooks.GitHookInstaller.uninstall_commit_msg_hook(lint_config)
- hook_path = hooks.GitHookInstaller.commit_msg_hook_path(lint_config)
+ 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))
ctx.exit(0)
except hooks.GitHookInstallerError as e:
@@ -312,6 +336,80 @@ def uninstall_hook(ctx):
ctx.exit(GIT_CONTEXT_ERROR_CODE)
+@cli.command("run-hook")
+@click.pass_context
+def run_hook(ctx):
+ """ Runs the gitlint commit-msg hook. """
+
+ exit_code = 1
+ while exit_code > 0:
+ try:
+ click.echo(u"gitlint: checking commit message...")
+ ctx.invoke(lint)
+ except click.exceptions.Exit as e:
+ # Flush stderr andstdout, this resolves an issue with output ordering in Cygwin
+ sys.stderr.flush()
+ sys.stdout.flush()
+
+ exit_code = e.exit_code
+ if exit_code == 0:
+ click.echo(u"gitlint: " + click.style("OK", fg='green') + u" (no violations in commit message)")
+ continue
+
+ click.echo(u"-----------------------------------------------")
+ click.echo(u"gitlint: " + click.style("Your commit message contains the above violations.", fg='red'))
+
+ value = None
+ while value not in ["y", "n", "e"]:
+ click.echo("Continue with commit anyways (this keeps the current commit message)? "
+ "[y(es)/n(no)/e(dit)] ", nl=False)
+
+ # Ideally, we'd want to use click.getchar() or click.prompt() to get user's input here instead of
+ # input(). However, those functions currently don't support getting answers from stdin.
+ # This wouldn't be a huge issue since this is unlikely to occur in the real world,
+ # were it not that we use a stdin to pipe answers into gitlint in our integration tests.
+ # If that ever changes, we can revisit this.
+ # Related click pointers:
+ # - https://github.com/pallets/click/issues/1370
+ # - 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()
+
+ if value == "y":
+ LOG.debug("run-hook: commit message accepted")
+ exit_code = 0
+ elif value == "e":
+ LOG.debug("run-hook: editing commit message")
+ msg_filename = ctx.obj.msg_filename
+ if msg_filename:
+ msg_filename.seek(0)
+ editor = os.environ.get("EDITOR", DEFAULT_COMMIT_MSG_EDITOR)
+ msg_filename_path = os.path.realpath(msg_filename.name)
+ 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.")
+ 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(ctx.obj.gitcontext.commits[0].message.full)
+ click.echo(u"-----------------------------------------------")
+ ctx.exit(exit_code)
+
+ ctx.exit(exit_code)
+
+
@cli.command("generate-config")
@click.pass_context
def generate_config(ctx):
diff --git a/gitlint/config.py b/gitlint/config.py
index 914357e..4dad707 100644
--- a/gitlint/config.py
+++ b/gitlint/config.py
@@ -12,7 +12,7 @@ import os
import shutil
from collections import OrderedDict
-from gitlint.utils import ustr, DEFAULT_ENCODING
+from gitlint.utils import ustr, sstr, 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
@@ -44,6 +44,7 @@ class LintConfig(object):
# Default tuple of rule classes (tuple because immutable).
default_rule_classes = (rules.IgnoreByTitle,
rules.IgnoreByBody,
+ rules.IgnoreBodyLines,
rules.TitleMaxLength,
rules.TitleTrailingWhitespace,
rules.TitleLeadingWhitespace,
@@ -51,6 +52,7 @@ class LintConfig(object):
rules.TitleHardTab,
rules.TitleMustNotContainWord,
rules.TitleRegexMatches,
+ rules.TitleMinLength,
rules.BodyMaxLineLength,
rules.BodyMinLength,
rules.BodyMissing,
@@ -58,6 +60,7 @@ class LintConfig(object):
rules.BodyHardTab,
rules.BodyFirstLineEmpty,
rules.BodyChangedFileMention,
+ rules.BodyRegexMatches,
rules.AuthorValidEmail)
def __init__(self):
@@ -290,7 +293,7 @@ class LintConfig(object):
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(self.contrib)
+ 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)
@@ -365,13 +368,20 @@ class RuleCollection(object):
def __len__(self):
return len(self._rules)
- def __str__(self):
+ def __repr__(self):
+ return self.__unicode__() # pragma: no cover
+
+ def __unicode__(self):
return_str = ""
for rule in self._rules.values():
return_str += u" {0}: {1}\n".format(rule.id, rule.name)
for option_name, option_value in sorted(rule.options.items()):
- if isinstance(option_value.value, list):
+ if option_value.value is None:
+ option_val_repr = None
+ elif isinstance(option_value.value, list):
option_val_repr = ",".join(option_value.value)
+ elif isinstance(option_value, options.RegexOption):
+ 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)
@@ -385,13 +395,15 @@ class LintConfigBuilder(object):
normalized, validated and build. Example usage can be found in gitlint.cli.
"""
+ RULE_QUALIFIER_SYMBOL = ":"
+
def __init__(self):
- self._config_blueprint = {}
+ self._config_blueprint = OrderedDict()
self._config_path = None
def set_option(self, section, option_name, option_value):
if section not in self._config_blueprint:
- self._config_blueprint[section] = {}
+ self._config_blueprint[section] = OrderedDict()
self._config_blueprint[section][option_name] = option_value
def set_config_from_commit(self, commit):
@@ -438,10 +450,43 @@ class LintConfigBuilder(object):
except ConfigParserError as e:
raise LintConfigError(ustr(e))
+ def _add_named_rule(self, config, qualified_rule_name):
+ """ Adds a Named Rule to a given LintConfig object.
+ IMPORTANT: This method does *NOT* overwrite existing Named Rules with the same canonical id.
+ """
+
+ # Split up named rule in its parts: the name/id that specifies the parent rule,
+ # And the name of the rule instance itself
+ rule_name_parts = qualified_rule_name.split(self.RULE_QUALIFIER_SYMBOL, 1)
+ rule_name = rule_name_parts[1].strip()
+ parent_rule_specifier = rule_name_parts[0].strip()
+
+ # assert that the rule name is valid:
+ # - 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))
+
+ # 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))
+
+ # 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
+ canonical_name = parent_rule.__class__.name + self.RULE_QUALIFIER_SYMBOL + rule_name
+
+ # Add the rule to the collection of rules if it's not there already
+ if not config.rules.find_rule(canonical_id):
+ config.rules.add_rule(parent_rule.__class__, canonical_id, {'is_named': True, 'name': canonical_name})
+
+ return canonical_id
+
def build(self, config=None):
""" Build a real LintConfig object by normalizing and validating the options that were previously set on this
factory. """
-
# If we are passed a config object, then rebuild that object instead of building a new lintconfig object from
# scratch
if not config:
@@ -459,6 +504,12 @@ class LintConfigBuilder(object):
for option_name, option_value in section_dict.items():
# Skip over the general section, as we've already done that above
if section_name != "general":
+
+ # If the section name contains a colon (:), then this section is defining a Named Rule
+ # Which means we need to instantiate that Named Rule in the config.
+ if self.RULE_QUALIFIER_SYMBOL in section_name:
+ section_name = self._add_named_rule(config, section_name)
+
config.set_rule_option(section_name, option_name, option_value)
return config
diff --git a/gitlint/contrib/rules/conventional_commit.py b/gitlint/contrib/rules/conventional_commit.py
index 3bbbd0f..8530343 100644
--- a/gitlint/contrib/rules/conventional_commit.py
+++ b/gitlint/contrib/rules/conventional_commit.py
@@ -17,7 +17,7 @@ class ConventionalCommit(LineRule):
options_spec = [
ListOption(
"types",
- ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert"],
+ ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert", "ci", "build"],
"Comma separated list of allowed commit types.",
)
]
diff --git a/gitlint/display.py b/gitlint/display.py
index dd17ac0..c66a256 100644
--- a/gitlint/display.py
+++ b/gitlint/display.py
@@ -1,12 +1,13 @@
import codecs
import locale
-from sys import stdout, stderr, version_info
+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 version_info[0] == 2:
+if IS_PY2:
stdout = codecs.getwriter(locale.getpreferredencoding())(stdout) # pylint: disable=invalid-name
stderr = codecs.getwriter(locale.getpreferredencoding())(stderr) # pylint: disable=invalid-name
diff --git a/gitlint/files/commit-msg b/gitlint/files/commit-msg
index e468290..6a25d34 100644
--- a/gitlint/files/commit-msg
+++ b/gitlint/files/commit-msg
@@ -8,74 +8,28 @@ stdin_available=1
(exec < /dev/tty) 2> /dev/null || stdin_available=0
if [ $stdin_available -eq 1 ]; then
- # Set bash color codes in case we have a tty
- RED="\033[31m"
- YELLOW="\033[33m"
- GREEN="\033[32m"
- END_COLOR="\033[0m"
-
# Now that we know we have a functional tty, set stdin to it so we can ask the user questions :-)
exec < /dev/tty
-else
- # Unset bash colors if we don't have a tty
- RED=""
- YELLOW=""
- GREEN=""
- END_COLOR=""
-fi
-run_gitlint(){
- echo "gitlint: checking commit message..."
- python -m gitlint.cli --staged --msg-filename "$1"
- gitlint_exit_code=$?
-}
-
-# Prompts a given yes/no question.
-# Returns 0 if user answers yes, 1 if no
-# Reprompts if different answer
-ask_yes_no_edit(){
- ask_yes_no_edit_result="no"
- # If we don't have a stdin available, then just return "No".
- if [ $stdin_available -eq 0 ]; then
- ask_yes_no_edit_result="no"
- return;
+ # On Windows, we need to explicitely set our stdout to the tty to make terminal editing work (e.g. vim)
+ # See SO for windows detection in bash (slight modified to work on plain shell (not bash)):
+ # https://stackoverflow.com/questions/394230/how-to-detect-the-os-from-a-bash-script
+ if [ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ] || [ "$OSTYPE" = "win32" ]; then
+ exec > /dev/tty
fi
- # Otherwise, ask the question until the user answers yes or no
- question="$1"
- while true; do
- read -p "$question" yn
- case $yn in
- [Yy]* ) ask_yes_no_edit_result="yes"; return;;
- [Nn]* ) ask_yes_no_edit_result="no"; return;;
- [Ee]* ) ask_yes_no_edit_result="edit"; return;;
- esac
- done
-}
-
-run_gitlint "$1"
+fi
-while [ $gitlint_exit_code -gt 0 ]; do
- echo "-----------------------------------------------"
- echo "gitlint: ${RED}Your commit message contains the above violations.${END_COLOR}"
- ask_yes_no_edit "Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] "
- if [ $ask_yes_no_edit_result = "yes" ]; then
- exit 0
- elif [ $ask_yes_no_edit_result = "edit" ]; then
- EDITOR=${EDITOR:-vim}
- $EDITOR "$1"
- run_gitlint "$1"
- else
- echo "Commit aborted."
- echo "Your commit message: "
- echo "-----------------------------------------------"
- cat "$1"
- echo "-----------------------------------------------"
+gitlint --staged --msg-filename "$1" run-hook
+exit_code=$?
- exit $gitlint_exit_code
- fi
-done
+# If we fail to find the gitlint binary (command not found), let's retry by executing as a python module.
+# This is the case for Atlassian SourceTree, where $PATH deviates from the user's shell $PATH.
+if [ $exit_code -eq 127 ]; then
+ echo "Fallback to python module execution"
+ python -m gitlint.cli --staged --msg-filename "$1" run-hook
+ exit_code=$?
+fi
-echo "gitlint: ${GREEN}OK${END_COLOR} (no violations in commit message)"
-exit 0
+exit $exit_code
### gitlint commit-msg hook end ###
diff --git a/gitlint/files/gitlint b/gitlint/files/gitlint
index 15a6626..e95bf9e 100644
--- a/gitlint/files/gitlint
+++ b/gitlint/files/gitlint
@@ -4,7 +4,7 @@
# one rule and each key in it is an option for that specific rule.
#
# Rules and sections can be referenced by their full name or by id. For example
-# section "[body-max-line-length]" could be written as "[B1]". Full section names are
+# section "[body-max-line-length]" could also be written as "[B1]". Full section names are
# used in here for clarity.
#
# [general]
@@ -43,6 +43,11 @@
# [title-max-length]
# line-length=50
+# Conversely, you can also enforce minimal length of a title with the
+# "title-min-length" rule:
+# [title-min-length]
+# min-length=5
+
# [title-must-not-contain-word]
# Comma-separated list of words that should not occur in the title. Matching is case
# insensitive. It's fine if the keyword occurs as part of a larger word (so "WIPING"
@@ -50,8 +55,7 @@
# words=wip
# [title-match-regex]
-# python like regex (https://docs.python.org/2/library/re.html) that the
-# commit-msg title must be matched to.
+# python-style regex that the commit-msg title must match
# Note that the regex can contradict with other rules if not used correctly
# (e.g. title-must-not-contain-word).
# regex=^US[0-9]*
@@ -74,9 +78,13 @@
# it in the commit message.
# files=gitlint/rules.py,README.md
+# [body-match-regex]
+# python-style regex that the commit-msg body must match.
+# E.g. body must end in My-Commit-Tag: foo
+# regex=My-Commit-Tag: foo$
+
# [author-valid-email]
-# python like regex (https://docs.python.org/2/library/re.html) that the
-# commit author email address should be matched to
+# python-style regex that the commit author email address must match.
# For example, use the following regex if you only want to allow email addresses from foo.com
# regex=[^@]+@foo.com
@@ -98,9 +106,14 @@
# Use 'all' to ignore all rules
# ignore=T1,body-min-length
+# [ignore-body-lines]
+# Ignore certain lines in a commit body that match a regex.
+# E.g. Ignore all lines that start with 'Co-Authored-By'
+# regex=^Co-Authored-By
+
# This is a contrib rule - a community contributed rule. These are disabled by default.
# You need to explicitly enable them one-by-one by adding them to the "contrib" option
# under [general] section above.
# [contrib-title-conventional-commits]
# Specify allowed commit types. For details see: https://www.conventionalcommits.org/
-# types = bugfix,user-story,epic \ No newline at end of file
+# types = bugfix,user-story,epic
diff --git a/gitlint/git.py b/gitlint/git.py
index ca7ad92..8e00f89 100644
--- a/gitlint/git.py
+++ b/gitlint/git.py
@@ -1,4 +1,6 @@
+import logging
import os
+
import arrow
from gitlint import shell as sh
@@ -12,6 +14,8 @@ from gitlint.utils import ustr, sstr
# We should fix this at some point :-)
GIT_TIMEFORMAT = "YYYY-MM-DD HH:mm:ss Z"
+LOG = logging.getLogger(__name__)
+
class GitContextError(Exception):
""" Exception indicating there is an issue with the git context """
@@ -25,11 +29,20 @@ class GitNotInstalledError(GitContextError):
u"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))
+
+
def _git(*command_parts, **kwargs):
""" Convenience function for running git commands. Automatically deals with exceptions and unicode. """
git_kwargs = {'_tty_out': False}
git_kwargs.update(kwargs)
try:
+ LOG.debug(sstr(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
@@ -44,12 +57,13 @@ def _git(*command_parts, **kwargs):
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'])
- elif (b"does not have any commits yet" in error_msg_lower or
- b"ambiguous argument 'head': unknown revision" in error_msg_lower):
+ raise GitContextError(error_msg)
+
+ 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.")
- else:
- error_msg = u"An error occurred while executing '{0}': {1}".format(e.full_cmd, error_msg)
- raise GitContextError(error_msg)
+
+ raise GitExitCodeError(e.full_cmd, error_msg)
def git_version():
@@ -291,12 +305,18 @@ class StagedLocalGitCommit(GitCommit, PropertyCache):
@property
@cache
def author_name(self):
- return ustr(_git("config", "--get", "user.name", _cwd=self.context.repository_path)).strip()
+ 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")
@property
@cache
def author_email(self):
- return ustr(_git("config", "--get", "user.email", _cwd=self.context.repository_path)).strip()
+ 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")
@property
@cache
diff --git a/gitlint/options.py b/gitlint/options.py
index a1ae59c..3ea8310 100644
--- a/gitlint/options.py
+++ b/gitlint/options.py
@@ -1,9 +1,22 @@
from abc import abstractmethod
import os
+import re
from gitlint.utils import ustr, sstr
+def allow_none(func):
+ """ Decorator that sets option value to None if the passed value is None, otherwise calls the regular set method """
+
+ def wrapped(obj, value):
+ if value is None:
+ obj.value = None
+ else:
+ func(obj, value)
+
+ return wrapped
+
+
class RuleOptionError(Exception):
pass
@@ -43,6 +56,7 @@ class RuleOption(object):
class StrOption(RuleOption):
+ @allow_none
def set(self, value):
self.value = ustr(value)
@@ -59,6 +73,7 @@ class IntOption(RuleOption):
error_msg = u"Option '{0}' must be a positive integer (current value: '{1}')".format(self.name, value)
raise RuleOptionError(error_msg)
+ @allow_none
def set(self, value):
try:
self.value = int(value)
@@ -70,6 +85,8 @@ class IntOption(RuleOption):
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()
if value not in ['true', 'false']:
@@ -81,6 +98,7 @@ class ListOption(RuleOption):
""" Option that is either a given list or a comma-separated string that can be splitted into a list when being set.
"""
+ @allow_none
def set(self, value):
if isinstance(value, list):
the_list = value
@@ -97,6 +115,7 @@ class PathOption(RuleOption):
self.type = type
super(PathOption, self).__init__(name, value, description)
+ @allow_none
def set(self, value):
value = ustr(value)
@@ -120,3 +139,21 @@ class PathOption(RuleOption):
raise RuleOptionError(error_msg)
self.value = os.path.realpath(value)
+
+
+class RegexOption(RuleOption):
+
+ @allow_none
+ def set(self, value):
+ try:
+ self.value = re.compile(value, re.UNICODE)
+ except (re.error, TypeError) as exc:
+ raise RuleOptionError("Invalid regular expression: '{0}'".format(exc))
+
+ def __deepcopy__(self, _):
+ # copy.deepcopy() - used in rules.py - doesn't support copying regex objects prior to Python 3.7
+ # To work around this, we have to implement this __deepcopy__ magic method
+ # Relevant SO thread:
+ # https://stackoverflow.com/questions/6279305/typeerror-cannot-deepcopy-this-pattern-object
+ value = None if self.value is None else self.value.pattern
+ return RegexOption(self.name, value, self.description)
diff --git a/gitlint/rule_finder.py b/gitlint/rule_finder.py
index 2b8b293..d7d700b 100644
--- a/gitlint/rule_finder.py
+++ b/gitlint/rule_finder.py
@@ -66,7 +66,9 @@ def find_rule_classes(extra_path):
if
inspect.isclass(clazz) and # check isclass to ensure clazz.__module__ exists
clazz.__module__ == module and # ignore imported classes
- (issubclass(clazz, rules.LineRule) or issubclass(clazz, rules.CommitRule))])
+ (issubclass(clazz, rules.LineRule) or
+ issubclass(clazz, rules.CommitRule) or
+ issubclass(clazz, rules.ConfigurationRule))])
# validate that the rule classes are valid user-defined rules
for rule_class in rule_classes:
@@ -75,24 +77,27 @@ def find_rule_classes(extra_path):
return rule_classes
-def assert_valid_rule_class(clazz, rule_type="User-defined"):
+def assert_valid_rule_class(clazz, rule_type="User-defined"): # pylint: disable=too-many-branches
"""
Asserts that a given rule clazz is valid by checking a number of its properties:
- - Rules must extend from LineRule or CommitRule
+ - Rules must extend from LineRule, CommitRule or ConfigurationRule
- Rule classes must have id and name string attributes.
The options_spec is optional, but if set, it must be a list of gitlint Options.
- Rule classes must have a validate method. In case of a CommitRule, validate must take a single commit parameter.
In case of LineRule, validate must take line and commit as first and second parameters.
- LineRule classes must have a target class attributes that is set to either
+ - ConfigurationRule classes must have an apply method that take `config` and `commit` as parameters.
CommitMessageTitle or CommitMessageBody.
- - Rule id's cannot start with R, T, B or M as these rule ids are reserved for gitlint itself.
+ - Rule id's cannot start with R, T, B, M or I as these rule ids are reserved for gitlint itself.
"""
- # Rules must extend from LineRule or CommitRule
- if not (issubclass(clazz, rules.LineRule) or issubclass(clazz, rules.CommitRule)):
- msg = u"{0} rule class '{1}' must extend from {2}.{3} or {2}.{4}"
+ # 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.LineRule.__name__, rules.CommitRule.__name__,
+ rules.ConfigurationRule.__name__))
# Rules must have an id attribute
if not hasattr(clazz, 'id') or clazz.id is None or not clazz.id:
@@ -100,8 +105,8 @@ def assert_valid_rule_class(clazz, rule_type="User-defined"):
raise rules.UserRuleError(msg.format(rule_type, clazz.__name__))
# Rule id's cannot start with gitlint reserved letters
- if clazz.id[0].upper() in ['R', 'T', 'B', 'M']:
- msg = u"The id '{1}' of '{0}' is invalid. Gitlint reserves ids starting with R,T,B,M"
+ 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]))
# Rules must have a name attribute
@@ -122,11 +127,17 @@ def assert_valid_rule_class(clazz, rule_type="User-defined"):
raise rules.UserRuleError(msg.format(rule_type.lower(), clazz.__name__,
options.RuleOption.__module__, options.RuleOption.__name__))
- # Rules must have a validate method. We use isroutine() as it's both python 2 and 3 compatible.
- # For more info see http://stackoverflow.com/a/17019998/381010
- 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__))
+ # 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__))
+ # 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__))
# LineRules must have a valid target: rules.CommitMessageTitle or rules.CommitMessageBody
if issubclass(clazz, rules.LineRule):
diff --git a/gitlint/rules.py b/gitlint/rules.py
index ad83204..1cb50da 100644
--- a/gitlint/rules.py
+++ b/gitlint/rules.py
@@ -3,12 +3,9 @@ import copy
import logging
import re
-from gitlint.options import IntOption, BoolOption, StrOption, ListOption
+from gitlint.options import IntOption, BoolOption, StrOption, ListOption, RegexOption
from gitlint.utils import sstr
-LOG = logging.getLogger(__name__)
-logging.basicConfig()
-
class Rule(object):
""" Class representing gitlint rules. """
@@ -16,6 +13,7 @@ class Rule(object):
id = None
name = None
target = None
+ _log = None
def __init__(self, opts=None):
if not opts:
@@ -27,6 +25,13 @@ class Rule(object):
if actual_option is not None:
self.options[op_spec.name].set(actual_option)
+ @property
+ def log(self):
+ if not self._log:
+ self._log = logging.getLogger(__name__)
+ logging.basicConfig()
+ return self._log
+
def __eq__(self, other):
return self.id == other.id and self.name == other.name and \
self.options == other.options and self.target == other.target # noqa
@@ -102,7 +107,7 @@ class RuleViolation(object):
self.content) # pragma: no cover
def __repr__(self):
- return self.__str__() # pragma: no cover
+ return self.__unicode__() # pragma: no cover
class UserRuleError(Exception):
@@ -126,10 +131,10 @@ class TrailingWhiteSpace(LineRule):
name = "trailing-whitespace"
id = "R2"
violation_message = "Line has trailing whitespace"
+ pattern = re.compile(r"\s$", re.UNICODE)
def validate(self, line, _commit):
- pattern = re.compile(r"\s$", re.UNICODE)
- if pattern.search(line):
+ if self.pattern.search(line):
return [RuleViolation(self.id, self.violation_message, line)]
@@ -226,16 +231,32 @@ class TitleRegexMatches(LineRule):
name = "title-match-regex"
id = "T7"
target = CommitMessageTitle
- options_spec = [StrOption('regex', ".*", "Regex the title should match")]
+ options_spec = [RegexOption('regex', None, "Regex the title should match")]
def validate(self, title, _commit):
- regex = self.options['regex'].value
- pattern = re.compile(regex, re.UNICODE)
- if not pattern.search(title):
- violation_msg = u"Title does not match regex ({0})".format(regex)
+ # If no regex is specified, immediately return
+ if not self.options['regex'].value:
+ return
+
+ if not self.options['regex'].value.search(title):
+ violation_msg = u"Title does not match regex ({0})".format(self.options['regex'].value.pattern)
return [RuleViolation(self.id, violation_msg, title)]
+class TitleMinLength(LineRule):
+ name = "title-min-length"
+ id = "T8"
+ target = CommitMessageTitle
+ options_spec = [IntOption('min-length', 5, "Minimum required title length")]
+
+ def validate(self, title, _commit):
+ 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)
+ return [RuleViolation(self.id, violation_message, title, 1)]
+
+
class BodyMaxLineLength(MaxLineLength):
name = "body-max-line-length"
id = "B1"
@@ -309,55 +330,109 @@ class BodyChangedFileMention(CommitRule):
return violations if violations else None
+class BodyRegexMatches(CommitRule):
+ name = "body-match-regex"
+ id = "B8"
+ options_spec = [RegexOption('regex', None, "Regex the body should match")]
+
+ def validate(self, commit):
+ # If no regex is specified, immediately return
+ if not self.options['regex'].value:
+ return
+
+ # We intentionally ignore the first line in the body as that's the empty line after the title,
+ # which most users are not going to expect to be part of the body when matching a regex.
+ # If this causes contention, we can always introduce an option to change the behavior in a backward-
+ # compatible way.
+ body_lines = commit.message.body[1:] if len(commit.message.body) > 1 else []
+
+ # Similarly, the last line is often empty, this has to do with how git returns commit messages
+ # User's won't expect this, so prune it off by default
+ if body_lines and body_lines[-1] == "":
+ body_lines.pop()
+
+ 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)
+ return [RuleViolation(self.id, violation_msg, None, len(commit.message.body) + 1)]
+
+
class AuthorValidEmail(CommitRule):
name = "author-valid-email"
id = "M1"
- options_spec = [StrOption('regex', r"[^@ ]+@[^@ ]+\.[^@ ]+", "Regex that author email address should match")]
+ options_spec = [RegexOption('regex', r"[^@ ]+@[^@ ]+\.[^@ ]+", "Regex that author email address should match")]
def validate(self, commit):
- # Note that unicode is allowed in email addresses
- # See http://stackoverflow.com/questions/3844431
- # /are-email-addresses-allowed-to-contain-non-alphanumeric-characters
- email_regex = re.compile(self.options['regex'].value, re.UNICODE)
+ # If no regex is specified, immediately return
+ if not self.options['regex'].value:
+ return
- if commit.author_email and not email_regex.match(commit.author_email):
+ if commit.author_email and not self.options['regex'].value.match(commit.author_email):
return [RuleViolation(self.id, "Author email for commit is invalid", commit.author_email)]
class IgnoreByTitle(ConfigurationRule):
name = "ignore-by-title"
id = "I1"
- options_spec = [StrOption('regex', None, "Regex matching the titles of commits this rule should apply to"),
+ options_spec = [RegexOption('regex', None, "Regex matching the titles of commits this rule should apply to"),
StrOption('ignore', "all", "Comma-separated list of rules to ignore")]
def apply(self, config, commit):
- title_regex = re.compile(self.options['regex'].value, re.UNICODE)
+ # If no regex is specified, immediately return
+ if not self.options['regex'].value:
+ return
- if title_regex.match(commit.message.title):
+ 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, self.options['ignore'].value)
+ message = message.format(commit.message.title, self.options['regex'].value.pattern,
+ self.options['ignore'].value)
- LOG.debug("Ignoring commit because of rule '%s': %s", self.id, message)
+ self.log.debug("Ignoring commit because of rule '%s': %s", self.id, message)
class IgnoreByBody(ConfigurationRule):
name = "ignore-by-body"
id = "I2"
- options_spec = [StrOption('regex', None, "Regex matching lines of the body of commits this rule should apply to"),
+ options_spec = [RegexOption('regex', None, "Regex matching lines of the body of commits this rule should apply to"),
StrOption('ignore', "all", "Comma-separated list of rules to ignore")]
def apply(self, config, commit):
- body_line_regex = re.compile(self.options['regex'].value, re.UNICODE)
+ # If no regex is specified, immediately return
+ if not self.options['regex'].value:
+ return
for line in commit.message.body:
- if body_line_regex.match(line):
+ 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, self.options['ignore'].value)
+ message = message.format(line, self.options['regex'].value.pattern, self.options['ignore'].value)
- LOG.debug("Ignoring commit because of rule '%s': %s", self.id, message)
+ self.log.debug("Ignoring commit because of rule '%s': %s", self.id, message)
# No need to check other lines if we found a match
return
+
+
+class IgnoreBodyLines(ConfigurationRule):
+ name = "ignore-body-lines"
+ id = "I3"
+ options_spec = [RegexOption('regex', None, "Regex matching lines of the body that should be ignored")]
+
+ def apply(self, _, commit):
+ # If no regex is specified, immediately return
+ if not self.options['regex'].value:
+ return
+
+ 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'"
+ 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)
diff --git a/gitlint/shell.py b/gitlint/shell.py
index 965f492..2601b04 100644
--- a/gitlint/shell.py
+++ b/gitlint/shell.py
@@ -2,12 +2,18 @@
"""
This module implements a shim for the 'sh' library, mainly for use on Windows (sh is not supported on Windows).
We might consider removing the 'sh' dependency alltogether in the future, but 'sh' does provide a few
-capabilities wrt dealing with more edge-case environments on *nix systems that might be useful.
+capabilities wrt dealing with more edge-case environments on *nix systems that are useful.
"""
import subprocess
-import sys
-from gitlint.utils import ustr, USE_SH_LIB
+from gitlint.utils import ustr, IS_PY2, USE_SH_LIB
+
+
+def shell(cmd):
+ """ Convenience function that opens a given command in a shell. Does not use 'sh' library. """
+ p = subprocess.Popen(cmd, shell=True)
+ p.communicate()
+
if USE_SH_LIB:
from sh import git # pylint: disable=unused-import,import-error
@@ -21,7 +27,7 @@ else:
class ShResult(object):
""" Result wrapper class. We use this to more easily migrate from using https://amoffat.github.io/sh/ to using
- the builtin subprocess. module """
+ the builtin subprocess module """
def __init__(self, full_cmd, stdout, stderr='', exitcode=0):
self.full_cmd = full_cmd
@@ -45,13 +51,13 @@ else:
return _exec(*args, **kwargs)
def _exec(*args, **kwargs):
- if sys.version_info[0] == 2:
+ 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['_tty_out']}
+ popen_kwargs = {'stdout': pipe, 'stderr': pipe, 'shell': kwargs.get('_tty_out', False)}
if '_cwd' in kwargs:
popen_kwargs['cwd'] = kwargs['_cwd']
diff --git a/gitlint/tests/base.py b/gitlint/tests/base.py
index add4d71..c8f68c4 100644
--- a/gitlint/tests/base.py
+++ b/gitlint/tests/base.py
@@ -1,10 +1,13 @@
# -*- coding: utf-8 -*-
+import contextlib
import copy
import io
import logging
import os
import re
+import shutil
+import tempfile
try:
# python 2.x
@@ -21,7 +24,7 @@ except ImportError:
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
from gitlint.git import GitContext
-from gitlint.utils import ustr, LOG_FORMAT, DEFAULT_ENCODING
+from gitlint.utils import ustr, IS_PY2, LOG_FORMAT, DEFAULT_ENCODING
# unittest2's assertRaisesRegex doesn't do unicode comparison.
@@ -57,6 +60,15 @@ class BaseTestCase(unittest.TestCase):
logging.getLogger('gitlint').propagate = False
@staticmethod
+ @contextlib.contextmanager
+ def tempdir():
+ tmpdir = tempfile.mkdtemp()
+ try:
+ yield tmpdir
+ finally:
+ shutil.rmtree(tmpdir)
+
+ @staticmethod
def get_sample_path(filename=""):
# Don't join up empty files names because this will add a trailing slash
if filename == "":
@@ -73,6 +85,15 @@ class BaseTestCase(unittest.TestCase):
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
+
+ @staticmethod
def get_expected(filename="", variable_dict=None):
""" Utility method to read an expected file from gitlint/tests/expected and return it as a string.
Optionally replace template variables specified by variable_dict. """
@@ -129,6 +150,24 @@ class BaseTestCase(unittest.TestCase):
return super(BaseTestCase, self).assertRaisesRegex(expected_exception, re.escape(expected_regex),
*args, **kwargs)
+ @contextlib.contextmanager
+ def assertRaisesMessage(self, expected_exception, expected_msg): # pylint: disable=invalid-name
+ """ Asserts an exception has occurred with a given error message """
+ try:
+ yield
+ except expected_exception as exc:
+ exception_msg = ustr(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))
+ # 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__))
+
+ # 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__))
+
def object_equality_test(self, obj, attr_list, ctor_kwargs=None):
""" Helper function to easily implement object equality tests.
Creates an object clone for every passed attribute and checks for (in)equality
diff --git a/gitlint/tests/cli/test_cli.py b/gitlint/tests/cli/test_cli.py
index 4d47f35..88bcfb7 100644
--- a/gitlint/tests/cli/test_cli.py
+++ b/gitlint/tests/cli/test_cli.py
@@ -1,12 +1,10 @@
# -*- coding: utf-8 -*-
-import contextlib
+
import io
import os
import sys
import platform
-import shutil
-import tempfile
import arrow
@@ -34,15 +32,6 @@ from gitlint import __version__
from gitlint.utils import DEFAULT_ENCODING
-@contextlib.contextmanager
-def tempdir():
- tmpdir = tempfile.mkdtemp()
- try:
- yield tmpdir
- finally:
- shutil.rmtree(tmpdir)
-
-
class CLITests(BaseTestCase):
USAGE_ERROR_CODE = 253
GIT_CONTEXT_ERROR_CODE = 254
@@ -64,7 +53,8 @@ class CLITests(BaseTestCase):
def get_system_info_dict():
""" Returns a dict with items related to system values logged by `gitlint --debug` """
return {'platform': platform.platform(), "python_version": sys.version, 'gitlint_version': __version__,
- 'GITLINT_USE_SH_LIB': BaseTestCase.GITLINT_USE_SH_LIB, 'target': os.path.realpath(os.getcwd())}
+ 'GITLINT_USE_SH_LIB': BaseTestCase.GITLINT_USE_SH_LIB, 'target': os.path.realpath(os.getcwd()),
+ 'DEFAULT_ENCODING': DEFAULT_ENCODING}
def test_version(self):
""" Test for --version option """
@@ -118,7 +108,7 @@ class CLITests(BaseTestCase):
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--commits", "foo...bar"])
- self.assertEqual(stderr.getvalue(), self.get_expected("test_cli/test_lint_multiple_commits_1"))
+ self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_multiple_commits_1"))
self.assertEqual(result.exit_code, 3)
@patch('gitlint.cli.get_stdin_data', return_value=False)
@@ -152,7 +142,7 @@ class CLITests(BaseTestCase):
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--commits", "foo...bar"])
# We expect that the second commit has no failures because of 'gitlint-ignore: T3' in its commit msg body
- self.assertEqual(stderr.getvalue(), self.get_expected("test_cli/test_lint_multiple_commits_config_1"))
+ self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_multiple_commits_config_1"))
self.assertEqual(result.exit_code, 3)
@patch('gitlint.cli.get_stdin_data', return_value=False)
@@ -205,7 +195,7 @@ class CLITests(BaseTestCase):
""" Test for linting when a message is passed via stdin """
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli)
- self.assertEqual(stderr.getvalue(), self.get_expected("test_cli/test_input_stream_1"))
+ self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_input_stream_1"))
self.assertEqual(result.exit_code, 3)
self.assertEqual(result.output, "")
@@ -215,11 +205,11 @@ class CLITests(BaseTestCase):
This tests specifically that git commit meta is not fetched when not passing --staged """
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--debug"])
- self.assertEqual(stderr.getvalue(), self.get_expected("test_cli/test_input_stream_debug_1"))
+ self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_input_stream_debug_1"))
self.assertEqual(result.exit_code, 3)
self.assertEqual(result.output, "")
expected_kwargs = self.get_system_info_dict()
- expected_logs = self.get_expected('test_cli/test_input_stream_debug_2', expected_kwargs)
+ expected_logs = self.get_expected('cli/test_cli/test_input_stream_debug_2', expected_kwargs)
self.assert_logged(expected_logs)
@patch('gitlint.cli.get_stdin_data', return_value="Should be ignored\n")
@@ -259,12 +249,12 @@ class CLITests(BaseTestCase):
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--debug", "--staged"])
- self.assertEqual(stderr.getvalue(), self.get_expected("test_cli/test_lint_staged_stdin_1"))
+ self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_staged_stdin_1"))
self.assertEqual(result.exit_code, 3)
self.assertEqual(result.output, "")
expected_kwargs = self.get_system_info_dict()
- expected_logs = self.get_expected('test_cli/test_lint_staged_stdin_2', expected_kwargs)
+ expected_logs = self.get_expected('cli/test_cli/test_lint_staged_stdin_2', expected_kwargs)
self.assert_logged(expected_logs)
@patch('arrow.now', return_value=arrow.get("2020-02-19T12:18:46.675182+01:00"))
@@ -280,19 +270,19 @@ class CLITests(BaseTestCase):
u"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
]
- with tempdir() as tmpdir:
+ 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")
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--debug", "--staged", "--msg-filename", msg_filename])
- self.assertEqual(stderr.getvalue(), self.get_expected("test_cli/test_lint_staged_msg_filename_1"))
+ self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_staged_msg_filename_1"))
self.assertEqual(result.exit_code, 2)
self.assertEqual(result.output, "")
expected_kwargs = self.get_system_info_dict()
- expected_logs = self.get_expected('test_cli/test_lint_staged_msg_filename_2', expected_kwargs)
+ expected_logs = self.get_expected('cli/test_cli/test_lint_staged_msg_filename_2', expected_kwargs)
self.assert_logged(expected_logs)
@patch('gitlint.cli.get_stdin_data', return_value=False)
@@ -306,7 +296,7 @@ class CLITests(BaseTestCase):
def test_msg_filename(self, _):
expected_output = u"3: B6 Body message is missing\n"
- with tempdir() as tmpdir:
+ 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")
@@ -375,7 +365,7 @@ class CLITests(BaseTestCase):
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öo\nbar",
+ 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
]
@@ -394,7 +384,7 @@ class CLITests(BaseTestCase):
expected_kwargs = self.get_system_info_dict()
expected_kwargs.update({'config_path': config_path})
- expected_logs = self.get_expected('test_cli/test_debug_1', expected_kwargs)
+ 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")
@@ -403,7 +393,7 @@ class CLITests(BaseTestCase):
# 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, "--debug"])
+ result = self.cli.invoke(cli.cli, ["--extra-path", extra_path])
expected_output = u"1: UC1 Commit violåtion 1: \"Contënt 1\"\n" + \
"3: B6 Body message is missing\n"
self.assertEqual(stderr.getvalue(), expected_output)
@@ -412,7 +402,7 @@ class CLITests(BaseTestCase):
# Test extra-path pointing to a file
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, "--debug"])
+ result = self.cli.invoke(cli.cli, ["--extra-path", extra_path])
expected_output = u"1: UC1 Commit violåtion 1: \"Contënt 1\"\n" + \
"3: B6 Body message is missing\n"
self.assertEqual(stderr.getvalue(), expected_output)
@@ -423,7 +413,7 @@ class CLITests(BaseTestCase):
# Test enabled contrib rules
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--contrib", "contrib-title-conventional-commits,CC1"])
- expected_output = self.get_expected('test_cli/test_contrib_1')
+ expected_output = self.get_expected('cli/test_cli/test_contrib_1')
self.assertEqual(stderr.getvalue(), expected_output)
self.assertEqual(result.exit_code, 3)
@@ -469,13 +459,14 @@ class CLITests(BaseTestCase):
@patch('gitlint.cli.get_stdin_data', return_value=False)
def test_target(self, _):
""" Test for the --target option """
- os.environ["LANGUAGE"] = "C" # Force language to english so we can check for error message
- result = self.cli.invoke(cli.cli, ["--target", "/tmp"])
- # We expect gitlint to tell us that /tmp is not a git repo (this proves that it takes the target parameter
- # into account).
- expected_path = os.path.realpath("/tmp")
- self.assertEqual(result.output, "%s is not a git repository.\n" % expected_path)
- self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
+ with self.tempdir() as tmpdir:
+ tmpdir_path = os.path.realpath(tmpdir)
+ os.environ["LANGUAGE"] = "C" # Force language to english so we can check for error message
+ result = self.cli.invoke(cli.cli, ["--target", tmpdir_path])
+ # We expect gitlint to tell us that /tmp is not a git repo (this proves that it takes the target parameter
+ # into account).
+ self.assertEqual(result.output, "%s is not a git repository.\n" % tmpdir_path)
+ self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
def test_target_negative(self):
""" Negative test for the --target option """
@@ -539,3 +530,18 @@ class CLITests(BaseTestCase):
self.assert_log_contains(u"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")
+ 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"))
+ result = self.cli.invoke(cli.cli, ["--config", config_path, "--debug"])
+ self.assertEqual(result.output, "")
+ self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_named_rules_1"))
+ self.assertEqual(result.exit_code, 4)
+
+ # Assert debug logs are correct
+ expected_kwargs = self.get_system_info_dict()
+ expected_kwargs.update({'config_path': config_path})
+ expected_logs = self.get_expected('cli/test_cli/test_named_rules_2', expected_kwargs)
+ self.assert_logged(expected_logs)
diff --git a/gitlint/tests/cli/test_cli_hooks.py b/gitlint/tests/cli/test_cli_hooks.py
index 0564808..b5e7fc4 100644
--- a/gitlint/tests/cli/test_cli_hooks.py
+++ b/gitlint/tests/cli/test_cli_hooks.py
@@ -1,11 +1,19 @@
# -*- coding: utf-8 -*-
+import io
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
@@ -16,6 +24,8 @@ from gitlint import cli
from gitlint import hooks
from gitlint import config
+from gitlint.utils import DEFAULT_ENCODING
+
class CLIHookTests(BaseTestCase):
USAGE_ERROR_CODE = 253
@@ -94,3 +104,165 @@ class CLIHookTests(BaseTestCase):
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):
+ """ 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.
+ """
+
+ # No need to patch git as we're passing a msg-filename to run-hook, so no git calls are made.
+ # Note that this is the case when passing --staged as well, but that's tested as part of the integration tests
+ # (=end-to-end scenario).
+
+ # Ideally we'd be able to assert that run-hook internally calls the lint cli command, but couldn't make
+ # that work. Have tried many different variatons of mocking and patching without avail. For now, we just
+ # check the output which indirectly proves the same thing.
+
+ with self.tempdir() as tmpdir:
+ msg_filename = os.path.join(tmpdir, u"hür")
+ with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
+ f.write(u"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"])
+ self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_no_tty_1_stdout'))
+ self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli_hooks/test_hook_no_tty_1_stderr"))
+
+ # exit code is 1 because aborted (no stdin available)
+ self.assertEqual(result.exit_code, 1)
+
+ @patch('gitlint.cli.shell')
+ def test_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"]
+
+ for i in range(0, len(set_editors)):
+ if set_editors[i]:
+ os.environ['EDITOR'] = set_editors[i]
+
+ with self.patch_input(['e', 'e', 'n']):
+ with self.tempdir() as tmpdir:
+ msg_filename = os.path.realpath(os.path.join(tmpdir, u"hür"))
+ with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
+ f.write(commit_messages[i] + "\n")
+
+ with patch('gitlint.display.stderr', new=StringIO()) as stderr:
+ result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"])
+ self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_edit_1_stdout',
+ {"commit_msg": commit_messages[i]}))
+ expected = self.get_expected("cli/test_cli_hooks/test_hook_edit_1_stderr",
+ {"commit_msg": commit_messages[i]})
+ self.assertEqual(stderr.getvalue(), expected)
+
+ # exit code = number of violations
+ 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))
+
+ def test_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")
+ with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
+ f.write(u"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"])
+ self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_no_1_stdout'))
+ self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli_hooks/test_hook_no_1_stderr"))
+
+ # We decided not to keep the commit message: hook returns number of violations (>0)
+ # This will cause git to abort the commit
+ self.assertEqual(result.exit_code, 2)
+ self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message declined")
+
+ def test_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")
+ with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
+ f.write(u"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"])
+ self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_yes_1_stdout'))
+ self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli_hooks/test_hook_yes_1_stderr"))
+
+ # Exit code is 0 because we decide to keep the commit message
+ # This will cause git to keep the commit
+ 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, _):
+ """ Test for passing stdin data to run-hook, expecting some violations. Equivalent of:
+ $ echo "WIP: Test hook stdin tïtle" | gitlint run-hook
+ """
+
+ with patch('gitlint.display.stderr', new=StringIO()) as stderr:
+ result = self.cli.invoke(cli.cli, ["run-hook"])
+ expected_stderr = self.get_expected('cli/test_cli_hooks/test_hook_stdin_violations_1_stderr')
+ self.assertEqual(stderr.getvalue(), expected_stderr)
+ self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_stdin_violations_1_stdout'))
+ # 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, _):
+ """ 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
+ """
+
+ with patch('gitlint.display.stderr', new=StringIO()) as stderr:
+ result = self.cli.invoke(cli.cli, ["run-hook"])
+ self.assertEqual(stderr.getvalue(), "") # no errors = no stderr output
+ expected_stdout = self.get_expected('cli/test_cli_hooks/test_hook_stdin_no_violations_1_stdout')
+ 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, _):
+ """ 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
+ """
+
+ with patch('gitlint.display.stderr', new=StringIO()) as stderr:
+ result = self.cli.invoke(cli.cli, ["-c", "title-max-length.line-length=5", "--ignore", "B6", "run-hook"])
+ self.assertEqual(stderr.getvalue(), self.get_expected('cli/test_cli_hooks/test_hook_config_1_stderr'))
+ self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_config_1_stdout'))
+ # 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=False)
+ @patch('gitlint.git.sh')
+ def test_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"
+ ]
+
+ with self.patch_input(['e']):
+ with patch('gitlint.display.stderr', new=StringIO()) as stderr:
+ result = self.cli.invoke(cli.cli, ["run-hook"])
+ expected = self.get_expected('cli/test_cli_hooks/test_hook_local_commit_1_stderr')
+ self.assertEqual(stderr.getvalue(), expected)
+ self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_local_commit_1_stdout'))
+ # If we can't edit the message, run-hook follows regular gitlint behavior and exit code = # violations
+ self.assertEqual(result.exit_code, 2)
diff --git a/gitlint/tests/config/test_config.py b/gitlint/tests/config/test_config.py
index d3fdc2c..b981a86 100644
--- a/gitlint/tests/config/test_config.py
+++ b/gitlint/tests/config/test_config.py
@@ -30,18 +30,18 @@ class LintConfigTests(BaseTestCase):
# non-existing rule
expected_error_msg = u"No such rule 'föobar'"
- with self.assertRaisesRegex(LintConfigError, expected_error_msg):
+ 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'"
- with self.assertRaisesRegex(LintConfigError, expected_error_msg):
+ 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')."
- with self.assertRaisesRegex(LintConfigError, expected_error_msg):
+ with self.assertRaisesMessage(LintConfigError, expected_error_msg):
config.set_rule_option('title-max-length', 'line-length', u"föo")
def test_set_general_option(self):
@@ -124,7 +124,7 @@ class LintConfigTests(BaseTestCase):
expected_rule_option = options.ListOption(
"types",
- ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert"],
+ ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert", "ci", "build"],
"Comma separated list of allowed commit types.",
)
@@ -151,14 +151,14 @@ class LintConfigTests(BaseTestCase):
def test_contrib_negative(self):
config = LintConfig()
# non-existent contrib rule
- with self.assertRaisesRegex(LintConfigError, u"No contrib rule with id or name 'föo' found."):
+ with self.assertRaisesMessage(LintConfigError, u"No contrib rule with id or name 'föo' found."):
config.contrib = u"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")]
for side_effect in side_effects:
with patch('gitlint.config.rule_finder.find_rule_classes', side_effect=side_effect):
- with self.assertRaisesRegex(LintConfigError, ustr(side_effect)):
+ with self.assertRaisesMessage(LintConfigError, ustr(side_effect)):
config.contrib = u"contrib-title-conventional-commits"
def test_extra_path(self):
@@ -185,36 +185,36 @@ class LintConfigTests(BaseTestCase):
config = LintConfig()
regex = u"Option extra-path must be either an existing directory or file (current value: 'föo/bar')"
# incorrect extra_path
- with self.assertRaisesRegex(LintConfigError, regex):
+ with self.assertRaisesMessage(LintConfigError, regex):
config.extra_path = u"föo/bar"
# extra path contains classes with errors
- with self.assertRaisesRegex(LintConfigError,
- "User-defined rule class 'MyUserLineRule' must have a 'validate' method"):
+ with self.assertRaisesMessage(LintConfigError,
+ "User-defined rule class 'MyUserLineRule' must have a 'validate' method"):
config.extra_path = self.get_sample_path("user_rules/incorrect_linerule")
def test_set_general_option_negative(self):
config = LintConfig()
# Note that we shouldn't test whether we can set unicode because python just doesn't allow unicode attributes
- with self.assertRaisesRegex(LintConfigError, "'foo' is not a valid gitlint option"):
+ with self.assertRaisesMessage(LintConfigError, "'foo' is not a valid gitlint option"):
config.set_general_option("foo", u"bår")
# try setting _config_path, this is a real attribute of LintConfig, but the code should prevent it from
# being set
- with self.assertRaisesRegex(LintConfigError, "'_config_path' is not a valid gitlint option"):
+ with self.assertRaisesMessage(LintConfigError, "'_config_path' is not a valid gitlint option"):
config.set_general_option("_config_path", u"bår")
# invalid verbosity
incorrect_values = [-1, u"föo"]
for value in incorrect_values:
expected_msg = u"Option 'verbosity' must be a positive integer (current value: '{0}')".format(value)
- with self.assertRaisesRegex(LintConfigError, expected_msg):
+ with self.assertRaisesMessage(LintConfigError, expected_msg):
config.verbosity = value
incorrect_values = [4]
for value in incorrect_values:
- with self.assertRaisesRegex(LintConfigError, "Option 'verbosity' must be set between 0 and 3"):
+ with self.assertRaisesMessage(LintConfigError, "Option 'verbosity' must be set between 0 and 3"):
config.verbosity = value
# invalid ignore_xxx_commits
@@ -224,8 +224,8 @@ class LintConfigTests(BaseTestCase):
for attribute in ignore_attributes:
for value in incorrect_values:
option_name = attribute.replace("_", "-")
- with self.assertRaisesRegex(LintConfigError,
- "Option '{0}' must be either 'true' or 'false'".format(option_name)):
+ with self.assertRaisesMessage(LintConfigError,
+ "Option '{0}' must be either 'true' or 'false'".format(option_name)):
setattr(config, attribute, value)
# invalid ignore -> not here because ignore is a ListOption which converts everything to a string before
@@ -234,15 +234,15 @@ class LintConfigTests(BaseTestCase):
# invalid boolean options
for attribute in ['debug', 'staged', 'ignore_stdin']:
option_name = attribute.replace("_", "-")
- with self.assertRaisesRegex(LintConfigError,
- "Option '{0}' must be either 'true' or 'false'".format(option_name)):
+ with self.assertRaisesMessage(LintConfigError,
+ "Option '{0}' must be either 'true' or 'false'".format(option_name)):
setattr(config, attribute, u"föobar")
# extra-path has its own negative test
# invalid target
- with self.assertRaisesRegex(LintConfigError,
- u"Option target must be an existing directory (current value: 'föo/bar')"):
+ with self.assertRaisesMessage(LintConfigError,
+ u"Option target must be an existing directory (current value: 'föo/bar')"):
config.target = u"föo/bar"
def test_ignore_independent_from_rules(self):
@@ -254,6 +254,30 @@ class LintConfigTests(BaseTestCase):
self.assertEqual(config.ignore, ["T1", "T2"])
self.assertSequenceEqual(config.rules, original_rules)
+ def test_config_equality(self):
+ self.assertEqual(LintConfig(), LintConfig())
+ self.assertNotEqual(LintConfig(), LintConfigGenerator())
+
+ # Ensure LintConfig are not equal if they differ on their attributes
+ attrs = [("verbosity", 1), ("rules", []), ("ignore_stdin", True), ("debug", True),
+ ("ignore", ["T1"]), ("staged", True), ("_config_path", self.get_sample_path()),
+ ("ignore_merge_commits", False), ("ignore_fixup_commits", False),
+ ("ignore_squash_commits", False), ("ignore_revert_commits", False),
+ ("extra_path", self.get_sample_path("user_rules")), ("target", self.get_sample_path()),
+ ("contrib", ["CC1"])]
+ for attr, val in attrs:
+ config = LintConfig()
+ setattr(config, attr, val)
+ self.assertNotEqual(LintConfig(), config)
+
+ # Other attributes don't matter
+ config1 = LintConfig()
+ config2 = LintConfig()
+ config1.foo = u"bår"
+ self.assertEqual(config1, config2)
+ config2.foo = u"dūr"
+ self.assertEqual(config1, config2)
+
class LintConfigGeneratorTests(BaseTestCase):
@staticmethod
diff --git a/gitlint/tests/config/test_config_builder.py b/gitlint/tests/config/test_config_builder.py
index 051a52f..5a28c9f 100644
--- a/gitlint/tests/config/test_config_builder.py
+++ b/gitlint/tests/config/test_config_builder.py
@@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
+import copy
from gitlint.tests.base import BaseTestCase
from gitlint.config import LintConfig, LintConfigBuilder, LintConfigError
+from gitlint import rules
+
class LintConfigBuilderTests(BaseTestCase):
def test_set_option(self):
@@ -88,12 +91,13 @@ class LintConfigBuilderTests(BaseTestCase):
# bad config file load
foo_path = self.get_sample_path(u"föo")
expected_error_msg = u"Invalid file path: {0}".format(foo_path)
- with self.assertRaisesRegex(LintConfigError, expected_error_msg):
+ 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."
+ # 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)
@@ -102,7 +106,7 @@ class LintConfigBuilderTests(BaseTestCase):
config_builder = LintConfigBuilder()
config_builder.set_from_config_file(path)
expected_error_msg = u"No such rule 'föobar'"
- with self.assertRaisesRegex(LintConfigError, expected_error_msg):
+ with self.assertRaisesMessage(LintConfigError, expected_error_msg):
config_builder.build()
# non-existing general option
@@ -110,7 +114,7 @@ class LintConfigBuilderTests(BaseTestCase):
config_builder = LintConfigBuilder()
config_builder.set_from_config_file(path)
expected_error_msg = u"'foo' is not a valid gitlint option"
- with self.assertRaisesRegex(LintConfigError, expected_error_msg):
+ with self.assertRaisesMessage(LintConfigError, expected_error_msg):
config_builder.build()
# non-existing option
@@ -118,7 +122,7 @@ class LintConfigBuilderTests(BaseTestCase):
config_builder = LintConfigBuilder()
config_builder.set_from_config_file(path)
expected_error_msg = u"Rule 'title-max-length' has no option 'föobar'"
- with self.assertRaisesRegex(LintConfigError, expected_error_msg):
+ with self.assertRaisesMessage(LintConfigError, expected_error_msg):
config_builder.build()
# invalid option value
@@ -127,7 +131,7 @@ class LintConfigBuilderTests(BaseTestCase):
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')."
- with self.assertRaisesRegex(LintConfigError, expected_error_msg):
+ with self.assertRaisesMessage(LintConfigError, expected_error_msg):
config_builder.build()
def test_set_config_from_string_list(self):
@@ -150,27 +154,27 @@ class LintConfigBuilderTests(BaseTestCase):
# assert error on incorrect rule - this happens at build time
config_builder.set_config_from_string_list([u"föo.bar=1"])
- with self.assertRaisesRegex(LintConfigError, u"No such rule 'föo'"):
+ with self.assertRaisesMessage(LintConfigError, u"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>'"
- with self.assertRaisesRegex(LintConfigError, expected_msg):
+ with self.assertRaisesMessage(LintConfigError, expected_msg):
config_builder.set_config_from_string_list([u"föo.bar"])
# missing value
expected_msg = u"'föo.bar=' is an invalid configuration option. Use '<rule>.<option>=<value>'"
- with self.assertRaisesRegex(LintConfigError, expected_msg):
+ with self.assertRaisesMessage(LintConfigError, expected_msg):
config_builder.set_config_from_string_list([u"föo.bar="])
# space instead of equal sign
expected_msg = u"'föo.bar 1' is an invalid configuration option. Use '<rule>.<option>=<value>'"
- with self.assertRaisesRegex(LintConfigError, expected_msg):
+ with self.assertRaisesMessage(LintConfigError, expected_msg):
config_builder.set_config_from_string_list([u"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>'"
- with self.assertRaisesRegex(LintConfigError, expected_msg):
+ with self.assertRaisesMessage(LintConfigError, expected_msg):
config_builder.set_config_from_string_list([u'föobar=1'])
def test_rebuild_config(self):
@@ -201,3 +205,60 @@ class LintConfigBuilderTests(BaseTestCase):
# Modify the original and make sure we're not modifying the clone (i.e. check that the copy is a deep copy)
config_builder.set_option('title-max-length', 'line-length', 120)
self.assertDictEqual(cloned_builder._config_blueprint, expected)
+
+ def test_named_rules(self):
+ # Store a copy of the default rules from the config, so we can reference it later
+ config_builder = LintConfigBuilder()
+ config = config_builder.build()
+ default_rules = copy.deepcopy(config.rules)
+ self.assertEqual(default_rules, config.rules) # deepcopy should be equal
+
+ # 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"]
+ for rule_qualifier in rule_qualifiers:
+ config_builder = LintConfigBuilder()
+ config_builder.set_option(rule_qualifier, 'regex', u"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"
+ expected_rules._rules[u'T7:my-extra-rüle'] = my_rule
+ self.assertEqual(config_builder.build().rules, expected_rules)
+
+ # assert that changing an option on the newly added rule is passed correctly to the RuleCollection
+ # we try this with all different rule qualifiers to ensure they all are normalized and map
+ # 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")
+ # 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")
+ self.assertEqual(cb.build().rules, expected_rules)
+ my_rule.options['regex'].set(u"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"]:
+ 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.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.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.build()
diff --git a/gitlint/tests/config/test_config_precedence.py b/gitlint/tests/config/test_config_precedence.py
index 9689e55..a0eeccd 100644
--- a/gitlint/tests/config/test_config_precedence.py
+++ b/gitlint/tests/config/test_config_precedence.py
@@ -25,40 +25,48 @@ class LintConfigPrecedenceTests(BaseTestCase):
def setUp(self):
self.cli = CliRunner()
- @patch('gitlint.cli.get_stdin_data', return_value=u"WIP\n\nThis is å test message\n")
+ @patch('gitlint.cli.get_stdin_data', return_value=u"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
# Test that the config precedence is followed:
# 1. commandline convenience flags
- # 2. commandline -c flags
- # 3. config file
- # 4. default config
+ # 2. environment variables
+ # 3. commandline -c flags
+ # 4. config file
+ # 5. default config
config_path = self.get_sample_path("config/gitlintconfig")
# 1. commandline convenience flags
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(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP\"\n")
+ self.assertEqual(stderr.getvalue(), u"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n")
- # 2. commandline -c flags
+ # 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")
+
+ # 3. commandline -c flags
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["-c", "general.verbosity=2", "--config", config_path])
self.assertEqual(result.output, "")
self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive)\n")
- # 3. config file
+ # 4. config file
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli, ["--config", config_path])
self.assertEqual(result.output, "")
self.assertEqual(stderr.getvalue(), "1: T5\n")
- # 4. default config
+ # 5. default config
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
result = self.cli.invoke(cli.cli)
self.assertEqual(result.output, "")
- self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP\"\n")
+ self.assertEqual(stderr.getvalue(), u"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")
def test_ignore_precedence(self, get_stdin_data):
diff --git a/qa/samples/config/contrib-enabled b/gitlint/tests/contrib/rules/__init__.py
index e69de29..e69de29 100644
--- a/qa/samples/config/contrib-enabled
+++ b/gitlint/tests/contrib/rules/__init__.py
diff --git a/gitlint/tests/contrib/test_conventional_commit.py b/gitlint/tests/contrib/rules/test_conventional_commit.py
index ea808fd..001af32 100644
--- a/gitlint/tests/contrib/test_conventional_commit.py
+++ b/gitlint/tests/contrib/rules/test_conventional_commit.py
@@ -19,13 +19,13 @@ class ContribConventionalCommitTests(BaseTestCase):
rule = ConventionalCommit()
# No violations when using a correct type and format
- for type in ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert"]:
+ for type in ["fix", "feat", "chore", "docs", "style", "refactor", "perf", "test", "revert", "ci", "build"]:
violations = rule.validate(type + u": 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", u"bår: foo")
+ " style, refactor, perf, test, revert, ci, build", u"bår: foo")
violations = rule.validate(u"bår: foo", None)
self.assertListEqual([expected_violation], violations)
diff --git a/gitlint/tests/contrib/test_signedoff_by.py b/gitlint/tests/contrib/rules/test_signedoff_by.py
index 934aec5..934aec5 100644
--- a/gitlint/tests/contrib/test_signedoff_by.py
+++ b/gitlint/tests/contrib/rules/test_signedoff_by.py
diff --git a/gitlint/tests/contrib/test_contrib_rules.py b/gitlint/tests/contrib/test_contrib_rules.py
index 3fa4048..84db2d5 100644
--- a/gitlint/tests/contrib/test_contrib_rules.py
+++ b/gitlint/tests/contrib/test_contrib_rules.py
@@ -3,7 +3,7 @@ import os
from gitlint.tests.base import BaseTestCase
from gitlint.contrib import rules as contrib_rules
-from gitlint.tests import contrib as contrib_tests
+from gitlint.tests.contrib import rules as contrib_tests
from gitlint import rule_finder, rules
from gitlint.utils import ustr
diff --git a/gitlint/tests/expected/test_cli/test_contrib_1 b/gitlint/tests/expected/cli/test_cli/test_contrib_1
index ea5d353..cdfb821 100644
--- a/gitlint/tests/expected/test_cli/test_contrib_1
+++ b/gitlint/tests/expected/cli/test_cli/test_contrib_1
@@ -1,3 +1,3 @@
1: CC1 Body does not contain a 'Signed-Off-By' line
-1: CT1 Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert: "Test tïtle"
+1: CT1 Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert, ci, build: "Test tïtle"
1: CT1 Title does not follow ConventionalCommits.org format 'type(optional-scope): description': "Test tïtle"
diff --git a/gitlint/tests/expected/test_cli/test_debug_1 b/gitlint/tests/expected/cli/test_cli/test_debug_1
index 612f78e..a95a58d 100644
--- a/gitlint/tests/expected/test_cli/test_debug_1
+++ b/gitlint/tests/expected/cli/test_cli/test_debug_1
@@ -4,6 +4,7 @@ DEBUG: gitlint.cli Python version: {python_version}
DEBUG: gitlint.cli Git version: git version 1.2.3
DEBUG: gitlint.cli Gitlint version: {gitlint_version}
DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
+DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli Configuration
config-path: {config_path}
[GENERAL]
@@ -26,6 +27,8 @@ target: {target}
I2: ignore-by-body
ignore=all
regex=None
+ I3: ignore-body-lines
+ regex=None
T1: title-max-length
line-length=20
T2: title-trailing-whitespace
@@ -35,7 +38,9 @@ target: {target}
T5: title-must-not-contain-word
words=WIP,bögus
T7: title-match-regex
- regex=.*
+ regex=None
+ T8: title-min-length
+ min-length=5
B1: body-max-line-length
line-length=30
B5: body-min-length
@@ -47,12 +52,19 @@ target: {target}
B4: body-first-line-empty
B7: body-changed-file-mention
files=
+ B8: body-match-regex
+ regex=None
M1: author-valid-email
regex=[^@ ]+@[^@ ]+\.[^@ ]+
DEBUG: gitlint.cli No --msg-filename flag, no or empty data passed to stdin. Using the local repo.
+DEBUG: gitlint.git ('rev-list', 'foo...bar')
DEBUG: gitlint.cli Linting 3 commit(s)
+DEBUG: gitlint.git ('log', '6f29bf81a8322a04071bb794666e48c443a90360', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B')
+DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
DEBUG: gitlint.lint Linting commit 6f29bf81a8322a04071bb794666e48c443a90360
+DEBUG: gitlint.git ('branch', '--contains', '6f29bf81a8322a04071bb794666e48c443a90360')
+DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', '6f29bf81a8322a04071bb794666e48c443a90360')
DEBUG: gitlint.lint Commit Object
--- Commit Message ----
commït-title1
@@ -68,7 +80,10 @@ is-revert-commit: False
Branches: ['commit-1-branch-1', 'commit-1-branch-2']
Changed Files: ['commit-1/file-1', 'commit-1/file-2']
-----------------------
+DEBUG: gitlint.git ('log', '25053ccec5e28e1bb8f7551fdbb5ab213ada2401', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B')
DEBUG: gitlint.lint Linting commit 25053ccec5e28e1bb8f7551fdbb5ab213ada2401
+DEBUG: gitlint.git ('branch', '--contains', '25053ccec5e28e1bb8f7551fdbb5ab213ada2401')
+DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', '25053ccec5e28e1bb8f7551fdbb5ab213ada2401')
DEBUG: gitlint.lint Commit Object
--- Commit Message ----
commït-title2.
@@ -84,10 +99,13 @@ is-revert-commit: False
Branches: ['commit-2-branch-1', 'commit-2-branch-2']
Changed Files: ['commit-2/file-1', 'commit-2/file-2']
-----------------------
+DEBUG: gitlint.git ('log', '4da2656b0dadc76c7ee3fd0243a96cb64007f125', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B')
DEBUG: gitlint.lint Linting commit 4da2656b0dadc76c7ee3fd0243a96cb64007f125
+DEBUG: gitlint.git ('branch', '--contains', '4da2656b0dadc76c7ee3fd0243a96cb64007f125')
+DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', '4da2656b0dadc76c7ee3fd0243a96cb64007f125')
DEBUG: gitlint.lint Commit Object
--- Commit Message ----
-föo
+föobar
bar
--- Meta info ---------
Author: test åuthor3 <test-email3@föo.com>
diff --git a/gitlint/tests/expected/test_cli/test_input_stream_1 b/gitlint/tests/expected/cli/test_cli/test_input_stream_1
index 4326729..4326729 100644
--- a/gitlint/tests/expected/test_cli/test_input_stream_1
+++ b/gitlint/tests/expected/cli/test_cli/test_input_stream_1
diff --git a/gitlint/tests/expected/test_cli/test_input_stream_debug_1 b/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_1
index 4326729..4326729 100644
--- a/gitlint/tests/expected/test_cli/test_input_stream_debug_1
+++ b/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_1
diff --git a/gitlint/tests/expected/test_cli/test_input_stream_debug_2 b/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_2
index a9028e1..c05d147 100644
--- a/gitlint/tests/expected/test_cli/test_input_stream_debug_2
+++ b/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_2
@@ -4,6 +4,7 @@ DEBUG: gitlint.cli Python version: {python_version}
DEBUG: gitlint.cli Git version: git version 1.2.3
DEBUG: gitlint.cli Gitlint version: {gitlint_version}
DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
+DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli Configuration
config-path: None
[GENERAL]
@@ -26,6 +27,8 @@ target: {target}
I2: ignore-by-body
ignore=all
regex=None
+ I3: ignore-body-lines
+ regex=None
T1: title-max-length
line-length=72
T2: title-trailing-whitespace
@@ -35,7 +38,9 @@ target: {target}
T5: title-must-not-contain-word
words=WIP
T7: title-match-regex
- regex=.*
+ regex=None
+ T8: title-min-length
+ min-length=5
B1: body-max-line-length
line-length=80
B5: body-min-length
@@ -47,12 +52,15 @@ target: {target}
B4: body-first-line-empty
B7: body-changed-file-mention
files=
+ B8: body-match-regex
+ regex=None
M1: author-valid-email
regex=[^@ ]+@[^@ ]+\.[^@ ]+
DEBUG: gitlint.cli Stdin data: 'WIP: tïtle
'
DEBUG: gitlint.cli Stdin detected and not ignored. Using as input.
+DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
DEBUG: gitlint.cli Linting 1 commit(s)
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
DEBUG: gitlint.lint Commit Object
diff --git a/gitlint/tests/expected/test_cli/test_lint_multiple_commits_1 b/gitlint/tests/expected/cli/test_cli/test_lint_multiple_commits_1
index be3288b..be3288b 100644
--- a/gitlint/tests/expected/test_cli/test_lint_multiple_commits_1
+++ b/gitlint/tests/expected/cli/test_cli/test_lint_multiple_commits_1
diff --git a/gitlint/tests/expected/test_cli/test_lint_multiple_commits_config_1 b/gitlint/tests/expected/cli/test_cli/test_lint_multiple_commits_config_1
index 1bf0503..1bf0503 100644
--- a/gitlint/tests/expected/test_cli/test_lint_multiple_commits_config_1
+++ b/gitlint/tests/expected/cli/test_cli/test_lint_multiple_commits_config_1
diff --git a/gitlint/tests/expected/test_cli/test_lint_staged_msg_filename_1 b/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_1
index 9a9091b..9a9091b 100644
--- a/gitlint/tests/expected/test_cli/test_lint_staged_msg_filename_1
+++ b/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_1
diff --git a/gitlint/tests/expected/test_cli/test_lint_staged_msg_filename_2 b/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_2
index 3e5dcb6..e8e9f33 100644
--- a/gitlint/tests/expected/test_cli/test_lint_staged_msg_filename_2
+++ b/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_2
@@ -4,6 +4,7 @@ DEBUG: gitlint.cli Python version: {python_version}
DEBUG: gitlint.cli Git version: git version 1.2.3
DEBUG: gitlint.cli Gitlint version: {gitlint_version}
DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
+DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli Configuration
config-path: None
[GENERAL]
@@ -26,6 +27,8 @@ target: {target}
I2: ignore-by-body
ignore=all
regex=None
+ I3: ignore-body-lines
+ regex=None
T1: title-max-length
line-length=72
T2: title-trailing-whitespace
@@ -35,7 +38,9 @@ target: {target}
T5: title-must-not-contain-word
words=WIP
T7: title-match-regex
- regex=.*
+ regex=None
+ T8: title-min-length
+ min-length=5
B1: body-max-line-length
line-length=80
B5: body-min-length
@@ -47,13 +52,20 @@ target: {target}
B4: body-first-line-empty
B7: body-changed-file-mention
files=
+ B8: body-match-regex
+ regex=None
M1: author-valid-email
regex=[^@ ]+@[^@ ]+\.[^@ ]+
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
DEBUG: gitlint.cli Using --msg-filename.
+DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
DEBUG: gitlint.cli Linting 1 commit(s)
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
+DEBUG: gitlint.git ('config', '--get', 'user.name')
+DEBUG: gitlint.git ('config', '--get', 'user.email')
+DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
+DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r')
DEBUG: gitlint.lint Commit Object
--- Commit Message ----
WIP: msg-filename tïtle
diff --git a/gitlint/tests/expected/test_cli/test_lint_staged_stdin_1 b/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_1
index 4326729..4326729 100644
--- a/gitlint/tests/expected/test_cli/test_lint_staged_stdin_1
+++ b/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_1
diff --git a/gitlint/tests/expected/test_cli/test_lint_staged_stdin_2 b/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_2
index 03fd8c3..b822edc 100644
--- a/gitlint/tests/expected/test_cli/test_lint_staged_stdin_2
+++ b/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_2
@@ -4,6 +4,7 @@ DEBUG: gitlint.cli Python version: {python_version}
DEBUG: gitlint.cli Git version: git version 1.2.3
DEBUG: gitlint.cli Gitlint version: {gitlint_version}
DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
+DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli Configuration
config-path: None
[GENERAL]
@@ -26,6 +27,8 @@ target: {target}
I2: ignore-by-body
ignore=all
regex=None
+ I3: ignore-body-lines
+ regex=None
T1: title-max-length
line-length=72
T2: title-trailing-whitespace
@@ -35,7 +38,9 @@ target: {target}
T5: title-must-not-contain-word
words=WIP
T7: title-match-regex
- regex=.*
+ regex=None
+ T8: title-min-length
+ min-length=5
B1: body-max-line-length
line-length=80
B5: body-min-length
@@ -47,6 +52,8 @@ target: {target}
B4: body-first-line-empty
B7: body-changed-file-mention
files=
+ B8: body-match-regex
+ regex=None
M1: author-valid-email
regex=[^@ ]+@[^@ ]+\.[^@ ]+
@@ -54,8 +61,13 @@ DEBUG: gitlint.cli Fetching additional meta-data from staged commit
DEBUG: gitlint.cli Stdin data: 'WIP: tïtle
'
DEBUG: gitlint.cli Stdin detected and not ignored. Using as input.
+DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
DEBUG: gitlint.cli Linting 1 commit(s)
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
+DEBUG: gitlint.git ('config', '--get', 'user.name')
+DEBUG: gitlint.git ('config', '--get', 'user.email')
+DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
+DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r')
DEBUG: gitlint.lint Commit Object
--- Commit Message ----
WIP: tïtle
diff --git a/gitlint/tests/expected/cli/test_cli/test_named_rules_1 b/gitlint/tests/expected/cli/test_cli/test_named_rules_1
new file mode 100644
index 0000000..a581d05
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli/test_named_rules_1
@@ -0,0 +1,4 @@
+1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: tëst tïtle"
+1: T5:even-more-wörds Title contains the word 'tïtle' (case-insensitive): "WIP: tëst tïtle"
+1: T5:extra-wörds Title contains the word 'tëst' (case-insensitive): "WIP: tëst tïtle"
+3: B6 Body message is missing
diff --git a/gitlint/tests/expected/cli/test_cli/test_named_rules_2 b/gitlint/tests/expected/cli/test_cli/test_named_rules_2
new file mode 100644
index 0000000..828e296
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli/test_named_rules_2
@@ -0,0 +1,82 @@
+DEBUG: gitlint.cli To report issues, please visit https://github.com/jorisroovers/gitlint/issues
+DEBUG: gitlint.cli Platform: {platform}
+DEBUG: gitlint.cli Python version: {python_version}
+DEBUG: gitlint.cli Git version: git version 1.2.3
+DEBUG: gitlint.cli Gitlint version: {gitlint_version}
+DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
+DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING}
+DEBUG: gitlint.cli Configuration
+config-path: {config_path}
+[GENERAL]
+extra-path: None
+contrib: []
+ignore:
+ignore-merge-commits: True
+ignore-fixup-commits: True
+ignore-squash-commits: True
+ignore-revert-commits: True
+ignore-stdin: False
+staged: False
+verbosity: 3
+debug: True
+target: {target}
+[RULES]
+ I1: ignore-by-title
+ ignore=all
+ regex=None
+ I2: ignore-by-body
+ ignore=all
+ regex=None
+ I3: ignore-body-lines
+ regex=None
+ T1: title-max-length
+ line-length=72
+ T2: title-trailing-whitespace
+ T6: title-leading-whitespace
+ T3: title-trailing-punctuation
+ T4: title-hard-tab
+ T5: title-must-not-contain-word
+ words=WIP,bögus
+ T7: title-match-regex
+ regex=None
+ T8: title-min-length
+ min-length=5
+ B1: body-max-line-length
+ line-length=80
+ B5: body-min-length
+ min-length=20
+ B6: body-is-missing
+ ignore-merge-commits=True
+ B2: body-trailing-whitespace
+ B3: body-hard-tab
+ B4: body-first-line-empty
+ B7: body-changed-file-mention
+ files=
+ B8: body-match-regex
+ regex=None
+ M1: author-valid-email
+ regex=[^@ ]+@[^@ ]+\.[^@ ]+
+ T5:extra-wörds: title-must-not-contain-word:extra-wörds
+ words=hür,tëst
+ T5:even-more-wörds: title-must-not-contain-word:even-more-wörds
+ words=hür,tïtle
+
+DEBUG: gitlint.cli Stdin data: 'WIP: tëst tïtle'
+DEBUG: gitlint.cli Stdin detected and not ignored. Using as input.
+DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
+DEBUG: gitlint.cli Linting 1 commit(s)
+DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
+DEBUG: gitlint.lint Commit Object
+--- Commit Message ----
+WIP: tëst tïtle
+--- Meta info ---------
+Author: None <None>
+Date: None
+is-merge-commit: False
+is-fixup-commit: False
+is-squash-commit: False
+is-revert-commit: False
+Branches: []
+Changed Files: []
+-----------------------
+DEBUG: gitlint.cli Exit Code = 4 \ No newline at end of file
diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_config_1_stderr b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_config_1_stderr
new file mode 100644
index 0000000..cfacd42
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_config_1_stderr
@@ -0,0 +1,2 @@
+1: T1 Title exceeds max length (27>5): "WIP: Test hook config tïtle"
+1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Test hook config tïtle"
diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_config_1_stdout b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_config_1_stdout
new file mode 100644
index 0000000..5d3f1fc
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_config_1_stdout
@@ -0,0 +1,5 @@
+gitlint: checking commit message...
+-----------------------------------------------
+gitlint: Your commit message contains the above violations.
+Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)]
+Aborted!
diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_edit_1_stderr b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_edit_1_stderr
new file mode 100644
index 0000000..3eb8fca
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_edit_1_stderr
@@ -0,0 +1,6 @@
+1: T5 Title contains the word 'WIP' (case-insensitive): "{commit_msg}"
+3: B6 Body message is missing
+1: T5 Title contains the word 'WIP' (case-insensitive): "{commit_msg}"
+3: B6 Body message is missing
+1: T5 Title contains the word 'WIP' (case-insensitive): "{commit_msg}"
+3: B6 Body message is missing
diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_edit_1_stdout b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_edit_1_stdout
new file mode 100644
index 0000000..fa6b3bc
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_edit_1_stdout
@@ -0,0 +1,14 @@
+gitlint: checking commit message...
+-----------------------------------------------
+gitlint: Your commit message contains the above violations.
+Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] gitlint: checking commit message...
+-----------------------------------------------
+gitlint: Your commit message contains the above violations.
+Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] gitlint: checking commit message...
+-----------------------------------------------
+gitlint: Your commit message contains the above violations.
+Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] Commit aborted.
+Your commit message:
+-----------------------------------------------
+{commit_msg}
+-----------------------------------------------
diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_local_commit_1_stderr b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_local_commit_1_stderr
new file mode 100644
index 0000000..11c3cd8
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_local_commit_1_stderr
@@ -0,0 +1,2 @@
+1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: commït-title"
+3: B5 Body message is too short (11<20): "commït-body"
diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_local_commit_1_stdout b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_local_commit_1_stdout
new file mode 100644
index 0000000..a95bfea
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_local_commit_1_stdout
@@ -0,0 +1,4 @@
+gitlint: checking commit message...
+-----------------------------------------------
+gitlint: Your commit message contains the above violations.
+Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] Editing only possible when --msg-filename is specified.
diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_1_stderr b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_1_stderr
new file mode 100644
index 0000000..6d0c9cf
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_1_stderr
@@ -0,0 +1,2 @@
+1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: höok no"
+3: B6 Body message is missing
diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_1_stdout b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_1_stdout
new file mode 100644
index 0000000..9cc53c1
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_1_stdout
@@ -0,0 +1,8 @@
+gitlint: checking commit message...
+-----------------------------------------------
+gitlint: Your commit message contains the above violations.
+Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] Commit aborted.
+Your commit message:
+-----------------------------------------------
+WIP: höok no
+-----------------------------------------------
diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_tty_1_stderr b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_tty_1_stderr
new file mode 100644
index 0000000..a8d8760
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_tty_1_stderr
@@ -0,0 +1,2 @@
+1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: tïtle"
+3: B6 Body message is missing
diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_tty_1_stdout b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_tty_1_stdout
new file mode 100644
index 0000000..5d3f1fc
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_no_tty_1_stdout
@@ -0,0 +1,5 @@
+gitlint: checking commit message...
+-----------------------------------------------
+gitlint: Your commit message contains the above violations.
+Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)]
+Aborted!
diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_no_violations_1_stdout b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_no_violations_1_stdout
new file mode 100644
index 0000000..da1ef0b
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_no_violations_1_stdout
@@ -0,0 +1,2 @@
+gitlint: checking commit message...
+gitlint: OK (no violations in commit message)
diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_violations_1_stderr b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_violations_1_stderr
new file mode 100644
index 0000000..1404f4a
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_violations_1_stderr
@@ -0,0 +1,2 @@
+1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Test hook stdin tïtle"
+3: B6 Body message is missing
diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_violations_1_stdout b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_violations_1_stdout
new file mode 100644
index 0000000..5d3f1fc
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_stdin_violations_1_stdout
@@ -0,0 +1,5 @@
+gitlint: checking commit message...
+-----------------------------------------------
+gitlint: Your commit message contains the above violations.
+Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)]
+Aborted!
diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_yes_1_stderr b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_yes_1_stderr
new file mode 100644
index 0000000..da6f874
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_yes_1_stderr
@@ -0,0 +1,2 @@
+1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: höok yes"
+3: B6 Body message is missing
diff --git a/gitlint/tests/expected/cli/test_cli_hooks/test_hook_yes_1_stdout b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_yes_1_stdout
new file mode 100644
index 0000000..bb753b0
--- /dev/null
+++ b/gitlint/tests/expected/cli/test_cli_hooks/test_hook_yes_1_stdout
@@ -0,0 +1,4 @@
+gitlint: checking commit message...
+-----------------------------------------------
+gitlint: Your commit message contains the above violations.
+Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] \ No newline at end of file
diff --git a/gitlint/tests/git/test_git.py b/gitlint/tests/git/test_git.py
index 297b10c..1830119 100644
--- a/gitlint/tests/git/test_git.py
+++ b/gitlint/tests/git/test_git.py
@@ -27,7 +27,7 @@ class GitTests(BaseTestCase):
sh.git.side_effect = CommandNotFound("git")
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.assertRaisesRegex(GitNotInstalledError, expected_msg):
+ with self.assertRaisesMessage(GitNotInstalledError, expected_msg):
GitContext.from_local_repository(u"fåke/path")
# assert that commit message was read using git command
@@ -39,7 +39,7 @@ 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.assertRaisesRegex(GitContextError, u"fåke/path is not a git repository."):
+ with self.assertRaisesMessage(GitContextError, u"fåke/path is not a git repository."):
GitContext.from_local_repository(u"fåke/path")
# assert that commit message was read using git command
@@ -50,7 +50,7 @@ class GitTests(BaseTestCase):
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)
- with self.assertRaisesRegex(GitContextError, expected_msg):
+ with self.assertRaisesMessage(GitContextError, expected_msg):
GitContext.from_local_repository(u"fåke/path")
# assert that commit message was read using git command
@@ -64,7 +64,7 @@ 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."
- with self.assertRaisesRegex(GitContextError, expected_msg):
+ with self.assertRaisesMessage(GitContextError, expected_msg):
GitContext.from_local_repository(u"fåke/path")
# assert that commit message was read using git command
@@ -82,7 +82,7 @@ class GitTests(BaseTestCase):
ErrorReturnCode("rev-parse --abbrev-ref HEAD", b"", err)
]
- with self.assertRaisesRegex(GitContextError, expected_msg):
+ with self.assertRaisesMessage(GitContextError, expected_msg):
context = GitContext.from_commit_msg(u"test")
context.current_branch
diff --git a/gitlint/tests/git/test_git_commit.py b/gitlint/tests/git/test_git_commit.py
index dc83ccb..5f87a8e 100644
--- a/gitlint/tests/git/test_git_commit.py
+++ b/gitlint/tests/git/test_git_commit.py
@@ -14,7 +14,9 @@ except ImportError:
from unittest.mock import patch, call # pylint: disable=no-name-in-module, import-error
from gitlint.tests.base import BaseTestCase
-from gitlint.git import GitContext, GitCommit, LocalGitCommit, StagedLocalGitCommit, GitCommitMessage
+from gitlint.git import GitContext, GitCommit, GitContextError, LocalGitCommit, StagedLocalGitCommit, GitCommitMessage
+from gitlint.shell import ErrorReturnCode
+from gitlint.utils import ustr
class GitCommitTests(BaseTestCase):
@@ -479,12 +481,46 @@ class GitCommitTests(BaseTestCase):
self.assertListEqual(last_commit.changed_files, ["file1.txt", u"påth/to/file2.txt"])
self.assertListEqual(sh.git.mock_calls, expected_calls[0:5])
+ @patch('gitlint.git.sh')
+ def test_staged_commit_with_missing_username(self, sh):
+ # StagedLocalGitCommit()
+
+ sh.git.side_effect = [
+ u"#", # 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]
+
+ @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
+ 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]
+
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"])
attrs = ['original', 'full', 'title', 'body']
self.object_equality_test(commit_message1, attrs, {"context": commit_message1.context})
- def test_gitcommit_equality(self):
+ @patch("gitlint.git._git")
+ 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"
+
# Test simple equality case
now = datetime.datetime.utcnow()
context1 = GitContext()
diff --git a/gitlint/tests/rules/test_body_rules.py b/gitlint/tests/rules/test_body_rules.py
index fcb1b30..f46760b 100644
--- a/gitlint/tests/rules/test_body_rules.py
+++ b/gitlint/tests/rules/test_body_rules.py
@@ -178,3 +178,49 @@ class BodyRuleTests(BaseTestCase):
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)
+
+ 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")
+
+ # assert no violation on default regex (=everything allowed)
+ rule = rules.BodyRegexMatches()
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # 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(.*)"})
+ 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$"})
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # common use-case: matching that a given line is present
+ rule = rules.BodyRegexMatches({'regex': u"(.*)Föo(.*)"})
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
+ # assert violation on non-matching body
+ rule = rules.BodyRegexMatches({'regex': u"^Tëst(.*)Foo"})
+ violations = rule.validate(commit)
+ expected_violation = rules.RuleViolation("B8", u"Body does not match regex (^Tëst(.*)Foo)", None, 6)
+ self.assertListEqual(violations, [expected_violation])
+
+ # assert no violation on None regex
+ rule = rules.BodyRegexMatches({'regex': None})
+ violations = rule.validate(commit)
+ 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"]
+ for body in bodies:
+ commit = self.gitcommit(body)
+ rule = rules.BodyRegexMatches({'regex': ".*"})
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
diff --git a/gitlint/tests/rules/test_configuration_rules.py b/gitlint/tests/rules/test_configuration_rules.py
index 73d42f3..121cb3a 100644
--- a/gitlint/tests/rules/test_configuration_rules.py
+++ b/gitlint/tests/rules/test_configuration_rules.py
@@ -67,5 +67,41 @@ class ConfigurationRuleTests(BaseTestCase):
rule.apply(config, commit)
self.assertEqual(config, expected_config)
- expected_log_message = u"DEBUG: gitlint.rules Ignoring commit because of rule 'I1': " + \
+ 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"
+ 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")
+
+ # no regex specified, nothing should have happened:
+ # commit and config should remain identical, log should be empty
+ rule = rules.IgnoreBodyLines()
+ config = LintConfig()
+ rule.apply(config, commit1)
+ self.assertEqual(commit1, commit2)
+ self.assertEqual(config, LintConfig())
+ self.assert_logged([])
+
+ # Matching regex
+ rule = rules.IgnoreBodyLines({"regex": u"(.*)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")
+ # 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(.*)'")
+
+ # 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(.*)"})
+ config = LintConfig()
+ rule.apply(config, commit1)
+ self.assertEqual(commit1, commit2)
+ self.assertEqual(config, LintConfig()) # config shouldn't have been modified
diff --git a/gitlint/tests/rules/test_meta_rules.py b/gitlint/tests/rules/test_meta_rules.py
index c94b8b3..987aa88 100644
--- a/gitlint/tests/rules/test_meta_rules.py
+++ b/gitlint/tests/rules/test_meta_rules.py
@@ -32,6 +32,15 @@ class MetaRuleTests(BaseTestCase):
[RuleViolation("M1", "Author email for commit is invalid", email)])
def test_author_valid_email_rule_custom_regex(self):
+ # regex=None -> the rule isn't applied
+ rule = AuthorValidEmail()
+ rule.options['regex'].set(None)
+ emailadresses = [u"föo", None, u"hür dür"]
+ for email in emailadresses:
+ commit = self.gitcommit(u"", author_email=email)
+ violations = rule.validate(commit)
+ self.assertIsNone(violations)
+
# Custom domain
rule = AuthorValidEmail({'regex': u"[^@]+@bår.com"})
valid_email_addresses = [
diff --git a/gitlint/tests/rules/test_rules.py b/gitlint/tests/rules/test_rules.py
index 89caa27..58ee1c3 100644
--- a/gitlint/tests/rules/test_rules.py
+++ b/gitlint/tests/rules/test_rules.py
@@ -13,6 +13,11 @@ class RuleTests(BaseTestCase):
setattr(rule, attr, u"å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")
+
def test_rule_violation_equality(self):
violation1 = RuleViolation(u"ïd1", u"My messåge", u"My cöntent", 1)
self.object_equality_test(violation1, ["rule_id", "message", "content", "line_nr"])
diff --git a/gitlint/tests/rules/test_title_rules.py b/gitlint/tests/rules/test_title_rules.py
index 07d2323..049735e 100644
--- a/gitlint/tests/rules/test_title_rules.py
+++ b/gitlint/tests/rules/test_title_rules.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from gitlint.tests.base import BaseTestCase
from gitlint.rules import TitleMaxLength, TitleTrailingWhitespace, TitleHardTab, TitleMustNotContainWord, \
- TitleTrailingPunctuation, TitleLeadingWhitespace, TitleRegexMatches, RuleViolation
+ TitleTrailingPunctuation, TitleLeadingWhitespace, TitleRegexMatches, RuleViolation, TitleMinLength
class TitleRuleTests(BaseTestCase):
@@ -152,3 +152,35 @@ class TitleRuleTests(BaseTestCase):
violations = rule.validate(commit.message.title, commit)
expected_violation = RuleViolation("T7", u"Title does not match regex (^UÅ[0-9]*)", u"US1234: åbc")
self.assertListEqual(violations, [expected_violation])
+
+ def test_min_line_length(self):
+ rule = TitleMinLength()
+
+ # assert no error
+ violation = rule.validate(u"å" * 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)
+ 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)
+ 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)
+ 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)
+ self.assertListEqual(violations, [expected_violation])
+
+ # assert raise on empty title
+ expected_violation = RuleViolation("T8", "Title is too short (0<3)", "", 1)
+ violations = rule.validate("", None)
+ self.assertListEqual(violations, [expected_violation])
diff --git a/gitlint/tests/rules/test_user_rules.py b/gitlint/tests/rules/test_user_rules.py
index 57c03a0..52d0283 100644
--- a/gitlint/tests/rules/test_user_rules.py
+++ b/gitlint/tests/rules/test_user_rules.py
@@ -92,7 +92,7 @@ class UserRuleTests(BaseTestCase):
find_rule_classes(user_rule_path)
def test_find_rule_classes_nonexisting_path(self):
- with self.assertRaisesRegex(UserRuleError, u"Invalid extra-path: föo/bar"):
+ with self.assertRaisesMessage(UserRuleError, u"Invalid extra-path: föo/bar"):
find_rule_classes(u"föo/bar")
def test_assert_valid_rule_class(self):
@@ -111,15 +111,23 @@ class UserRuleTests(BaseTestCase):
def validate(self):
pass
+ class MyConfigurationRuleClass(rules.ConfigurationRule):
+ id = 'UC3'
+ name = u'my-cönfiguration-rule'
+
+ def apply(self):
+ pass
+
# Just assert that no error is raised
self.assertIsNone(assert_valid_rule_class(MyLineRuleClass))
self.assertIsNone(assert_valid_rule_class(MyCommitRuleClass))
+ self.assertIsNone(assert_valid_rule_class(MyConfigurationRuleClass))
def test_assert_valid_rule_class_negative(self):
# general test to make sure that incorrect rules will raise an exception
user_rule_path = self.get_sample_path("user_rules/incorrect_linerule")
- with self.assertRaisesRegex(UserRuleError,
- "User-defined rule class 'MyUserLineRule' must have a 'validate' method"):
+ with self.assertRaisesMessage(UserRuleError,
+ "User-defined rule class 'MyUserLineRule' must have a 'validate' method"):
find_rule_classes(user_rule_path)
def test_assert_valid_rule_class_negative_parent(self):
@@ -127,76 +135,101 @@ class UserRuleTests(BaseTestCase):
class MyRuleClass(object):
pass
- expected_msg = "User-defined rule class 'MyRuleClass' must extend from gitlint.rules.LineRule " + \
- "or gitlint.rules.CommitRule"
- with self.assertRaisesRegex(UserRuleError, expected_msg):
+ expected_msg = "User-defined rule class 'MyRuleClass' must extend from gitlint.rules.LineRule, " + \
+ "gitlint.rules.CommitRule or gitlint.rules.ConfigurationRule"
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
assert_valid_rule_class(MyRuleClass)
def test_assert_valid_rule_class_negative_id(self):
- class MyRuleClass(rules.LineRule):
- pass
- # Rule class must have an id
- expected_msg = "User-defined rule class 'MyRuleClass' must have an 'id' attribute"
- with self.assertRaisesRegex(UserRuleError, expected_msg):
- assert_valid_rule_class(MyRuleClass)
+ for parent_class in [rules.LineRule, rules.CommitRule]:
- # Rule ids must be non-empty
- MyRuleClass.id = ""
- with self.assertRaisesRegex(UserRuleError, expected_msg):
- assert_valid_rule_class(MyRuleClass)
+ class MyRuleClass(parent_class):
+ pass
- # Rule ids must not start with one of the reserved id letters
- for letter in ["T", "R", "B", "M"]:
- MyRuleClass.id = letter + "1"
- expected_msg = "The id '{0}' of 'MyRuleClass' is invalid. Gitlint reserves ids starting with R,T,B,M"
- with self.assertRaisesRegex(UserRuleError, expected_msg.format(letter)):
+ # Rule class must have an id
+ expected_msg = "User-defined rule class 'MyRuleClass' must have an 'id' attribute"
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
assert_valid_rule_class(MyRuleClass)
+ # Rule ids must be non-empty
+ MyRuleClass.id = ""
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
+ assert_valid_rule_class(MyRuleClass)
+
+ # 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)):
+ assert_valid_rule_class(MyRuleClass)
+
def test_assert_valid_rule_class_negative_name(self):
- class MyRuleClass(rules.LineRule):
- id = "UC1"
+ for parent_class in [rules.LineRule, rules.CommitRule]:
- # Rule class must have an name
- expected_msg = "User-defined rule class 'MyRuleClass' must have a 'name' attribute"
- with self.assertRaisesRegex(UserRuleError, expected_msg):
- assert_valid_rule_class(MyRuleClass)
+ class MyRuleClass(parent_class):
+ id = "UC1"
- # Rule names must be non-empty
- MyRuleClass.name = ""
- with self.assertRaisesRegex(UserRuleError, expected_msg):
- assert_valid_rule_class(MyRuleClass)
+ # Rule class must have an name
+ expected_msg = "User-defined rule class 'MyRuleClass' must have a 'name' attribute"
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
+ assert_valid_rule_class(MyRuleClass)
+
+ # Rule names must be non-empty
+ MyRuleClass.name = ""
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
+ assert_valid_rule_class(MyRuleClass)
def test_assert_valid_rule_class_negative_option_spec(self):
- class MyRuleClass(rules.LineRule):
- id = "UC1"
- name = u"my-rüle-class"
- # if set, option_spec must be a list of gitlint options
- MyRuleClass.options_spec = u"föo"
- expected_msg = "The options_spec attribute of user-defined rule class 'MyRuleClass' must be a list " + \
- "of gitlint.options.RuleOption"
- with self.assertRaisesRegex(UserRuleError, expected_msg):
- assert_valid_rule_class(MyRuleClass)
+ for parent_class in [rules.LineRule, rules.CommitRule]:
- # 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
- with self.assertRaisesRegex(UserRuleError, expected_msg):
- assert_valid_rule_class(MyRuleClass)
+ class MyRuleClass(parent_class):
+ id = "UC1"
+ name = u"my-rüle-class"
+
+ # if set, option_spec must be a list of gitlint options
+ MyRuleClass.options_spec = u"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
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
+ assert_valid_rule_class(MyRuleClass)
def test_assert_valid_rule_class_negative_validate(self):
- class MyRuleClass(rules.LineRule):
- id = "UC1"
+
+ baseclasses = [rules.LineRule, rules.CommitRule]
+ for clazz in baseclasses:
+ class MyRuleClass(clazz):
+ id = "UC1"
+ name = u"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"
+ with self.assertRaisesMessage(UserRuleError,
+ "User-defined rule class 'MyRuleClass' must have a 'validate' method"):
+ assert_valid_rule_class(MyRuleClass)
+
+ def test_assert_valid_rule_class_negative_apply(self):
+ class MyRuleClass(rules.ConfigurationRule):
+ id = "UCR1"
name = u"my-rüle-class"
- with self.assertRaisesRegex(UserRuleError,
- "User-defined rule class 'MyRuleClass' must have a 'validate' method"):
+ 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"
- with self.assertRaisesRegex(UserRuleError,
- "User-defined rule class 'MyRuleClass' must have a 'validate' method"):
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
assert_valid_rule_class(MyRuleClass)
def test_assert_valid_rule_class_negative_target(self):
@@ -210,12 +243,12 @@ class UserRuleTests(BaseTestCase):
# no target
expected_msg = "The target attribute of the user-defined LineRule class 'MyRuleClass' must be either " + \
"gitlint.rules.CommitMessageTitle or gitlint.rules.CommitMessageBody"
- with self.assertRaisesRegex(UserRuleError, expected_msg):
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
assert_valid_rule_class(MyRuleClass)
# invalid target
MyRuleClass.target = u"föo"
- with self.assertRaisesRegex(UserRuleError, expected_msg):
+ with self.assertRaisesMessage(UserRuleError, expected_msg):
assert_valid_rule_class(MyRuleClass)
# valid target, no exception should be raised
diff --git a/gitlint/tests/samples/commit_message/no-violations b/gitlint/tests/samples/commit_message/no-violations
new file mode 100644
index 0000000..33c73b9
--- /dev/null
+++ b/gitlint/tests/samples/commit_message/no-violations
@@ -0,0 +1,6 @@
+Normal Commit Tïtle
+
+Nörmal body that contains a few lines of text describing the changes in the
+commit without violating any of gitlint's rules.
+
+Sïgned-Off-By: foo@bar.com
diff --git a/gitlint/tests/samples/config/named-rules b/gitlint/tests/samples/config/named-rules
new file mode 100644
index 0000000..73ab0d2
--- /dev/null
+++ b/gitlint/tests/samples/config/named-rules
@@ -0,0 +1,8 @@
+[title-must-not-contain-word]
+words=WIP,bögus
+
+[title-must-not-contain-word:extra-wörds]
+words=hür,tëst
+
+[T5:even-more-wörds]
+words=hür,tïtle \ No newline at end of file
diff --git a/gitlint/tests/test_hooks.py b/gitlint/tests/test_hooks.py
index 08bd730..62f55e5 100644
--- a/gitlint/tests/test_hooks.py
+++ b/gitlint/tests/test_hooks.py
@@ -58,8 +58,8 @@ class HookTests(BaseTestCase):
git_hooks_dir.return_value = os.path.join(u"/föo", u"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)
- with self.assertRaisesRegex(GitHookInstallerError, expected_msg):
+ expected_msg = u"{0} is not a git repository.".format(lint_config.target)
+ with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
GitHookInstaller.install_commit_msg_hook(lint_config)
isdir.assert_called_with(git_hooks_dir.return_value)
path_exists.assert_not_called()
@@ -71,7 +71,7 @@ class HookTests(BaseTestCase):
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) + \
"gitlint currently does not support appending to an existing commit-msg file."
- with self.assertRaisesRegex(GitHookInstallerError, expected_msg):
+ with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
GitHookInstaller.install_commit_msg_hook(lint_config)
@staticmethod
@@ -104,8 +104,8 @@ class HookTests(BaseTestCase):
# 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)
- with self.assertRaisesRegex(GitHookInstallerError, expected_msg):
+ expected_msg = u"{0} is not a git repository.".format(lint_config.target)
+ with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
GitHookInstaller.uninstall_commit_msg_hook(lint_config)
isdir.assert_called_with(git_hooks_dir.return_value)
path_exists.assert_not_called()
@@ -116,7 +116,7 @@ class HookTests(BaseTestCase):
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)
- with self.assertRaisesRegex(GitHookInstallerError, expected_msg):
+ with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
GitHookInstaller.uninstall_commit_msg_hook(lint_config)
isdir.assert_called_with(git_hooks_dir.return_value)
path_exists.assert_called_once_with(expected_dst)
@@ -131,6 +131,6 @@ class HookTests(BaseTestCase):
"(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):
- with self.assertRaisesRegex(GitHookInstallerError, expected_msg):
+ with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
GitHookInstaller.uninstall_commit_msg_hook(lint_config)
remove.assert_not_called()
diff --git a/gitlint/tests/test_lint.py b/gitlint/tests/test_lint.py
index bcdd984..3bf9a94 100644
--- a/gitlint/tests/test_lint.py
+++ b/gitlint/tests/test_lint.py
@@ -16,7 +16,7 @@ except ImportError:
from gitlint.tests.base import BaseTestCase
from gitlint.lint import GitLinter
-from gitlint.rules import RuleViolation
+from gitlint.rules import RuleViolation, TitleMustNotContainWord
from gitlint.config import LintConfig, LintConfigBuilder
@@ -103,7 +103,7 @@ class LintTests(BaseTestCase):
self.assertListEqual(violations, expected)
def test_lint_meta(self):
- """ Lint sample2 but also add some metadata to the commit so we that get's linted as well """
+ """ 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"
@@ -150,6 +150,25 @@ class LintTests(BaseTestCase):
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(.*)")
+ 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),
+ RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
+ u"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", "This line has a trailing tab.\t", 4),
+ RuleViolation("B3", "Line contains hard tab characters (\\t)",
+ "This line has a trailing tab.\t", 4)]
+
+ self.assertListEqual(violations, expected_errors)
+
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)))
@@ -166,6 +185,31 @@ class LintTests(BaseTestCase):
violations = linter.lint(commit)
self.assertTrue(len(violations) > 0)
+ def test_lint_regex_rules(self):
+ """ Additional test for title-match-regex, body-match-regex """
+ commit = self.gitcommit(self.get_sample("commit_message/no-violations"))
+ lintconfig = LintConfig()
+ linter = GitLinter(lintconfig)
+ violations = linter.lint(commit)
+ # No violations by default
+ 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: (.*)$")]
+ for rule_regex in rule_regexes:
+ lintconfig.set_rule_option(rule_regex[0], "regex", rule_regex[1])
+ violations = linter.lint(commit)
+ self.assertListEqual(violations, [])
+
+ # 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)]
+ 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)]
@@ -195,3 +239,49 @@ class LintTests(BaseTestCase):
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"
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"]:
+ 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"])
+ 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")))
+
+ 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"])
+ 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")
+
+ # 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)]
+ self.assertListEqual(violations, linter.lint(commit))
+
+ # ignore regular rule: only named rule violations show up
+ lint_config.ignore = ["T5"]
+ self.assertListEqual(violations[1:], linter.lint(commit))
+
+ # ignore named rule by id: only regular rule violations show up
+ lint_config.ignore = [rule_id]
+ 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"]
+ self.assertListEqual(violations[:-1], linter.lint(commit))
diff --git a/gitlint/tests/test_options.py b/gitlint/tests/test_options.py
index 2c17226..68f0f8c 100644
--- a/gitlint/tests/test_options.py
+++ b/gitlint/tests/test_options.py
@@ -1,42 +1,51 @@
# -*- coding: utf-8 -*-
import os
+import re
from gitlint.tests.base import BaseTestCase
-from gitlint.options import IntOption, BoolOption, StrOption, ListOption, PathOption, RuleOptionError
+from gitlint.options import IntOption, BoolOption, StrOption, ListOption, PathOption, RegexOption, RuleOptionError
class RuleOptionTests(BaseTestCase):
def test_option_equality(self):
- # 2 options are equal if their name, value and description match
- option1 = IntOption("test-option", 123, u"Test Dëscription")
- option2 = IntOption("test-option", 123, u"Test Dëscription")
- self.assertEqual(option1, option2)
-
- # Not equal: name, description, value are different
- self.assertNotEqual(option1, IntOption("test-option1", 123, u"Test Dëscription"))
- self.assertNotEqual(option1, IntOption("test-option", 1234, u"Test Dëscription"))
- self.assertNotEqual(option1, IntOption("test-option", 123, u"Test Dëscription2"))
+ options = {IntOption: 123, StrOption: u"foöbar", BoolOption: False, ListOption: ["a", "b"],
+ PathOption: ".", RegexOption: u"^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")
+ 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"))
def test_int_option(self):
# normal behavior
- option = IntOption("test-name", 123, "Test Description")
+ 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")
self.assertEqual(option.value, 123)
- self.assertEqual(option.name, "test-name")
- self.assertEqual(option.description, "Test Description")
# re-set value
option.set(456)
self.assertEqual(option.value, 456)
+ # set to None
+ option.set(None)
+ self.assertEqual(option.value, None)
+
# error on negative int when not allowed
- expected_error = u"Option 'test-name' must be a positive integer (current value: '-123')"
- with self.assertRaisesRegex(RuleOptionError, expected_error):
+ expected_error = u"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 'test-name' must be a positive integer (current value: 'foo')"
- with self.assertRaisesRegex(RuleOptionError, expected_error):
+ expected_error = u"Option 'tëst-name' must be a positive integer (current value: 'foo')"
+ with self.assertRaisesMessage(RuleOptionError, expected_error):
option.set("foo")
# no error on negative value when allowed and negative int is passed
@@ -46,15 +55,15 @@ class RuleOptionTests(BaseTestCase):
# error on non-int value when negative int is allowed
expected_error = u"Option 'test-name' must be an integer (current value: 'foo')"
- with self.assertRaisesRegex(RuleOptionError, expected_error):
+ with self.assertRaisesMessage(RuleOptionError, expected_error):
option.set("foo")
def test_str_option(self):
# normal behavior
- option = StrOption("test-name", u"föo", "Test Description")
+ 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")
- self.assertEqual(option.name, "test-name")
- self.assertEqual(option.description, "Test Description")
# re-set value
option.set(u"bår")
@@ -68,9 +77,15 @@ class RuleOptionTests(BaseTestCase):
option.set(-123)
self.assertEqual(option.value, "-123")
+ # None value
+ option.set(None)
+ self.assertEqual(option.value, None)
+
def test_boolean_option(self):
# normal behavior
- option = BoolOption("test-name", "true", "Test Description")
+ 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")
self.assertEqual(option.value, True)
# re-set value
@@ -82,14 +97,16 @@ class RuleOptionTests(BaseTestCase):
self.assertEqual(option.value, True)
# error on incorrect value
- incorrect_values = [1, -1, "foo", u"bår", ["foo"], {'foo': "bar"}]
+ incorrect_values = [1, -1, "foo", u"bår", ["foo"], {'foo': "bar"}, None]
for value in incorrect_values:
- with self.assertRaisesRegex(RuleOptionError, "Option 'test-name' must be either 'true' or 'false'"):
+ with self.assertRaisesMessage(RuleOptionError, u"Option 'tëst-name' must be either 'true' or 'false'"):
option.set(value)
def test_list_option(self):
# normal behavior
- option = ListOption("test-name", u"å,b,c,d", "Test Description")
+ 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"])
# re-set value
@@ -100,6 +117,10 @@ class RuleOptionTests(BaseTestCase):
option.set([u"foo", u"bår", u"test"])
self.assertListEqual(option.value, [u"foo", u"bår", u"test"])
+ # None
+ option.set(None)
+ self.assertIsNone(option.value)
+
# empty string
option.set("")
self.assertListEqual(option.value, [])
@@ -129,40 +150,44 @@ class RuleOptionTests(BaseTestCase):
self.assertListEqual(option.value, ["123"])
def test_path_option(self):
- option = PathOption("test-directory", ".", u"Test Description", type=u"dir")
+ 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")
self.assertEqual(option.value, os.getcwd())
- self.assertEqual(option.name, "test-directory")
- self.assertEqual(option.description, u"Test Description")
self.assertEqual(option.type, u"dir")
# re-set value
option.set(self.SAMPLES_DIR)
self.assertEqual(option.value, self.SAMPLES_DIR)
+ # set to None
+ option.set(None)
+ self.assertIsNone(option.value)
+
# set to int
- expected = u"Option test-directory must be an existing directory (current value: '1234')"
- with self.assertRaisesRegex(RuleOptionError, expected):
+ expected = u"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 test-directory must be an existing directory (current value: '{0}')"
- with self.assertRaisesRegex(RuleOptionError, expected.format(non_existing_path)):
+ expected = u"Option tëst-directory must be an existing directory (current value: '{0}')"
+ with self.assertRaisesMessage(RuleOptionError, expected.format(non_existing_path)):
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 test-directory must be an existing directory (current value: '{0}')".format(sample_path)
- with self.assertRaisesRegex(RuleOptionError, expected):
+ expected = u"Option tëst-directory must be an existing directory (current value: '{0}')".format(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.set(sample_path)
self.assertEqual(option.value, sample_path)
- expected = u"Option test-directory must be an existing file (current value: '{0}')".format(
+ expected = u"Option tëst-directory must be an existing file (current value: '{0}')".format(
self.get_sample_path())
- with self.assertRaisesRegex(RuleOptionError, expected):
+ with self.assertRaisesMessage(RuleOptionError, expected):
option.set(self.get_sample_path())
# set option.type = both, files and directories should now be accepted
@@ -174,6 +199,27 @@ class RuleOptionTests(BaseTestCase):
# Expect exception if path type is invalid
option.type = u'föo'
- expected = u"Option test-directory type must be one of: 'file', 'dir', 'both' (current: 'föo')"
- with self.assertRaisesRegex(RuleOptionError, expected):
+ expected = u"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))
+
+ # re-set value
+ option.set(u"[0-9]föbar.*")
+ self.assertEqual(option.value, re.compile(u"[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]
+ for value in incorrect_values:
+ with self.assertRaisesRegex(RuleOptionError, u"Invalid regular expression"):
+ option.set(value)
diff --git a/gitlint/tests/test_utils.py b/gitlint/tests/test_utils.py
index 6f667c2..5841b63 100644
--- a/gitlint/tests/test_utils.py
+++ b/gitlint/tests/test_utils.py
@@ -60,19 +60,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"lc_all_välue", "LC_CTYPE": u"foo", "LANG": u"bar"}
- self.assertEqual(utils.getpreferredencoding(), u"lc_all_välue")
- mock_env = {"LC_CTYPE": u"lc_ctype_välue", "LANG": u"hur"}
- self.assertEqual(utils.getpreferredencoding(), u"lc_ctype_välue")
- mock_env = {"LANG": u"lang_välue"}
- self.assertEqual(utils.getpreferredencoding(), u"lang_välue")
+ 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")
# Assert split on dot
- mock_env = {"LANG": u"foo.bär"}
- self.assertEqual(utils.getpreferredencoding(), u"bär")
+ mock_env = {"LANG": u"foo.UTF-16"}
+ self.assertEqual(utils.getpreferredencoding(), u"UTF-16")
# assert default encoding is UTF-8
mock_env = {}
self.assertEqual(utils.getpreferredencoding(), "UTF-8")
mock_env = {"FOO": u"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")
diff --git a/gitlint/utils.py b/gitlint/utils.py
index c418347..89015e7 100644
--- a/gitlint/utils.py
+++ b/gitlint/utils.py
@@ -1,4 +1,5 @@
# pylint: disable=bad-option-value,unidiomatic-typecheck,undefined-variable,no-else-return
+import codecs
import platform
import sys
import os
@@ -24,6 +25,16 @@ 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
# On windows we won't want to use the sh library since it's not supported - instead we'll use our own shell module.
@@ -46,13 +57,14 @@ USE_SH_LIB = use_sh_library()
def getpreferredencoding():
""" Modified version of local.getpreferredencoding() that takes into account LC_ALL, LC_CTYPE, LANG env vars
on windows and falls back to UTF-8. """
- default_encoding = locale.getpreferredencoding() or "UTF-8"
+ fallback_encoding = "UTF-8"
+ default_encoding = locale.getpreferredencoding() or fallback_encoding
# On Windows, we mimic git/linux by trying to read the LC_ALL, LC_CTYPE, LANG env vars manually
# (on Linux/MacOS the `getpreferredencoding()` call will take care of this).
# We fallback to UTF-8
if PLATFORM_IS_WINDOWS:
- default_encoding = "UTF-8"
+ default_encoding = fallback_encoding
for env_var in ["LC_ALL", "LC_CTYPE", "LANG"]:
encoding = os.environ.get(env_var, False)
if encoding:
@@ -65,6 +77,15 @@ def getpreferredencoding():
default_encoding = encoding
break
+ # We've determined what encoding the user *wants*, let's now check if it's actually a valid encoding on the
+ # system. If not, fallback to UTF-8.
+ # This scenario is fairly common on Windows where git sets LC_CTYPE=C when invoking the commit-msg hook, which
+ # is not a valid encoding in Python on Windows.
+ try:
+ codecs.lookup(default_encoding)
+ except LookupError:
+ default_encoding = fallback_encoding
+
return default_encoding
@@ -76,7 +97,7 @@ DEFAULT_ENCODING = getpreferredencoding()
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 sys.version_info[0] == 2:
+ 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
@@ -94,11 +115,13 @@ 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 sys.version_info[0] == 2:
- # For lists in python2, remove unicode string representation characters.
+ 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:
diff --git a/mkdocs.yml b/mkdocs.yml
index e373b71..ce3ac60 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -9,9 +9,14 @@ nav:
- Contrib Rules: contrib_rules.md
- User Defined Rules: user_defined_rules.md
- Contributing: contributing.md
- - Changelog: https://github.com/jorisroovers/gitlint/blob/master/CHANGELOG.md
+ - Changelog: https://github.com/jorisroovers/gitlint/blob/main/CHANGELOG.md
markdown_extensions: [admonition]
-theme: readthedocs
+theme:
+ name: readthedocs
+ navigation_depth: 2
strict: true
-extra_css: [extra.css] \ No newline at end of file
+extra_css:
+ - extra.css
+extra_javascript:
+ - extra.js \ No newline at end of file
diff --git a/qa/base.py b/qa/base.py
index 05d85e5..f9e520a 100644
--- a/qa/base.py
+++ b/qa/base.py
@@ -93,6 +93,19 @@ class BaseTestCase(TestCase):
io.open(os.path.join(parent_dir, test_filename), 'a', encoding=DEFAULT_ENCODING).close()
return test_filename
+ @staticmethod
+ def create_environment(envvars=None):
+ """ Creates a copy of the current os.environ and adds/overwrites a given set of variables to it """
+ environment = os.environ.copy()
+ if envvars:
+ environment.update(envvars)
+ return environment
+
+ def create_tmp_git_config(self, contents):
+ """ Creates an environment with the GIT_CONFIG variable set to a file with the given contents. """
+ tmp_config = self.create_tmpfile(contents)
+ return self.create_environment({"GIT_CONFIG": tmp_config})
+
def create_simple_commit(self, message, out=None, ok_code=None, env=None, git_repo=None, tty_in=False):
""" Creates a simple commit with an empty test file.
:param message: Commit message for the commit. """
@@ -103,9 +116,7 @@ class BaseTestCase(TestCase):
# variables can influence how git runs.
# This was needed to fix https://github.com/jorisroovers/gitlint/issues/15 as we need to make sure to use
# the PATH variable that contains the virtualenv's python binary.
- environment = os.environ
- if env:
- environment.update(env)
+ environment = self.create_environment(env)
# Create file and add to git
test_filename = self.create_file(git_repo)
@@ -164,7 +175,7 @@ class BaseTestCase(TestCase):
expected_git_version = git("--version").replace("\n", "")
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}
+ 'GITLINT_USE_SH_LIB': BaseTestCase.GITLINT_USE_SH_LIB, 'DEFAULT_ENCODING': DEFAULT_ENCODING}
def get_debug_vars_last_commit(self, git_repo=None):
""" Returns a dict with items related to `gitlint --debug` output for the last commit. """
diff --git a/qa/expected/test_commits/test_lint_staged_msg_filename_1 b/qa/expected/test_commits/test_lint_staged_msg_filename_1
index 878bc4c..eb2682f 100644
--- a/qa/expected/test_commits/test_lint_staged_msg_filename_1
+++ b/qa/expected/test_commits/test_lint_staged_msg_filename_1
@@ -1,9 +1,11 @@
DEBUG: gitlint.cli To report issues, please visit https://github.com/jorisroovers/gitlint/issues
DEBUG: gitlint.cli Platform: {platform}
DEBUG: gitlint.cli Python version: {python_version}
+DEBUG: gitlint.git ('--version',)
DEBUG: gitlint.cli Git version: {git_version}
DEBUG: gitlint.cli Gitlint version: {gitlint_version}
DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
+DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli Configuration
config-path: None
[GENERAL]
@@ -26,6 +28,8 @@ target: {target}
I2: ignore-by-body
ignore=all
regex=None
+ I3: ignore-body-lines
+ regex=None
T1: title-max-length
line-length=72
T2: title-trailing-whitespace
@@ -35,7 +39,9 @@ target: {target}
T5: title-must-not-contain-word
words=WIP
T7: title-match-regex
- regex=.*
+ regex=None
+ T8: title-min-length
+ min-length=5
B1: body-max-line-length
line-length=80
B5: body-min-length
@@ -47,13 +53,20 @@ target: {target}
B4: body-first-line-empty
B7: body-changed-file-mention
files=
+ B8: body-match-regex
+ regex=None
M1: author-valid-email
regex=[^@ ]+@[^@ ]+\.[^@ ]+
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
DEBUG: gitlint.cli Using --msg-filename.
+DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
DEBUG: gitlint.cli Linting 1 commit(s)
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
+DEBUG: gitlint.git ('config', '--get', 'user.name')
+DEBUG: gitlint.git ('config', '--get', 'user.email')
+DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
+DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r')
DEBUG: gitlint.lint Commit Object
--- Commit Message ----
WIP: from fïle test.
diff --git a/qa/expected/test_commits/test_lint_staged_stdin_1 b/qa/expected/test_commits/test_lint_staged_stdin_1
index 3f178f8..76b5048 100644
--- a/qa/expected/test_commits/test_lint_staged_stdin_1
+++ b/qa/expected/test_commits/test_lint_staged_stdin_1
@@ -1,9 +1,11 @@
DEBUG: gitlint.cli To report issues, please visit https://github.com/jorisroovers/gitlint/issues
DEBUG: gitlint.cli Platform: {platform}
DEBUG: gitlint.cli Python version: {python_version}
+DEBUG: gitlint.git ('--version',)
DEBUG: gitlint.cli Git version: {git_version}
DEBUG: gitlint.cli Gitlint version: {gitlint_version}
DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
+DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli Configuration
config-path: None
[GENERAL]
@@ -26,6 +28,8 @@ target: {target}
I2: ignore-by-body
ignore=all
regex=None
+ I3: ignore-body-lines
+ regex=None
T1: title-max-length
line-length=72
T2: title-trailing-whitespace
@@ -35,7 +39,9 @@ target: {target}
T5: title-must-not-contain-word
words=WIP
T7: title-match-regex
- regex=.*
+ regex=None
+ T8: title-min-length
+ min-length=5
B1: body-max-line-length
line-length=80
B5: body-min-length
@@ -47,6 +53,8 @@ target: {target}
B4: body-first-line-empty
B7: body-changed-file-mention
files=
+ B8: body-match-regex
+ regex=None
M1: author-valid-email
regex=[^@ ]+@[^@ ]+\.[^@ ]+
@@ -54,8 +62,13 @@ DEBUG: gitlint.cli Fetching additional meta-data from staged commit
DEBUG: gitlint.cli Stdin data: 'WIP: Pïpe test.
'
DEBUG: gitlint.cli Stdin detected and not ignored. Using as input.
+DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
DEBUG: gitlint.cli Linting 1 commit(s)
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
+DEBUG: gitlint.git ('config', '--get', 'user.name')
+DEBUG: gitlint.git ('config', '--get', 'user.email')
+DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
+DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r')
DEBUG: gitlint.lint Commit Object
--- Commit Message ----
WIP: Pïpe test.
diff --git a/qa/expected/test_config/test_config_from_env_1 b/qa/expected/test_config/test_config_from_env_1
new file mode 100644
index 0000000..dd761da
--- /dev/null
+++ b/qa/expected/test_config/test_config_from_env_1
@@ -0,0 +1,93 @@
+DEBUG: gitlint.cli To report issues, please visit https://github.com/jorisroovers/gitlint/issues
+DEBUG: gitlint.cli Platform: {platform}
+DEBUG: gitlint.cli Python version: {python_version}
+DEBUG: gitlint.git ('--version',)
+DEBUG: gitlint.cli Git version: {git_version}
+DEBUG: gitlint.cli Gitlint version: {gitlint_version}
+DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
+DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING}
+DEBUG: gitlint.cli Configuration
+config-path: None
+[GENERAL]
+extra-path: None
+contrib: ['CC1', 'CT1']
+ignore: T1,T2
+ignore-merge-commits: True
+ignore-fixup-commits: True
+ignore-squash-commits: True
+ignore-revert-commits: True
+ignore-stdin: True
+staged: False
+verbosity: 2
+debug: True
+target: {target}
+[RULES]
+ I1: ignore-by-title
+ ignore=all
+ regex=None
+ I2: ignore-by-body
+ ignore=all
+ regex=None
+ I3: ignore-body-lines
+ regex=None
+ T1: title-max-length
+ line-length=72
+ T2: title-trailing-whitespace
+ T6: title-leading-whitespace
+ T3: title-trailing-punctuation
+ T4: title-hard-tab
+ T5: title-must-not-contain-word
+ words=WIP
+ T7: title-match-regex
+ regex=None
+ T8: title-min-length
+ min-length=5
+ B1: body-max-line-length
+ line-length=80
+ B5: body-min-length
+ min-length=20
+ B6: body-is-missing
+ ignore-merge-commits=True
+ B2: body-trailing-whitespace
+ B3: body-hard-tab
+ B4: body-first-line-empty
+ B7: body-changed-file-mention
+ files=
+ B8: body-match-regex
+ regex=None
+ M1: author-valid-email
+ regex=[^@ ]+@[^@ ]+\.[^@ ]+
+ CC1: contrib-body-requires-signed-off-by
+ CT1: contrib-title-conventional-commits
+ types=fix,feat,chore,docs,style,refactor,perf,test,revert,ci,build
+
+DEBUG: gitlint.cli No --msg-filename flag, no or empty data passed to stdin. Using the local repo.
+DEBUG: gitlint.git ('rev-list', '{commit_sha}')
+DEBUG: gitlint.cli Linting 1 commit(s)
+DEBUG: gitlint.git ('log', '{commit_sha}', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B')
+DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
+DEBUG: gitlint.lint Linting commit {commit_sha}
+DEBUG: gitlint.git ('branch', '--contains', '{commit_sha}')
+DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', '{commit_sha}')
+DEBUG: gitlint.lint Commit Object
+--- Commit Message ----
+WIP: Thïs is a title thåt is a bit longer.
+Content on the second line
+This line of the body is here because we need it
+
+--- Meta info ---------
+Author: gitlint-test-user <gitlint@test.com>
+Date: {commit_date}
+is-merge-commit: False
+is-fixup-commit: False
+is-squash-commit: False
+is-revert-commit: False
+Branches: ['master']
+Changed Files: {changed_files}
+-----------------------
+1: CC1 Body does not contain a 'Signed-Off-By' line
+1: CT1 Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert, ci, build
+1: T3 Title has trailing punctuation (.)
+1: T5 Title contains the word 'WIP' (case-insensitive)
+2: B4 Second line is not empty
+DEBUG: gitlint.cli Exit Code = 5
diff --git a/qa/expected/test_config/test_config_from_env_2 b/qa/expected/test_config/test_config_from_env_2
new file mode 100644
index 0000000..8d36672
--- /dev/null
+++ b/qa/expected/test_config/test_config_from_env_2
@@ -0,0 +1,83 @@
+DEBUG: gitlint.cli To report issues, please visit https://github.com/jorisroovers/gitlint/issues
+DEBUG: gitlint.cli Platform: {platform}
+DEBUG: gitlint.cli Python version: {python_version}
+DEBUG: gitlint.git ('--version',)
+DEBUG: gitlint.cli Git version: {git_version}
+DEBUG: gitlint.cli Gitlint version: {gitlint_version}
+DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
+DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING}
+DEBUG: gitlint.cli Configuration
+config-path: None
+[GENERAL]
+extra-path: None
+contrib: []
+ignore:
+ignore-merge-commits: True
+ignore-fixup-commits: True
+ignore-squash-commits: True
+ignore-revert-commits: True
+ignore-stdin: False
+staged: True
+verbosity: 0
+debug: True
+target: {target}
+[RULES]
+ I1: ignore-by-title
+ ignore=all
+ regex=None
+ I2: ignore-by-body
+ ignore=all
+ regex=None
+ I3: ignore-body-lines
+ regex=None
+ T1: title-max-length
+ line-length=72
+ T2: title-trailing-whitespace
+ T6: title-leading-whitespace
+ T3: title-trailing-punctuation
+ T4: title-hard-tab
+ T5: title-must-not-contain-word
+ words=WIP
+ T7: title-match-regex
+ regex=None
+ T8: title-min-length
+ min-length=5
+ B1: body-max-line-length
+ line-length=80
+ B5: body-min-length
+ min-length=20
+ B6: body-is-missing
+ ignore-merge-commits=True
+ B2: body-trailing-whitespace
+ B3: body-hard-tab
+ B4: body-first-line-empty
+ B7: body-changed-file-mention
+ files=
+ B8: body-match-regex
+ regex=None
+ M1: author-valid-email
+ regex=[^@ ]+@[^@ ]+\.[^@ ]+
+
+DEBUG: gitlint.cli Fetching additional meta-data from staged commit
+DEBUG: gitlint.cli Using --msg-filename.
+DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
+DEBUG: gitlint.cli Linting 1 commit(s)
+DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
+DEBUG: gitlint.git ('config', '--get', 'user.name')
+DEBUG: gitlint.git ('config', '--get', 'user.email')
+DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
+DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r')
+DEBUG: gitlint.lint Commit Object
+--- Commit Message ----
+WIP: msg-fïlename test.
+--- Meta info ---------
+Author: gitlint-test-user <gitlint@test.com>
+Date: {date}
+is-merge-commit: False
+is-fixup-commit: False
+is-squash-commit: False
+is-revert-commit: False
+Branches: ['master']
+Changed Files: []
+-----------------------
+DEBUG: gitlint.cli Exit Code = 3
diff --git a/qa/expected/test_config/test_config_from_file_debug_1 b/qa/expected/test_config/test_config_from_file_debug_1
index 443ee26..540c3a0 100644
--- a/qa/expected/test_config/test_config_from_file_debug_1
+++ b/qa/expected/test_config/test_config_from_file_debug_1
@@ -1,9 +1,11 @@
DEBUG: gitlint.cli To report issues, please visit https://github.com/jorisroovers/gitlint/issues
DEBUG: gitlint.cli Platform: {platform}
DEBUG: gitlint.cli Python version: {python_version}
+DEBUG: gitlint.git ('--version',)
DEBUG: gitlint.cli Git version: {git_version}
DEBUG: gitlint.cli Gitlint version: {gitlint_version}
DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB}
+DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING}
DEBUG: gitlint.cli Configuration
config-path: {config_path}
[GENERAL]
@@ -26,6 +28,8 @@ target: {target}
I2: ignore-by-body
ignore=all
regex=None
+ I3: ignore-body-lines
+ regex=None
T1: title-max-length
line-length=20
T2: title-trailing-whitespace
@@ -35,7 +39,9 @@ target: {target}
T5: title-must-not-contain-word
words=WIP,thåt
T7: title-match-regex
- regex=.*
+ regex=None
+ T8: title-min-length
+ min-length=5
B1: body-max-line-length
line-length=30
B5: body-min-length
@@ -47,12 +53,19 @@ target: {target}
B4: body-first-line-empty
B7: body-changed-file-mention
files=
+ B8: body-match-regex
+ regex=None
M1: author-valid-email
regex=[^@ ]+@[^@ ]+\.[^@ ]+
DEBUG: gitlint.cli No --msg-filename flag, no or empty data passed to stdin. Using the local repo.
+DEBUG: gitlint.git ('log', '-1', '--pretty=%H')
DEBUG: gitlint.cli Linting 1 commit(s)
+DEBUG: gitlint.git ('log', '{commit_sha}', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B')
+DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
DEBUG: gitlint.lint Linting commit {commit_sha}
+DEBUG: gitlint.git ('branch', '--contains', '{commit_sha}')
+DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', '{commit_sha}')
DEBUG: gitlint.lint Commit Object
--- Commit Message ----
WIP: Thïs is a title thåt is a bit longer.
diff --git a/qa/expected/test_contrib/test_contrib_rules_1 b/qa/expected/test_contrib/test_contrib_rules_1
index 99b33b7..0d333bc 100644
--- a/qa/expected/test_contrib/test_contrib_rules_1
+++ b/qa/expected/test_contrib/test_contrib_rules_1
@@ -1,4 +1,4 @@
1: CC1 Body does not contain a 'Signed-Off-By' line
-1: CT1 Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert: "WIP Thi$ is å title"
+1: CT1 Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert, ci, build: "WIP Thi$ is å title"
1: CT1 Title does not follow ConventionalCommits.org format 'type(optional-scope): description': "WIP Thi$ is å title"
1: T5 Title contains the word 'WIP' (case-insensitive): "WIP Thi$ is å title"
diff --git a/qa/expected/test_named_rules/test_named_rule_1 b/qa/expected/test_named_rules/test_named_rule_1
new file mode 100644
index 0000000..e5a380c
--- /dev/null
+++ b/qa/expected/test_named_rules/test_named_rule_1
@@ -0,0 +1,5 @@
+1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: thåt dûr bår"
+1: T5 Title contains the word 'thåt' (case-insensitive): "WIP: thåt dûr bår"
+1: T5:even$more%wôrds Title contains the word 'bår' (case-insensitive): "WIP: thåt dûr bår"
+1: T5:extra-wôrds Title contains the word 'dûr' (case-insensitive): "WIP: thåt dûr bår"
+3: B5 Body message is too short (18<20): "Sïmple commit body"
diff --git a/qa/expected/test_named_rules/test_named_user_rule_1 b/qa/expected/test_named_rules/test_named_user_rule_1
new file mode 100644
index 0000000..3cd18b4
--- /dev/null
+++ b/qa/expected/test_named_rules/test_named_user_rule_1
@@ -0,0 +1,9 @@
+1: UC4 int-öption: 2
+1: UC4 str-öption: föo
+1: UC4 list-öption: ['foo', 'bar']
+1: UC4:bår int-öption: 2
+1: UC4:bår str-öption: bår
+1: UC4:bår list-öption: ['bar', 'list']
+1: UC4:föo int-öption: 3
+1: UC4:föo str-öption: föo
+1: UC4:föo list-öption: ['foo', 'bar']
diff --git a/qa/expected/test_user_defined/test_user_defined_rules_examples_2 b/qa/expected/test_user_defined/test_user_defined_rules_examples_2
new file mode 100644
index 0000000..31a9280
--- /dev/null
+++ b/qa/expected/test_user_defined/test_user_defined_rules_examples_2
@@ -0,0 +1,4 @@
+1: UC2 Body does not contain a 'Signed-Off-By' line
+1: UC3 Branch name 'master' does not start with one of ['feature/', 'hotfix/', 'release/']
+1: UL1 Title contains the special character '$'
+2: B4 Second line is not empty
diff --git a/qa/expected/test_user_defined/test_user_defined_rules_extra_1 b/qa/expected/test_user_defined/test_user_defined_rules_extra_1
index 65f3507..1f48fad 100644
--- a/qa/expected/test_user_defined/test_user_defined_rules_extra_1
+++ b/qa/expected/test_user_defined/test_user_defined_rules_extra_1
@@ -2,4 +2,8 @@
1: UC1 GitContext.current_branch: master
1: UC1 GitContext.commentchar: #
1: UC2 GitCommit.branches: ['master']
-2: B4 Second line is not empty: "Content on the second line"
+1: UC2 GitCommit.custom_prop: foöbar
+1: UC4 int-öption: 2
+1: UC4 str-öption: föo
+1: UC4 list-öption: ['foo', 'bar']
+4: B2 Line has trailing whitespace: "{repo-path} "
diff --git a/qa/samples/config/named-rules b/qa/samples/config/named-rules
new file mode 100644
index 0000000..f9bbdf5
--- /dev/null
+++ b/qa/samples/config/named-rules
@@ -0,0 +1,8 @@
+[title-must-not-contain-word]
+words=WIP,thåt
+
+[title-must-not-contain-word:extra-wôrds]
+words=hûr,dûr
+
+[title-must-not-contain-word: even$more%wôrds ]
+words=fôo,bår \ No newline at end of file
diff --git a/qa/samples/config/named-user-rules b/qa/samples/config/named-user-rules
new file mode 100644
index 0000000..ed811fb
--- /dev/null
+++ b/qa/samples/config/named-user-rules
@@ -0,0 +1,15 @@
+# Ignore other user-defined rules
+[general]
+ignore=UC1,UC2,UC3,configürable:ignöred
+
+[UC4:föo]
+int-öption=3
+str-öption=föo
+
+[configürable:bår]
+str-öption=bår
+list-öption=bar,list
+
+# The following rule will be ignored
+[configürable:ignöred]
+str-öption=foöbar \ No newline at end of file
diff --git a/qa/samples/user_rules/extra/extra_rules.py b/qa/samples/user_rules/extra/extra_rules.py
index 8109299..6fb985f 100644
--- a/qa/samples/user_rules/extra/extra_rules.py
+++ b/qa/samples/user_rules/extra/extra_rules.py
@@ -1,16 +1,19 @@
-from gitlint.rules import CommitRule, RuleViolation
+# -*- coding: utf-8 -*-
+
+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 = "gitcontext"
+ name = u"gïtcontext"
id = "UC1"
def validate(self, commit):
violations = [
- RuleViolation(self.id, "GitContext.current_branch: {0}".format(commit.context.current_branch), line_nr=1),
- RuleViolation(self.id, "GitContext.commentchar: {0}".format(commit.context.commentchar), line_nr=1)
+ 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)
]
return violations
@@ -18,12 +21,49 @@ class GitContextRule(CommitRule):
class GitCommitRule(CommitRule):
""" Rule that tests whether we can correctly access certain commit properties """
- name = "gitcommit"
+ name = u"gïtcommit"
id = "UC2"
def validate(self, commit):
violations = [
- RuleViolation(self.id, "GitCommit.branches: {0}".format(sstr(commit.branches)), line_nr=1),
+ 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),
+ ]
+
+ return violations
+
+
+class GitlintConfigurationRule(ConfigurationRule):
+ """ Rule that tests whether we can correctly access the config as well as modify the commit message """
+ name = u"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
+
+ # 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"
+
+ # We also ignore some extra rules, proving that we can modify the config
+ config.ignore.append("B4")
+
+
+class ConfigurableCommitRule(CommitRule):
+ """ Rule that tests that we can add configuration to user-defined rules """
+ name = u"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")]
+
+ 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),
]
return violations
diff --git a/qa/shell.py b/qa/shell.py
index 8ba6dc1..43e5bbd 100644
--- a/qa/shell.py
+++ b/qa/shell.py
@@ -3,8 +3,7 @@
# on gitlint internals for our integration testing framework.
import subprocess
-import sys
-from qa.utils import ustr, USE_SH_LIB
+from qa.utils import ustr, USE_SH_LIB, IS_PY2
if USE_SH_LIB:
from sh import git, echo, gitlint # pylint: disable=unused-import,no-name-in-module,import-error
@@ -59,7 +58,7 @@ else:
return ustr(result)
def _exec(*args, **kwargs):
- if sys.version_info[0] == 2:
+ if IS_PY2:
no_command_error = OSError # noqa pylint: disable=undefined-variable,invalid-name
else:
no_command_error = FileNotFoundError # noqa pylint: disable=undefined-variable
@@ -68,6 +67,8 @@ else:
popen_kwargs = {'stdout': pipe, 'stderr': pipe, 'shell': kwargs.get('_tty_out', False)}
if '_cwd' in kwargs:
popen_kwargs['cwd'] = kwargs['_cwd']
+ if '_env' in kwargs:
+ popen_kwargs['env'] = kwargs['_env']
try:
p = subprocess.Popen(args, **popen_kwargs)
diff --git a/qa/test_config.py b/qa/test_config.py
index b893b1d..9415990 100644
--- a/qa/test_config.py
+++ b/qa/test_config.py
@@ -1,8 +1,11 @@
# -*- coding: utf-8 -*-
# pylint: disable=too-many-function-args,unexpected-keyword-arg
+
+import re
+
from qa.shell import gitlint
from qa.base import BaseTestCase
-from qa.utils import sstr
+from qa.utils import sstr, ustr
class ConfigTests(BaseTestCase):
@@ -52,7 +55,7 @@ class ConfigTests(BaseTestCase):
self.assertEqualStdout(output, self.get_expected("test_config/test_config_from_file_1"))
def test_config_from_file_debug(self):
- # Test bot on existing and new repo (we've had a bug in the past that was unique to empty repos)
+ # 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" + \
@@ -65,3 +68,39 @@ class ConfigTests(BaseTestCase):
expected_kwargs.update({'config_path': config_path, 'changed_files': sstr([filename])})
self.assertEqualStdout(output, self.get_expected("test_config/test_config_from_file_debug_1",
expected_kwargs))
+
+ def test_config_from_env(self):
+ """ Test for configuring gitlint from environment variables """
+
+ # 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" + \
+ "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",
+ "GITLINT_IGNORE": "T1,T2", "GITLINT_CONTRIB": "CC1,CT1",
+ "GITLINT_IGNORE_STDIN": "1", "GITLINT_TARGET": target_repo,
+ "GITLINT_COMMITS": self.get_last_commit_hash(git_repo=target_repo)})
+ output = gitlint(_env=env, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5])
+ expected_kwargs = self.get_debug_vars_last_commit(git_repo=target_repo)
+ expected_kwargs.update({'changed_files': sstr([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.")
+ env = self.create_environment({"GITLINT_DEBUG": "1", "GITLINT_TARGET": target_repo,
+ "GITLINT_SILENT": "1", "GITLINT_STAGED": "1"})
+
+ output = gitlint("--msg-filename", tmp_commit_msg_file,
+ _env=env, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3])
+
+ # 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))
+ date = result.group(1).strip()
+ expected_kwargs.update({"date": date})
+
+ self.assertEqualStdout(output, self.get_expected("test_config/test_config_from_env_2", expected_kwargs))
diff --git a/qa/test_gitlint.py b/qa/test_gitlint.py
index 4762721..2e837b9 100644
--- a/qa/test_gitlint.py
+++ b/qa/test_gitlint.py
@@ -157,6 +157,28 @@ class IntegrationTests(BaseTestCase):
self.assertEqualStdout(output, self.get_expected("test_gitlint/test_msg_filename_no_tty_1"))
+ 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.")
+ # 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")
+ 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"
+ 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")
+ 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"
+ self.assertEqualStdout(output, expected)
+
def test_git_errors(self):
# Repo has no commits: caused by `git log`
empty_git_repo = self.create_tmp_git_repo()
diff --git a/qa/test_hooks.py b/qa/test_hooks.py
index a41580b..7c07a61 100644
--- a/qa/test_hooks.py
+++ b/qa/test_hooks.py
@@ -53,6 +53,18 @@ class HookTests(BaseTestCase):
stdin.put("{0}\n".format(response))
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",
+ 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,
+ " 1 file changed, 0 insertions(+), 0 deletions(-)\n",
+ u" 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",
diff --git a/qa/test_named_rules.py b/qa/test_named_rules.py
new file mode 100644
index 0000000..6020bbf
--- /dev/null
+++ b/qa/test_named_rules.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+from qa.shell import gitlint
+from qa.base import BaseTestCase
+
+
+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"
+ 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"
+ 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")
+ output = gitlint("--extra-path", extra_path, "--config", config_path, _cwd=self.tmp_git_repo, _tty_in=True,
+ _ok_code=[9])
+ self.assertEqualStdout(output, self.get_expected("test_named_rules/test_named_user_rule_1"))
diff --git a/qa/test_user_defined.py b/qa/test_user_defined.py
index cf7effd..566d0b2 100644
--- a/qa/test_user_defined.py
+++ b/qa/test_user_defined.py
@@ -7,14 +7,24 @@ from qa.base import BaseTestCase
class UserDefinedRuleTests(BaseTestCase):
""" Integration tests for user-defined rules."""
- def test_user_defined_rules_examples(self):
+ 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"
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"))
+ 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"
+ 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"))
+
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"
self.create_simple_commit(commit_msg)
@@ -27,8 +37,9 @@ class UserDefinedRuleTests(BaseTestCase):
extra_path = self.get_sample_path("user_rules/extra")
commit_msg = u"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_extra_1"))
+ 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",
+ {'repo-path': self.tmp_git_repo}))
def test_invalid_user_defined_rules(self):
extra_path = self.get_sample_path("user_rules/incorrect_linerule")
diff --git a/qa/utils.py b/qa/utils.py
index eb9869a..f44917e 100644
--- a/qa/utils.py
+++ b/qa/utils.py
@@ -16,6 +16,16 @@ 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
# On windows we won't want to use the sh library since it's not supported - instead we'll use our own shell module.
@@ -68,7 +78,7 @@ DEFAULT_ENCODING = getpreferredencoding()
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 sys.version_info[0] == 2:
+ 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
@@ -86,11 +96,13 @@ 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 sys.version_info[0] == 2:
- # For lists in python2, remove unicode string representation characters.
+ 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:
diff --git a/run_tests.sh b/run_tests.sh
index 23ccb37..e558b3b 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -121,7 +121,7 @@ run_integration_tests(){
run_git_check(){
echo -ne "Running gitlint...${RED}"
- RESULT=$(gitlint 2>&1)
+ RESULT=$(gitlint $testargs 2>&1)
local exit_code=$?
handle_test_result $exit_code "$RESULT"
# FUTURE: check if we use str() function: egrep -nriI "( |\(|\[)+str\(" gitlint | egrep -v "\w*#(.*)"
@@ -190,7 +190,7 @@ run_stats(){
echo " Integration Tests: ${nr_integration_tests//[[:space:]]/}"
echo "*** Git ***"
echo " Commits: $(git rev-list --all --count)"
- echo " Commits (master): $(git rev-list master --count)"
+ echo " Commits (main): $(git rev-list main --count)"
echo " First commit: $(git log --pretty="%aD" $(git rev-list --max-parents=0 HEAD))"
echo " Contributors: $(git log --format='%aN' | sort -u | wc -l | tr -d ' ')"
echo " Releases (tags): $(git tag --list | wc -l | tr -d ' ')"
@@ -259,28 +259,23 @@ install_virtualenv(){
python_binary="/usr/bin/python${version:0:1}.${version:1:1}"
# For pypy: custom path + fetch from the web if not installed (=distro agnostic)
- if [[ $version == *"pypy2"* ]]; then
- python_binary="/opt/pypy2.7-v7.3.0-linux64/bin/pypy"
- if [ ! -f $python_binary ]; then
- assert_root "Must be root to install pypy2.7, use sudo"
- title "### DOWNLOADING PYPY2 ($pypy_archive) ###"
- pushd "/opt"
- pypy_archive="pypy2.7-v7.3.0-linux64.tar.bz2"
- wget "https://bitbucket.org/pypy/pypy/downloads/$pypy_archive"
- title "### EXTRACTING PYPY TARBALL ($pypy_archive) ###"
- tar xvf $pypy_archive
- popd
+ 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
+ pypy_full_version="pypy3.6-v7.3.2-linux64"
+ elif [[ $version == *"pypy37"* ]]; then
+ pypy_full_version="pypy3.7-v7.3.2-linux64"
fi
- fi
- if [[ $version == *"pypy35"* ]]; then
- python_binary="/opt/pypy3.5-v7.0.0-linux64/bin/pypy3"
+ python_binary="/opt/$pypy_full_version/bin/pypy"
+ pypy_archive="$pypy_full_version.tar.bz2"
if [ ! -f $python_binary ]; then
- assert_root "Must be root to install pypy3.5, use sudo"
- title "### DOWNLOADING PYPY3 ($pypy_archive) ###"
+ assert_root "Must be root to install $version, use sudo"
+ title "### DOWNLOADING $version ($pypy_archive) ###"
pushd "/opt"
- pypy_archive="pypy3.5-v7.0.0-linux64.tar.bz2"
- wget "https://bitbucket.org/pypy/pypy/downloads/$pypy_archive"
+ wget "$pypy_download_mirror/$pypy_archive"
title "### EXTRACTING PYPY TARBALL ($pypy_archive) ###"
tar xvf $pypy_archive
popd
@@ -466,7 +461,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,pypy2,pypy35"
+ envs="27,35,36,37,38,39,pypy2,pypy35"
fi
original_envs="$envs"
envs=$(echo "$envs" | tr ',' '\n') # Split the env list on comma so we can loop through it
diff --git a/setup.py b/setup.py
index 278e065..1e9f35a 100644
--- a/setup.py
+++ b/setup.py
@@ -17,7 +17,7 @@ except:
description = "Git commit message linter written in python, checks your commit messages for style."
long_description = """
-Great for use as a commit-msg git hook or as part of your gating script in a CI pipeline (e.g. jenkins, gitlab).
+Great for use as a commit-msg git hook or as part of your gating script in a CI pipeline (e.g. jenkins, github actions).
Many of the gitlint validations are based on `well-known`_ community_ `standards`_, others are based on checks that
we've found useful throughout the years. Gitlint has sane defaults, but you can also easily customize it to your
own liking.
@@ -31,7 +31,7 @@ Source code on `github.com/jorisroovers/gitlint`_.
.. _community: http://addamhardy.com/blog/2013/06/05/good-commit-messages-and-enforcing-them-with-git-hooks/
.. _standards: http://chris.beams.io/posts/git-commit/
.. _jorisroovers.github.io/gitlint: https://jorisroovers.github.io/gitlint
-.. _CHANGELOG: https://github.com/jorisroovers/gitlint/blob/master/CHANGELOG.md
+.. _CHANGELOG: https://github.com/jorisroovers/gitlint/blob/main/CHANGELOG.md
.. _github.com/jorisroovers/gitlint: https://github.com/jorisroovers/gitlint
"""
@@ -57,6 +57,7 @@ setup(
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Environment :: Console",
@@ -89,13 +90,15 @@ setup(
},
)
-# Print a red deprecation warning for python 2.6 users
-if sys.version_info[0] == 2 and sys.version_info[1] <= 6:
- msg = "\033[31mDEPRECATION: Python 2.6 or below are no longer supported by gitlint or the Python core team." + \
- "Please upgrade your Python to a later version.\033[0m"
+# 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. " + \
+ "Please upgrade your Python to 3.6 or above.\033[0m"
print(msg)
-# Print a red deprecation warning for python 2.6 users
+# Print a warning message for Windows users
PLATFORM_IS_WINDOWS = "windows" in platform.system().lower()
if PLATFORM_IS_WINDOWS:
msg = "\n\n\n\n\n****************\n" + \
diff --git a/tools/windows/create-test-repo.bat b/tools/windows/create-test-repo.bat
index 4220ad1..27e3394 100644
--- a/tools/windows/create-test-repo.bat
+++ b/tools/windows/create-test-repo.bat
@@ -2,15 +2,16 @@
:: Use pushd, so we can popd back at the end (directory changes are not contained inside batch file)
PUSHD C:\Windows\Temp
-:: Determine unique git repo name
-:: Note that date/time parsing on windows is locale dependent, so this might not work on every windows machine
-:: (see https://stackoverflow.com/questions/203090/how-do-i-get-current-date-time-on-the-windows-command-line-in-a-suitable-format)
@echo off
-For /f "tokens=2-4 delims=/ " %%a in ('date /t') do (set mydate=%%c-%%a-%%b)
-For /f "tokens=1-2 delims=/:" %%a in ("%TIME%") do (set mytime=%%a-%%b)
-echo %mydate%_%mytime%
-set Reponame=gitlint-test-%mydate%_%mytime%
+:: Determine unique git repo name
+:: We use Python to determine to get a datetime stamp since other workarounds in BATCH are locale dependent
+:: Note that we double escape the % in the format string to %%
+FOR /F "tokens=* USEBACKQ" %%F IN (`python -c "import datetime; print(datetime.datetime.now().strftime('%%Y-%%m-%%d_%%H-%%M-%%S'))"`) DO (
+SET datetime=%%F
+)
+echo %datetime%
+set Reponame=gitlint-test-%datetime%
echo %Reponame%
:: Create git repo
@@ -28,8 +29,9 @@ echo "tëst 123" > test.txt
git add test.txt
git commit -m "test cömmit title" -m "test cömmit body that has a bit more text"
-:: echo. -> the dot allows us to print and empty line
+:: echo. -> the dot allows us to print an empty line
echo.
echo Created C:\Windows\Temp\%Reponame%
+
:: Move back to original dir
-POPD
+POPD \ No newline at end of file