Merging upstream version 0.18.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
0453b640a2
commit
129d2ce1fc
118 changed files with 4146 additions and 2087 deletions
21
.devcontainer/Dockerfile
Normal file
21
.devcontainer/Dockerfile
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/python-3/.devcontainer/base.Dockerfile
|
||||||
|
|
||||||
|
# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
|
||||||
|
ARG VARIANT="3.10-bullseye"
|
||||||
|
FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
|
||||||
|
|
||||||
|
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
|
||||||
|
ARG NODE_VERSION="none"
|
||||||
|
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||||
|
|
||||||
|
# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
|
||||||
|
# COPY requirements.txt /tmp/pip-tmp/
|
||||||
|
# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
|
||||||
|
# && rm -rf /tmp/pip-tmp
|
||||||
|
|
||||||
|
# [Optional] Uncomment this section to install additional OS packages.
|
||||||
|
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||||
|
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||||
|
|
||||||
|
# [Optional] Uncomment this line to install global node packages.
|
||||||
|
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
|
56
.devcontainer/devcontainer.json
Normal file
56
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||||
|
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/python-3
|
||||||
|
{
|
||||||
|
"name": "Python 3",
|
||||||
|
"build": {
|
||||||
|
"dockerfile": "Dockerfile",
|
||||||
|
"context": "..",
|
||||||
|
"args": {
|
||||||
|
// Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6
|
||||||
|
// Append -bullseye or -buster to pin to an OS version.
|
||||||
|
// Use -bullseye variants on local on arm64/Apple Silicon.
|
||||||
|
"VARIANT": "3.10",
|
||||||
|
// Options
|
||||||
|
"NODE_VERSION": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
"customizations": {
|
||||||
|
// Configure properties specific to VS Code.
|
||||||
|
"vscode": {
|
||||||
|
// Set *default* container specific settings.json values on container create.
|
||||||
|
"settings": {
|
||||||
|
"python.defaultInterpreterPath": "/usr/local/bin/python",
|
||||||
|
"python.linting.enabled": true,
|
||||||
|
"python.linting.pylintEnabled": true,
|
||||||
|
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
|
||||||
|
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||||
|
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
||||||
|
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
|
||||||
|
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
|
||||||
|
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
|
||||||
|
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
|
||||||
|
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
|
||||||
|
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
|
||||||
|
},
|
||||||
|
// Add the IDs of extensions you want installed when the container is created.
|
||||||
|
"extensions": [
|
||||||
|
"ms-python.python",
|
||||||
|
"ms-python.vscode-pylance"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [],
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "pip3 install --user -r requirements.txt",
|
||||||
|
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||||
|
"remoteUser": "vscode",
|
||||||
|
"features": {
|
||||||
|
"git": "latest",
|
||||||
|
"github-cli": "latest",
|
||||||
|
"sshd": "latest",
|
||||||
|
"homebrew": "latest"
|
||||||
|
},
|
||||||
|
"postCreateCommand": "./.devcontainer/postCreateCommand.sh"
|
||||||
|
}
|
23
.devcontainer/postCreateCommand.sh
Executable file
23
.devcontainer/postCreateCommand.sh
Executable file
|
@ -0,0 +1,23 @@
|
||||||
|
#!/bin/sh -x
|
||||||
|
|
||||||
|
brew install asdf
|
||||||
|
source "$(brew --prefix asdf)/libexec/asdf.sh"
|
||||||
|
|
||||||
|
# Install latest python
|
||||||
|
asdf plugin add python
|
||||||
|
asdf install python 3.11.0
|
||||||
|
asdf global python 3.11.0
|
||||||
|
|
||||||
|
# You can easily install other python versions like so:
|
||||||
|
# asdf install python 3.6.15
|
||||||
|
# asdf install python 3.7.15
|
||||||
|
# asdf install python 3.8.15
|
||||||
|
# asdf install python 3.9.15
|
||||||
|
# asdf install python 3.10.8
|
||||||
|
# asdf install python pypy3.9-7.3.9
|
||||||
|
|
||||||
|
# Setup virtualenv, install all dependencies
|
||||||
|
cd /workspaces/gitlint
|
||||||
|
$(asdf which python) -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt -r test-requirements.txt -r doc-requirements.txt
|
11
.flake8
11
.flake8
|
@ -1,11 +0,0 @@
|
||||||
[flake8]
|
|
||||||
# H307: like imports should be grouped together
|
|
||||||
# H405: multi line docstring summary not separated with an empty line
|
|
||||||
# H803: git title must end with a period
|
|
||||||
# H904: Wrap long lines in parentheses instead of a backslash
|
|
||||||
# H802: git commit title should be under 50 chars
|
|
||||||
# H701: empty localization string
|
|
||||||
extend-ignore = H307,H405,H803,H904,H802,H701
|
|
||||||
# exclude settings files and virtualenvs
|
|
||||||
exclude = *settings.py,*.venv/*.py
|
|
||||||
max-line-length = 120
|
|
44
.github/workflows/checks.yml
vendored
44
.github/workflows/checks.yml
vendored
|
@ -7,21 +7,21 @@ jobs:
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", pypy3]
|
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", pypy-3.9]
|
||||||
os: ["macos-latest", "ubuntu-latest"]
|
os: ["macos-latest", "ubuntu-latest"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3.0.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }} # Checkout pull request HEAD commit instead of merge commit
|
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
|
# 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.
|
# 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.
|
# 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
|
- name: Temporarily remove git version control from code
|
||||||
run: mv .git ._git
|
run: mv .git ._git
|
||||||
|
|
||||||
- name: Setup python
|
- name: Setup python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
@ -40,16 +40,28 @@ jobs:
|
||||||
# COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
# COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
||||||
# run: coveralls
|
# run: coveralls
|
||||||
|
|
||||||
|
# Patch the commit-msg hook to make it work in GH CI
|
||||||
|
# Specifically, within the commit-msg hook, wrap the invocation of gitlint with `script`
|
||||||
|
|
||||||
|
- name: Patch commit-msg hook
|
||||||
|
run: |
|
||||||
|
# Escape " to \"
|
||||||
|
sed -i -E '/^gitlint/ s/"/\\"/g' gitlint-core/gitlint/files/commit-msg
|
||||||
|
# Replace `gitlint <args>` with `script -e -q -c "gitlint <args>"`
|
||||||
|
sed -i -E 's/^gitlint(.*)/script -e -q -c "\0"/' gitlint-core/gitlint/files/commit-msg
|
||||||
|
|
||||||
- name: Integration Tests
|
- name: Integration Tests
|
||||||
run: ./run_tests.sh -i
|
run: ./run_tests.sh -i
|
||||||
|
|
||||||
- name: Integration Tests (GITLINT_USE_SH_LIB=0)
|
# Gitlint no longer uses `sh` by default, but for now we're still supporting the manual enablement of it.
|
||||||
|
# By setting GITLINT_USE_SH_LIB=1, we test whether this still works.
|
||||||
|
- name: Integration Tests (GITLINT_USE_SH_LIB=1)
|
||||||
env:
|
env:
|
||||||
GITLINT_USE_SH_LIB: 0
|
GITLINT_USE_SH_LIB: 1
|
||||||
run: ./run_tests.sh -i
|
run: ./run_tests.sh -i
|
||||||
|
|
||||||
- name: PEP8
|
- name: Code formatting (black)
|
||||||
run: ./run_tests.sh -p
|
run: ./run_tests.sh -f
|
||||||
|
|
||||||
- name: PyLint
|
- name: PyLint
|
||||||
run: ./run_tests.sh -l
|
run: ./run_tests.sh -l
|
||||||
|
@ -79,25 +91,25 @@ jobs:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [3.6]
|
python-version: ["3.10"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3.0.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }} # Checkout pull request HEAD commit instead of merge commit
|
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
|
# 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.
|
# 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.
|
# 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
|
- name: Temporarily remove git version control from code
|
||||||
run: Rename-Item .git ._git
|
run: Rename-Item .git ._git
|
||||||
|
|
||||||
- name: Setup python
|
- name: Setup python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v4.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: "Upgrade pip on Python 3"
|
- name: "Upgrade pip on Python 3"
|
||||||
if: matrix.python-version == '3.6'
|
if: matrix.python-version == '3.10'
|
||||||
run: python -m pip install --upgrade pip
|
run: python -m pip install --upgrade pip
|
||||||
|
|
||||||
- name: Install requirements
|
- name: Install requirements
|
||||||
|
@ -126,8 +138,8 @@ jobs:
|
||||||
run: pytest -rw -s qa
|
run: pytest -rw -s qa
|
||||||
continue-on-error: true # Known to fail at this point
|
continue-on-error: true # Known to fail at this point
|
||||||
|
|
||||||
- name: PEP8
|
- name: Code formatting (black)
|
||||||
run: flake8 gitlint-core qa examples
|
run: black .
|
||||||
|
|
||||||
- name: PyLint
|
- name: PyLint
|
||||||
run: pylint gitlint-core\gitlint qa --rcfile=".pylintrc" -r n
|
run: pylint gitlint-core\gitlint qa --rcfile=".pylintrc" -r n
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
- id: gitlint
|
- id: gitlint
|
||||||
name: gitlint
|
name: gitlint
|
||||||
language: python
|
description: Checks your git commit messages for style.
|
||||||
additional_dependencies: ["./gitlint-core[trusted-deps]"]
|
language: python
|
||||||
entry: gitlint
|
additional_dependencies: ["./gitlint-core[trusted-deps]"]
|
||||||
args: [--staged, --msg-filename]
|
entry: gitlint
|
||||||
stages: [commit-msg]
|
args: [--staged, --msg-filename]
|
||||||
|
stages: [commit-msg]
|
||||||
|
- id: gitlint-ci
|
||||||
|
name: gitlint
|
||||||
|
language: python
|
||||||
|
additional_dependencies: ["./gitlint-core[trusted-deps]"]
|
||||||
|
entry: gitlint
|
||||||
|
always_run: true
|
||||||
|
pass_filenames: false
|
||||||
|
stages: [manual]
|
||||||
|
|
40
CHANGELOG.md
40
CHANGELOG.md
|
@ -1,5 +1,37 @@
|
||||||
# Changelog #
|
# Changelog #
|
||||||
|
|
||||||
|
|
||||||
|
## v0.18.0 (2022-11-16) ##
|
||||||
|
Contributors:
|
||||||
|
Special thanks to all contributors for this release - details inline!
|
||||||
|
|
||||||
|
- Python 3.11 support
|
||||||
|
- Last release to support Python 3.6 ([EOL since 2021-12-23](https://endoflife.date/python))
|
||||||
|
- **Behavior Change**: In a future release, gitlint will be switching to use `re.search` instead of `re.match` semantics for all rules. Your rule regexes might need updating as a result, gitlint will print a warning if so. [More details are in the docs](https://jorisroovers.com/gitlint/configuration/#regex-style-search). ([#254](https://github.com/jorisroovers/gitlint/issues/254))
|
||||||
|
- gitlint no longer uses the [sh](https://amoffat.github.io/sh/) library by default in an attempt to reduce external dependencies. In case of issues, the use of `sh` can be re-enabled by setting the env var `GITLINT_USE_SH_LIB=1`. This fallback will be removed entirely in a future gitlint release. ([#351](https://github.com/jorisroovers/gitlint/issues/351))
|
||||||
|
- `--commits` now also accepts a comma-separated list of commit hashes, making it possible to lint a list of non-contiguous commits without invoking gitlint multiple times ([#283](https://github.com/jorisroovers/gitlint/issues/283))
|
||||||
|
- Improved handling of branches that have no commits ([#188](https://github.com/jorisroovers/gitlint/issues/189)) - thanks [domsekotill](https://github.com/domsekotill)
|
||||||
|
- Support for `GITLINT_CONFIG` env variable ([#189](https://github.com/jorisroovers/gitlint/issues/188)) - thanks [Notgnoshi](https://github.com/Notgnoshi)
|
||||||
|
- Added [a new `gitlint-ci` pre-commit hook](https://jorisroovers.com/gitlint/#gitlint-and-pre-commit-in-ci), making it easier to run gitlint through pre-commit in CI ([#191](https://github.com/jorisroovers/gitlint/issues/191)) - thanks [guillaumelambert](https://github.com/guillaumelambert)
|
||||||
|
- Contrib Rules:
|
||||||
|
- New [contrib-disallow-cleanup-commits](https://jorisroovers.com/gitlint/contrib_rules/#cc2-contrib-disallow-cleanup-commits) rule ([#312](https://github.com/jorisroovers/gitlint/issues/312)) - thanks [matthiasbeyer](https://github.com/matthiasbeyer)
|
||||||
|
- New [contrib-allowed-authors](https://jorisroovers.com/gitlint/contrib_rules/#cc3-contrib-allowed-authors) rule ([#358](https://github.com/jorisroovers/gitlint/issues/358)) - thanks [stauchert](https://github.com/stauchert)
|
||||||
|
- User Defined rules:
|
||||||
|
- Gitlint now recognizes `fixup=amend` commits (see related [git documentation](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupamendrewordltcommitgt)), available as `commit.is_fixup_amend_commit=True`
|
||||||
|
- Gitlint now parses diff **stat** information, available in `commit.changed_files_stats` ([#314](https://github.com/jorisroovers/gitlint/issues/314))
|
||||||
|
- Bugfixes:
|
||||||
|
- Use correct encoding when using `--msg-filename` parameter ([#310](https://github.com/jorisroovers/gitlint/issues/310))
|
||||||
|
- Various documentation fixes ([#244](https://github.com/jorisroovers/gitlint/issues/244)) ([#263](https://github.com/jorisroovers/gitlint/issues/263)) ([#266](https://github.com/jorisroovers/gitlint/issues/266)) ([#294](https://github.com/jorisroovers/gitlint/issues/294)) ([#295](https://github.com/jorisroovers/gitlint/issues/295)) ([#347](https://github.com/jorisroovers/gitlint/issues/347)) ([#364](https://github.com/jorisroovers/gitlint/issues/364)) - thanks [scop](https://github.com/scop), [OrBin](https://github.com/OrBin), [jtaylor100](https://github.com/jtaylor100), [stauchert](https://github.com/stauchert)
|
||||||
|
- Under-the-hood:
|
||||||
|
- Dependencies updated
|
||||||
|
- Moved to [black](https://github.com/psf/black) for formatting
|
||||||
|
- Fixed nasty CI issue ([#298](https://github.com/jorisroovers/gitlint/issues/298))
|
||||||
|
- Unit tests fix ([#256](https://github.com/jorisroovers/gitlint/issues/256)) - thanks [carlsmedstad](https://github.com/carlsmedstad)
|
||||||
|
- Vagrant box removed in favor of github dev containers ([#348](https://github.com/jorisroovers/gitlint/issues/348))
|
||||||
|
- Removed a few lingering references to the `master` branch in favor of `main`
|
||||||
|
- Moved [roadmap and project planning](https://github.com/users/jorisroovers/projects/1) to github projects
|
||||||
|
- Thanks to [sigmavirus24](https://github.com/sigmavirus24) for continued overall help and support
|
||||||
|
|
||||||
## v0.17.0 (2021-11-28) ##
|
## v0.17.0 (2021-11-28) ##
|
||||||
Contributors:
|
Contributors:
|
||||||
Special thanks to all contributors for this release, in particular [andersk](https://github.com/andersk) and [sigmavirus24](https://github.com/sigmavirus24).
|
Special thanks to all contributors for this release, in particular [andersk](https://github.com/andersk) and [sigmavirus24](https://github.com/sigmavirus24).
|
||||||
|
@ -13,12 +45,12 @@ Special thanks to all contributors for this release, in particular [sigmavirus24
|
||||||
|
|
||||||
- Python 3.10 support
|
- Python 3.10 support
|
||||||
- **New Rule**: [ignore-by-author-name](http://jorisroovers.github.io/gitlint/rules/#i4-ignore-by-author-name) allows users to skip linting commit messages made by specific authors
|
- **New Rule**: [ignore-by-author-name](http://jorisroovers.github.io/gitlint/rules/#i4-ignore-by-author-name) allows users to skip linting commit messages made by specific authors
|
||||||
- `--commit <SHA>` flag to more easily lint a single commit message ([#141](https://github.com/jorisroovers/gitlint/issues/141))
|
- `--commit <ref>` flag to more easily lint a single commit message ([#141](https://github.com/jorisroovers/gitlint/issues/141))
|
||||||
- `--fail-without-commits` flag will force gitlint to fail ([exit code 253](https://jorisroovers.com/gitlint/#exit-codes)) when the target commit range is empty (typically when using `--commits`) ([#193](https://github.com/jorisroovers/gitlint/issues/193))
|
- `--fail-without-commits` flag will force gitlint to fail ([exit code 253](https://jorisroovers.com/gitlint/#exit-codes)) when the target commit range is empty (typically when using `--commits`) ([#193](https://github.com/jorisroovers/gitlint/issues/193))
|
||||||
- Bugfixes:
|
- Bugfixes:
|
||||||
- [contrib-title-conventional-commits (CT1)](https://jorisroovers.com/gitlint/contrib_rules/#ct1-contrib-title-conventional-commits) now properly enforces the commit type ([#185](https://github.com/jorisroovers/gitlint/issues/185))
|
- [contrib-title-conventional-commits (CT1)](https://jorisroovers.com/gitlint/contrib_rules/#ct1-contrib-title-conventional-commits) now properly enforces the commit type ([#185](https://github.com/jorisroovers/gitlint/issues/185))
|
||||||
- [contrib-title-conventional-commits (CT1)](https://jorisroovers.com/gitlint/contrib_rules/#ct1-contrib-title-conventional-commits) now supports the BREAKING CHANGE symbol "!" ([#186](https://github.com/jorisroovers/gitlint/issues/186))
|
- [contrib-title-conventional-commits (CT1)](https://jorisroovers.com/gitlint/contrib_rules/#ct1-contrib-title-conventional-commits) now supports the BREAKING CHANGE symbol "!" ([#186](https://github.com/jorisroovers/gitlint/issues/186))
|
||||||
- Heads-up: [Python 3.6 will become EOL at the end of 2021](https://endoflife.date/python). It's likely that future gitlint releases will stop supporting Python 3.6 as a result. We will continue to support Python 3.6 as long as its easily doable, which in practice usually means as long as our dependencies support it.
|
- Heads-up: [Python 3.6 will become EOL at the end of 2021](https://endoflife.date/python). It's likely that future gitlint releases will stop supporting Python 3.6 as a result. We will continue to support Python 3.6 as long as it's easily doable, which in practice usually means as long as our dependencies support it.
|
||||||
- Under-the-hood: dependencies updated, test and github action improvements.
|
- Under-the-hood: dependencies updated, test and github action improvements.
|
||||||
## v0.15.1 (2021-04-16) ##
|
## v0.15.1 (2021-04-16) ##
|
||||||
|
|
||||||
|
@ -168,8 +200,8 @@ and [AlexMooney](https://github.com/AlexMooney) for their contributions.
|
||||||
[Rules section of the documentation](http://jorisroovers.github.io/gitlint/rules/#m1-author-valid-email).
|
[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
|
- **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
|
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,
|
that linting a single commit using `gitlint --commits <ref>` won't work anymore. Instead, for single commits,
|
||||||
users now need to specificy `gitlint --commits <SHA>^...<SHA>`. On the upside, this change also means
|
users now need to specificy `gitlint --commits <ref>^...<ref>`. On the upside, this change also means
|
||||||
that gitlint will now understand all refspec formatters, including `gitlint --commits HEAD` to lint all commits
|
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).
|
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
|
- **Breaking change**: Gitlint now always falls back on trying to read a git message from a local git repository, only
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
# NOTE: --ulimit is required to work around a limitation in Docker
|
# NOTE: --ulimit is required to work around a limitation in Docker
|
||||||
# Details: https://github.com/jorisroovers/gitlint/issues/129
|
# Details: https://github.com/jorisroovers/gitlint/issues/129
|
||||||
|
|
||||||
FROM python:3.10-alpine
|
FROM python:3.11.0-alpine
|
||||||
ARG GITLINT_VERSION
|
ARG GITLINT_VERSION
|
||||||
|
|
||||||
RUN apk add git
|
RUN apk add git
|
||||||
|
|
|
@ -20,4 +20,4 @@ All contributions are welcome and very much appreciated!
|
||||||
See [jorisroovers.github.io/gitlint/contributing](http://jorisroovers.github.io/gitlint/contributing) for details on
|
See [jorisroovers.github.io/gitlint/contributing](http://jorisroovers.github.io/gitlint/contributing) for details on
|
||||||
how to get started - it's easy!
|
how to get started - it's easy!
|
||||||
|
|
||||||
We maintain a [loose roadmap on our wiki](https://github.com/jorisroovers/gitlint/wiki/Roadmap).
|
We maintain a [loose project plan on Github Projects](https://github.com/users/jorisroovers/projects/1/views/1).
|
||||||
|
|
49
Vagrantfile
vendored
49
Vagrantfile
vendored
|
@ -1,49 +0,0 @@
|
||||||
# -*- mode: ruby -*-
|
|
||||||
# vi: set ft=ruby :
|
|
||||||
|
|
||||||
VAGRANTFILE_API_VERSION = "2"
|
|
||||||
|
|
||||||
INSTALL_DEPS=<<EOF
|
|
||||||
cd /vagrant
|
|
||||||
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y --allow-unauthenticated python3.6-dev python3.7-dev python3.8-dev python3.9-dev
|
|
||||||
sudo apt-get install -y --allow-unauthenticated python3.8-distutils python3.9-distutils # Needed to work around python3.8/9+virtualenv issue
|
|
||||||
sudo apt-get install -y git python3-pip ripgrep jq
|
|
||||||
sudo apt-get install -y build-essential libssl-dev libffi-dev # for rebuilding cryptography (required for pypy2)
|
|
||||||
sudo apt-get install -y python3-pip
|
|
||||||
pip3 install -U pip
|
|
||||||
pip3 install 'virtualenv!=20.1.0'
|
|
||||||
|
|
||||||
./run_tests.sh --uninstall --envs all
|
|
||||||
./run_tests.sh --install --envs all
|
|
||||||
|
|
||||||
grep 'cd /vagrant' /home/vagrant/.bashrc || echo 'cd /vagrant' >> /home/vagrant/.bashrc
|
|
||||||
grep 'source .venv36/bin/activate' /home/vagrant/.bashrc || echo 'source .venv36/bin/activate' >> /home/vagrant/.bashrc
|
|
||||||
EOF
|
|
||||||
|
|
||||||
INSTALL_JENKINS=<<EOF
|
|
||||||
wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add -
|
|
||||||
sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y openjdk-8-jre
|
|
||||||
sudo apt-get install -y jenkins
|
|
||||||
EOF
|
|
||||||
|
|
||||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
|
||||||
|
|
||||||
config.vm.box = "ubuntu/focal64"
|
|
||||||
|
|
||||||
config.vm.define "dev" do |dev|
|
|
||||||
dev.vm.provision "gitlint", type: "shell", inline: "#{INSTALL_DEPS}"
|
|
||||||
# Use 'vagrant provision --provision-with jenkins' to only run jenkins install
|
|
||||||
dev.vm.provision "jenkins", type: "shell", inline: "#{INSTALL_JENKINS}"
|
|
||||||
end
|
|
||||||
|
|
||||||
config.vm.network "forwarded_port", guest: 8080, host: 9080
|
|
||||||
|
|
||||||
if Vagrant.has_plugin?("vagrant-cachier")
|
|
||||||
config.cache.scope = :box
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
|
@ -1 +1 @@
|
||||||
mkdocs==1.2.3
|
mkdocs==1.4.1
|
|
@ -11,7 +11,7 @@ gitlint generate-config
|
||||||
You can also use a different config file like so:
|
You can also use a different config file like so:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
gitlint --config myconfigfile.ini
|
gitlint --config myconfigfile.ini
|
||||||
```
|
```
|
||||||
|
|
||||||
The block below shows a sample `.gitlint` file. Details about rule config options can be found on the
|
The block below shows a sample `.gitlint` file. Details about rule config options can be found on the
|
||||||
|
@ -39,16 +39,17 @@ ignore=title-trailing-punctuation, T3
|
||||||
# precedence over this
|
# precedence over this
|
||||||
verbosity = 2
|
verbosity = 2
|
||||||
|
|
||||||
# By default gitlint will ignore merge, revert, fixup and squash commits.
|
# By default gitlint will ignore merge, revert, fixup, fixup=amend, and squash commits.
|
||||||
ignore-merge-commits=true
|
ignore-merge-commits=true
|
||||||
ignore-revert-commits=true
|
ignore-revert-commits=true
|
||||||
ignore-fixup-commits=true
|
ignore-fixup-commits=true
|
||||||
|
ignore-fixup-amend-commits=true
|
||||||
ignore-squash-commits=true
|
ignore-squash-commits=true
|
||||||
|
|
||||||
# Ignore any data send to gitlint via stdin
|
# Ignore any data sent to gitlint via stdin
|
||||||
ignore-stdin=true
|
ignore-stdin=true
|
||||||
|
|
||||||
# Fetch additional meta-data from the local repository when manually passing a
|
# Fetch additional meta-data from the local repository when manually passing a
|
||||||
# commit message to gitlint via stdin or --commit-msg. Disabled by default.
|
# commit message to gitlint via stdin or --commit-msg. Disabled by default.
|
||||||
staged=true
|
staged=true
|
||||||
|
|
||||||
|
@ -58,6 +59,11 @@ staged=true
|
||||||
# Disabled by default.
|
# Disabled by default.
|
||||||
fail-without-commits=true
|
fail-without-commits=true
|
||||||
|
|
||||||
|
# Whether to use Python `search` instead of `match` semantics in rules that use
|
||||||
|
# regexes. Context: https://github.com/jorisroovers/gitlint/issues/254
|
||||||
|
# Disabled by default, but will be enabled by default in the future.
|
||||||
|
regex-style-search=true
|
||||||
|
|
||||||
# Enable debug mode (prints more output). Disabled by default.
|
# Enable debug mode (prints more output). Disabled by default.
|
||||||
debug=true
|
debug=true
|
||||||
|
|
||||||
|
@ -187,7 +193,7 @@ 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:
|
You can also specify specific rules to be ignored as follows:
|
||||||
```
|
```
|
||||||
WIP: This is my commit message
|
WIP: This is my commit message
|
||||||
|
|
||||||
|
@ -201,7 +207,7 @@ gitlint-ignore: T1, body-hard-tab
|
||||||
gitlint configuration is applied in the following order of 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))
|
2. Configuration Rules (e.g.: [ignore-by-title](rules.md#i1-ignore-by-title))
|
||||||
3. Commandline convenience flags (e.g.: `-vv`, `--silent`, `--ignore`)
|
3. Commandline convenience flags (e.g.: `-vv`, `--silent`, `--ignore`)
|
||||||
4. Environment variables (e.g.: `GITLINT_VERBOSITY=3`)
|
4. Environment variables (e.g.: `GITLINT_VERBOSITY=3`)
|
||||||
5. Commandline configuration flags (e.g.: `-c title-max-length=123`)
|
5. Commandline configuration flags (e.g.: `-c title-max-length=123`)
|
||||||
|
@ -216,9 +222,9 @@ using commandline flags or in `[general]` section in a `.gitlint` configuration
|
||||||
|
|
||||||
Enable silent mode (no output). Use [exit](index.md#exit-codes) code to determine result.
|
Enable silent mode (no output). Use [exit](index.md#exit-codes) code to determine result.
|
||||||
|
|
||||||
Default value | gitlint version | commandline flag | environment variable
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
---------------|------------------|-------------------|-----------------------
|
| ------------- | --------------- | ---------------- | -------------------- |
|
||||||
`False` | >= 0.1.0 | `--silent` | `GITLINT_SILENT`
|
| `False` | >= 0.1.0 | `--silent` | `GITLINT_SILENT` |
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
```sh
|
```sh
|
||||||
|
@ -226,14 +232,15 @@ Default value | gitlint version | commandline flag | environment variable
|
||||||
gitlint --silent
|
gitlint --silent
|
||||||
GITLINT_SILENT=1 gitlint # using env variable
|
GITLINT_SILENT=1 gitlint # using env variable
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
### verbosity
|
### verbosity
|
||||||
|
|
||||||
Amount of output gitlint will show when printing errors.
|
Amount of output gitlint will show when printing errors.
|
||||||
|
|
||||||
Default value | gitlint version | commandline flag | environment variable
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
---------------|------------------|-------------------|-----------------------
|
| ------------- | --------------- | ---------------- | -------------------- |
|
||||||
3 | >= 0.1.0 | `-v` | `GITLINT_VERBOSITY`
|
| 3 | >= 0.1.0 | `-v` | `GITLINT_VERBOSITY` |
|
||||||
|
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
|
@ -252,14 +259,15 @@ GITLINT_VERBOSITY=2 gitlint # using env variable
|
||||||
[general]
|
[general]
|
||||||
verbosity=2
|
verbosity=2
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
### ignore
|
### ignore
|
||||||
|
|
||||||
Comma separated list of rules to ignore (by name or id).
|
Comma separated list of rules to ignore (by name or id).
|
||||||
|
|
||||||
Default value | gitlint version | commandline flag | environment variable
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
---------------------------|------------------|-------------------|-----------------------
|
| ---------------- | --------------- | ---------------- | -------------------- |
|
||||||
[] (=empty list) | >= 0.1.0 | `--ignore` | `GITLINT_IGNORE`
|
| [] (=empty list) | >= 0.1.0 | `--ignore` | `GITLINT_IGNORE` |
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
```sh
|
```sh
|
||||||
|
@ -274,14 +282,15 @@ GITLINT_IGNORE=T1,body-min-length gitlint # using env variable
|
||||||
[general]
|
[general]
|
||||||
ignore=T1,body-min-length
|
ignore=T1,body-min-length
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
### debug
|
### debug
|
||||||
|
|
||||||
Enable debugging output.
|
Enable debugging output.
|
||||||
|
|
||||||
Default value | gitlint version | commandline flag | environment variable
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
---------------|------------------|-------------------|-----------------------
|
| ------------- | --------------- | ---------------- | -------------------- |
|
||||||
false | >= 0.7.1 | `--debug` | `GITLINT_DEBUG`
|
| false | >= 0.7.1 | `--debug` | `GITLINT_DEBUG` |
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
```sh
|
```sh
|
||||||
|
@ -291,14 +300,15 @@ GITLINT_DEBUG=1 gitlint # using env variable
|
||||||
# --debug is special, the following does NOT work
|
# --debug is special, the following does NOT work
|
||||||
# gitlint -c general.debug=true
|
# gitlint -c general.debug=true
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
### target
|
### target
|
||||||
|
|
||||||
Target git repository gitlint should be linting against.
|
Target git repository gitlint should be linting against.
|
||||||
|
|
||||||
Default value | gitlint version | commandline flag | environment variable
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
---------------------------|------------------|-------------------|-----------------------
|
| ------------- | --------------- | ---------------- | -------------------- |
|
||||||
(empty) | >= 0.8.0 | `--target` | `GITLINT_TARGET`
|
| (empty) | >= 0.8.0 | `--target` | `GITLINT_TARGET` |
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
```sh
|
```sh
|
||||||
|
@ -312,14 +322,31 @@ GITLINT_TARGET=/home/joe/myrepo/ gitlint # using env variable
|
||||||
[general]
|
[general]
|
||||||
target=/home/joe/myrepo/
|
target=/home/joe/myrepo/
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### config
|
||||||
|
|
||||||
|
Path where gitlint looks for a config file.
|
||||||
|
|
||||||
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
|
| ------------- | --------------- | ---------------- | -------------------- |
|
||||||
|
| `.gitlint` | >= 0.1.0 | `--config` | `GITLINT_CONFIG` |
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
```sh
|
||||||
|
gitlint --config=/home/joe/gitlint.ini
|
||||||
|
gitlint -C /home/joe/gitlint.ini # different way of doing the same
|
||||||
|
GITLINT_CONFIG=/home/joe/gitlint.ini # using env variable
|
||||||
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
### extra-path
|
### extra-path
|
||||||
|
|
||||||
Path where gitlint looks for [user-defined rules](user_defined_rules.md).
|
Path where gitlint looks for [user-defined rules](user_defined_rules.md).
|
||||||
|
|
||||||
Default value | gitlint version | commandline flag | environment variable
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
---------------------------|------------------|-------------------|-----------------------
|
| ------------- | --------------- | ---------------- | -------------------- |
|
||||||
(empty) | >= 0.8.0 | `--extra-path` | `GITLINT_EXTRA_PATH`
|
| (empty) | >= 0.8.0 | `--extra-path` | `GITLINT_EXTRA_PATH` |
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
```sh
|
```sh
|
||||||
|
@ -333,14 +360,14 @@ GITLINT_EXTRA_PATH=/home/joe/rules/ gitlint # using env variable
|
||||||
[general]
|
[general]
|
||||||
extra-path=/home/joe/rules/
|
extra-path=/home/joe/rules/
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
### contrib
|
### contrib
|
||||||
|
|
||||||
Comma-separated list of [Contrib rules](contrib_rules) to enable (by name or id).
|
Comma-separated list of [Contrib rules](contrib_rules.md) to enable (by name or id).
|
||||||
|
|
||||||
Default value | gitlint version | commandline flag | environment variable
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
---------------------------|------------------|-------------------|-----------------------
|
| ------------- | --------------- | ---------------- | -------------------- |
|
||||||
(empty) | >= 0.12.0 | `--contrib` | `GITLINT_CONTRIB`
|
| (empty) | >= 0.12.0 | `--contrib` | `GITLINT_CONTRIB` |
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
```sh
|
```sh
|
||||||
|
@ -349,21 +376,31 @@ gitlint --contrib=contrib-title-conventional-commits,CC1
|
||||||
# different way of doing the same
|
# different way of doing the same
|
||||||
gitlint -c general.contrib=contrib-title-conventional-commits,CC1
|
gitlint -c general.contrib=contrib-title-conventional-commits,CC1
|
||||||
# using env variable
|
# using env variable
|
||||||
GITLINT_CONTRIB=contrib-title-conventional-commits,CC1 gitlint
|
GITLINT_CONTRIB=contrib-title-conventional-commits,CC1 gitlint
|
||||||
```
|
```
|
||||||
```ini
|
```ini
|
||||||
#.gitlint
|
#.gitlint
|
||||||
[general]
|
[general]
|
||||||
contrib=contrib-title-conventional-commits,CC1
|
contrib=contrib-title-conventional-commits,CC1
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
### staged
|
### staged
|
||||||
|
|
||||||
Fetch additional meta-data from the local repository when manually passing a commit message to gitlint via stdin or `--commit-msg`.
|
Attempt smart guesses about meta info (like author name, email, branch, changed files, etc) when manually passing a
|
||||||
|
commit message to gitlint via stdin or `--commit-msg`.
|
||||||
|
|
||||||
Default value | gitlint version | commandline flag | environment variable
|
Since in such cases no actual git commit exists (yet) for the message being linted, gitlint
|
||||||
---------------|------------------|-------------------|-----------------------
|
needs to apply some heuristics (like checking `git config` and any staged changes) to make a smart guess about what the
|
||||||
false | >= 0.13.0 | `--staged` | `GITLINT_STAGED`
|
likely author name, email, commit date, changed files and branch of the ensuing commit would be.
|
||||||
|
|
||||||
|
When not using the `--staged` flag while linting a commit message via stdin or `--commit-msg`, gitlint will only have
|
||||||
|
access to the commit message itself for linting and won't be able to enforce rules like
|
||||||
|
[M1:author-valid-email](rules.md#m1-author-valid-email).
|
||||||
|
|
||||||
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
|
| ------------- | --------------- | ---------------- | -------------------- |
|
||||||
|
| false | >= 0.13.0 | `--staged` | `GITLINT_STAGED` |
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
```sh
|
```sh
|
||||||
|
@ -377,6 +414,7 @@ GITLINT_STAGED=1 gitlint # using env variable
|
||||||
[general]
|
[general]
|
||||||
staged=true
|
staged=true
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
### fail-without-commits
|
### fail-without-commits
|
||||||
|
|
||||||
|
@ -384,15 +422,15 @@ Hard fail when the target commit range is empty. Note that gitlint will
|
||||||
already fail by default on invalid commit ranges. This option is specifically
|
already fail by default on invalid commit ranges. This option is specifically
|
||||||
to tell gitlint to fail on **valid but empty** commit ranges.
|
to tell gitlint to fail on **valid but empty** commit ranges.
|
||||||
|
|
||||||
Default value | gitlint version | commandline flag | environment variable
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
---------------|------------------|---------------------------|-----------------------
|
| ------------- | --------------- | ------------------------ | ------------------------------ |
|
||||||
false | >= 0.15.2 | `--fail-without-commits` | `GITLINT_FAIL_WITHOUT_COMMITS`
|
| false | >= 0.15.2 | `--fail-without-commits` | `GITLINT_FAIL_WITHOUT_COMMITS` |
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
```sh
|
```sh
|
||||||
# CLI
|
# CLI
|
||||||
# The following will cause gitlint to hard fail (i.e. exit code > 0)
|
# The following will cause gitlint to hard fail (i.e. exit code > 0)
|
||||||
# since HEAD..HEAD is a valid but empty commit range.
|
# since HEAD..HEAD is a valid but empty commit range.
|
||||||
gitlint --fail-without-commits --commits HEAD..HEAD
|
gitlint --fail-without-commits --commits HEAD..HEAD
|
||||||
GITLINT_FAIL_WITHOUT_COMMITS=1 gitlint # using env variable
|
GITLINT_FAIL_WITHOUT_COMMITS=1 gitlint # using env variable
|
||||||
```
|
```
|
||||||
|
@ -402,13 +440,79 @@ GITLINT_FAIL_WITHOUT_COMMITS=1 gitlint # using env variable
|
||||||
fail-without-commits=true
|
fail-without-commits=true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
### regex-style-search
|
||||||
|
|
||||||
|
Whether to use Python `re.search()` instead of `re.match()` semantics in all built-in rules that use regular expressions.
|
||||||
|
|
||||||
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
|
| ------------- | --------------- | ---------------- | -------------------- |
|
||||||
|
| false | >= 0.18.0 | Not Available | Not Available |
|
||||||
|
|
||||||
|
!!! important
|
||||||
|
At this time, `regex-style-search` is **disabled** by default, but it will be **enabled** by default in the future.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Gitlint will log a warning when you're using a rule that uses a custom regex and this option is not enabled:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
WARNING: I1 - ignore-by-title: gitlint will be switching from using Python regex 'match' (match beginning) to
|
||||||
|
'search' (match anywhere) semantics. Please review your ignore-by-title.regex option accordingly.
|
||||||
|
To remove this warning, set general.regex-style-search=True.
|
||||||
|
More details: https://jorisroovers.github.io/gitlint/configuration/#regex-style-search
|
||||||
|
```
|
||||||
|
|
||||||
|
*If you don't have any custom regex specified, gitlint will not log a warning and no action is needed.*
|
||||||
|
|
||||||
|
**To remove the warning:**
|
||||||
|
|
||||||
|
1. Review your regex in the rules gitlint warned for and ensure it's still accurate when using [`re.search()` semantics](https://docs.python.org/3/library/re.html#search-vs-match).
|
||||||
|
2. Enable `regex-style-search` in your `.gitlint` file (or using [any other way to configure gitlint](http://127.0.0.1:8000/gitlint/configuration/)):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[general]
|
||||||
|
regex-style-search=true
|
||||||
|
```
|
||||||
|
|
||||||
|
#### More context
|
||||||
|
Python offers [two different primitive operations based on regular expressions](https://docs.python.org/3/library/re.html#search-vs-match):
|
||||||
|
`re.match()` checks for a match only at the beginning of the string, while `re.search()` checks for a match anywhere
|
||||||
|
in the string.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Most rules in gitlint already use `re.search()` instead of `re.match()`, but there's a few notable exceptions that
|
||||||
|
use `re.match()`, which can lead to unexpected matching behavior.
|
||||||
|
|
||||||
|
- M1 - author-valid-email
|
||||||
|
- I1 - ignore-by-title
|
||||||
|
- I2 - ignore-by-body
|
||||||
|
- I3 - ignore-body-lines
|
||||||
|
- I4 - ignore-by-author-name
|
||||||
|
|
||||||
|
The `regex-style-search` option is meant to fix this inconsistency. Setting it to `true` will force the above rules to
|
||||||
|
use `re.search()` instead of `re.match()`. For detailed context, see [issue #254](https://github.com/jorisroovers/gitlint/issues/254).
|
||||||
|
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
```sh
|
||||||
|
# CLI
|
||||||
|
gitlint -c general.regex-style-search=true
|
||||||
|
```
|
||||||
|
```ini
|
||||||
|
#.gitlint
|
||||||
|
[general]
|
||||||
|
regex-style-search=true
|
||||||
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
### ignore-stdin
|
### ignore-stdin
|
||||||
|
|
||||||
Ignore any stdin data. Sometimes useful when running gitlint in a CI server.
|
Ignore any stdin data. Sometimes useful when running gitlint in a CI server.
|
||||||
|
|
||||||
Default value | gitlint version | commandline flag | environment variable
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
---------------|------------------|-------------------|-----------------------
|
| ------------- | --------------- | ---------------- | ---------------------- |
|
||||||
false | >= 0.12.0 | `--ignore-stdin` | `GITLINT_IGNORE_STDIN`
|
| false | >= 0.12.0 | `--ignore-stdin` | `GITLINT_IGNORE_STDIN` |
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
```sh
|
```sh
|
||||||
|
@ -422,14 +526,15 @@ GITLINT_IGNORE_STDIN=1 gitlint # using env variable
|
||||||
[general]
|
[general]
|
||||||
ignore-stdin=true
|
ignore-stdin=true
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
### ignore-merge-commits
|
### ignore-merge-commits
|
||||||
|
|
||||||
Whether or not to ignore merge commits.
|
Whether or not to ignore merge commits.
|
||||||
|
|
||||||
Default value | gitlint version | commandline flag | environment variable
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
---------------|------------------|-------------------|-----------------------
|
| ------------- | --------------- | ---------------- | -------------------- |
|
||||||
true | >= 0.7.0 | Not Available | Not Available
|
| true | >= 0.7.0 | Not Available | Not Available |
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
```sh
|
```sh
|
||||||
|
@ -441,14 +546,15 @@ gitlint -c general.ignore-merge-commits=false
|
||||||
[general]
|
[general]
|
||||||
ignore-merge-commits=false
|
ignore-merge-commits=false
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
### ignore-revert-commits
|
### ignore-revert-commits
|
||||||
|
|
||||||
Whether or not to ignore revert commits.
|
Whether or not to ignore revert commits.
|
||||||
|
|
||||||
Default value | gitlint version | commandline flag | environment variable
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
---------------|------------------|-------------------|-----------------------
|
| ------------- | --------------- | ---------------- | -------------------- |
|
||||||
true | >= 0.13.0 | Not Available | Not Available
|
| true | >= 0.13.0 | Not Available | Not Available |
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
```sh
|
```sh
|
||||||
|
@ -460,14 +566,15 @@ gitlint -c general.ignore-revert-commits=false
|
||||||
[general]
|
[general]
|
||||||
ignore-revert-commits=false
|
ignore-revert-commits=false
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
### ignore-fixup-commits
|
### ignore-fixup-commits
|
||||||
|
|
||||||
Whether or not to ignore [fixup](https://git-scm.com/docs/git-commit#git-commit---fixupltcommitgt) commits.
|
Whether or not to ignore [fixup](https://git-scm.com/docs/git-commit#git-commit---fixupltcommitgt) commits.
|
||||||
|
|
||||||
Default value | gitlint version | commandline flag | environment variable
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
---------------|------------------|-------------------|-----------------------
|
| ------------- | --------------- | ---------------- | -------------------- |
|
||||||
true | >= 0.9.0 | Not Available | Not Available
|
| true | >= 0.9.0 | Not Available | Not Available |
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
```sh
|
```sh
|
||||||
|
@ -479,14 +586,35 @@ gitlint -c general.ignore-fixup-commits=false
|
||||||
[general]
|
[general]
|
||||||
ignore-fixup-commits=false
|
ignore-fixup-commits=false
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### ignore-fixup-amend-commits
|
||||||
|
|
||||||
|
Whether or not to ignore [fixup=amend](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupamendrewordltcommitgt) commits.
|
||||||
|
|
||||||
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
|
| ------------- | --------------- | ---------------- | -------------------- |
|
||||||
|
| true | >= 0.18.0 | Not Available | Not Available |
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
```sh
|
||||||
|
# CLI
|
||||||
|
gitlint -c general.ignore-fixup-amend-commits=false
|
||||||
|
```
|
||||||
|
```ini
|
||||||
|
#.gitlint
|
||||||
|
[general]
|
||||||
|
ignore-fixup-amend-commits=false
|
||||||
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
### ignore-squash-commits
|
### ignore-squash-commits
|
||||||
|
|
||||||
Whether or not to ignore [squash](https://git-scm.com/docs/git-commit#git-commit---squashltcommitgt) commits.
|
Whether or not to ignore [squash](https://git-scm.com/docs/git-commit#git-commit---squashltcommitgt) commits.
|
||||||
|
|
||||||
Default value | gitlint version | commandline flag | environment variable
|
| Default value | gitlint version | commandline flag | environment variable |
|
||||||
---------------|------------------|-------------------|-----------------------
|
| ------------- | --------------- | ---------------- | -------------------- |
|
||||||
true | >= 0.9.0 | Not Available | Not Available
|
| true | >= 0.9.0 | Not Available | Not Available |
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
```sh
|
```sh
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
# Using Contrib Rules
|
# Using Contrib Rules
|
||||||
|
|
||||||
_Introduced in gitlint v0.12.0_
|
_Introduced in gitlint v0.12.0_
|
||||||
|
|
||||||
Contrib rules are community-**contrib**uted rules that are disabled by default, but can be enabled through configuration.
|
Contrib rules are community-**contrib**uted 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
|
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. This also means that users don't have to
|
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.
|
re-implement these commonly used rules themselves as [user-defined](user_defined_rules.md) rules.
|
||||||
|
|
||||||
To enable certain contrib rules, you can use the `--contrib` flag.
|
To enable certain contrib rules, you can use the `--contrib` flag.
|
||||||
```sh
|
```sh
|
||||||
|
@ -42,6 +43,8 @@ ID | Name | gitlint version | Description
|
||||||
------|-------------------------------------|------------------ |-------------------------------------------
|
------|-------------------------------------|------------------ |-------------------------------------------
|
||||||
CT1 | contrib-title-conventional-commits | >= 0.12.0 | Enforces [Conventional Commits](https://www.conventionalcommits.org/) commit message style on the title.
|
CT1 | contrib-title-conventional-commits | >= 0.12.0 | Enforces [Conventional Commits](https://www.conventionalcommits.org/) commit message style on the title.
|
||||||
CC1 | contrib-body-requires-signed-off-by | >= 0.12.0 | Commit body must contain a `Signed-off-by` line.
|
CC1 | contrib-body-requires-signed-off-by | >= 0.12.0 | Commit body must contain a `Signed-off-by` line.
|
||||||
|
CC2 | contrib-disallow-cleanup-commits | >= 0.18.0 | Commit title must not contain `fixup!`, `squash!`, `amend!`.
|
||||||
|
CC3 | contrib-allowed-authors | >= 0.18.0 | Enforce that only authors listed in the `AUTHORS` file are allowed to commit.
|
||||||
|
|
||||||
## CT1: contrib-title-conventional-commits ##
|
## CT1: contrib-title-conventional-commits ##
|
||||||
|
|
||||||
|
@ -63,5 +66,18 @@ ID | Name | gitlint version | Description
|
||||||
CC1 | contrib-body-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.
|
CC1 | contrib-body-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.
|
||||||
|
|
||||||
|
|
||||||
|
## CC2: contrib-disallow-cleanup-commits ##
|
||||||
|
|
||||||
|
ID | Name | gitlint version | Description
|
||||||
|
------|----------------------------------|--------------------|-------------------------------------------
|
||||||
|
CC2 | contrib-disallow-cleanup-commits | >= 0.18.0 | Commit title must not contain `fixup!`, `squash!` or `amend!`. This means `git commit --fixup` and `git commit --squash` commits are not allowed.
|
||||||
|
|
||||||
|
## CC3: contrib-allowed-authors ##
|
||||||
|
|
||||||
|
ID | Name | gitlint version | Description
|
||||||
|
------|----------------------------------|--------------------|-------------------------------------------
|
||||||
|
CC3 | contrib-allowed-authors | >= 0.18.0 | The commit author must be listed in an `AUTHORS` file to be allowed to commit. Possible file names are also `AUTHORS.txt` and `AUTHORS.md`.
|
||||||
|
|
||||||
## Contributing Contrib rules
|
## 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.
|
|
||||||
|
We'd love for you to contribute new Contrib rules to gitlint or improve existing ones! Please visit the [Contributing](contributing.md) page on how to get started.
|
||||||
|
|
|
@ -6,7 +6,7 @@ The [source-code and issue tracker](https://github.com/jorisroovers/gitlint) are
|
||||||
Often it takes a while for us (well, actually just [me](https://github.com/jorisroovers)) to get back to you
|
Often it takes a while for us (well, actually just [me](https://github.com/jorisroovers)) to get back to you
|
||||||
(sometimes up to a few months, this is a hobby project), but rest assured that we read your message and appreciate
|
(sometimes up to a few months, this is a hobby project), but rest assured that we read your message and appreciate
|
||||||
your interest!
|
your interest!
|
||||||
We maintain a [loose roadmap on our wiki](https://github.com/jorisroovers/gitlint/wiki/Roadmap), but
|
We maintain a [loose project plan on github projects](https://github.com/users/jorisroovers/projects/1/), but
|
||||||
that's open to a lot of change and input.
|
that's open to a lot of change and input.
|
||||||
|
|
||||||
## Guidelines
|
## Guidelines
|
||||||
|
@ -19,11 +19,15 @@ When contributing code, please consider all the parts that are typically require
|
||||||
- [Integration tests](https://github.com/jorisroovers/gitlint/tree/main/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
|
[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.
|
for your functionality, not only updating existing ones to make the build pass.
|
||||||
- [Documentation](https://github.com/jorisroovers/gitlint/tree/main/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
|
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
|
can make it as part of a release. **Gitlint commits and pull requests are gated on all of our tests and checks as well as
|
||||||
and it's likely that your PR will be merged and released a lot sooner. Thanks!
|
code-review**. 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.
|
||||||
|
|
||||||
|
It's also a good idea to open an issue before submitting a PR for non-trivial changes, so we can discuss what you have
|
||||||
|
in mind before you spend the effort. Thanks!
|
||||||
|
|
||||||
!!! Important
|
!!! Important
|
||||||
**On the topic of releases**: Gitlint releases typically go out when there's either enough new features and fixes
|
**On the topic of releases**: Gitlint releases typically go out when there's either enough new features and fixes
|
||||||
|
@ -32,55 +36,105 @@ and it's likely that your PR will be merged and released a lot sooner. Thanks!
|
||||||
or months before merged code actually gets released - we know that can be frustrating but please understand it's
|
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.
|
a well-considered trade-off based on available time.
|
||||||
|
|
||||||
## Development
|
## Local setup
|
||||||
|
|
||||||
There is a Vagrantfile (Ubuntu) in this repository that can be used for development.
|
To install gitlint for local development:
|
||||||
It comes pre-installed with all Python versions that gitlint supports.
|
|
||||||
```sh
|
|
||||||
vagrant up
|
|
||||||
vagrant ssh
|
|
||||||
```
|
|
||||||
|
|
||||||
Or you can choose to use your local environment:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
. .venv/bin/activate
|
. .venv/bin/activate
|
||||||
pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt -r test-requirements.txt -r doc-requirements.txt
|
pip install -r requirements.txt -r test-requirements.txt -r doc-requirements.txt
|
||||||
python setup.py develop
|
python setup.py develop
|
||||||
```
|
```
|
||||||
|
|
||||||
To run tests:
|
## Github Devcontainer
|
||||||
|
|
||||||
|
We provide a devcontainer on github to make it easier to get started with gitlint development using VSCode.
|
||||||
|
|
||||||
|
To start one, click the plus button under the *Code* dropdown on
|
||||||
|
[the gitlint repo on github](https://github.com/jorisroovers/gitlint).
|
||||||
|
|
||||||
|
**It can take ~15min for all post installation steps to finish.**
|
||||||
|
|
||||||
|
![Gitlint Dev Container Instructions](images/dev-container.png)
|
||||||
|
|
||||||
|
|
||||||
|
After setup has finished, you should be able to just activate the virtualenv in the home dir and run the tests:
|
||||||
|
```sh
|
||||||
|
. ~/.venv/bin/activate
|
||||||
|
./run_tests.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
By default we have python 3.11 installed in the dev container, but you can also use [asdf](https://asdf-vm.com/)
|
||||||
|
(preinstalled) to install additional python versions:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Ensure ASDF overrides system python in PATH
|
||||||
|
# You can also append this line to your ~/.bash_profile in the devcontainer to have this happen automatically on login
|
||||||
|
source "$(brew --prefix asdf)/libexec/asdf.sh"
|
||||||
|
|
||||||
|
# Install python 3.9.15
|
||||||
|
asdf install python 3.9.15
|
||||||
|
# List all available python versions
|
||||||
|
asdf list all python
|
||||||
|
# List installed python versions
|
||||||
|
asdf list python
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running tests
|
||||||
```sh
|
```sh
|
||||||
./run_tests.sh # run unit tests and print test coverage
|
./run_tests.sh # run unit tests and print test coverage
|
||||||
./run_tests.sh gitlint/tests/rules/test_body_rules.py::BodyRuleTests::test_body_missing # run a single test
|
./run_tests.sh gitlint-core/gitlint/tests/rules/test_body_rules.py::BodyRuleTests::test_body_missing # run a single test
|
||||||
|
pytest -k test_body_missing # Alternative way to run a specific test by invoking pytest directly with a keyword expression
|
||||||
./run_tests.sh --no-coverage # run unit tests without test coverage
|
./run_tests.sh --no-coverage # run unit tests without test coverage
|
||||||
./run_tests.sh --collect-only --no-coverage # Only collect, don't run unit tests
|
./run_tests.sh --collect-only --no-coverage # Only collect, don't run unit tests
|
||||||
./run_tests.sh --integration # Run integration tests (requires that you have gitlint installed)
|
./run_tests.sh --integration # Run integration tests (requires that you have gitlint installed)
|
||||||
./run_tests.sh --build # Run build tests (=build python package)
|
./run_tests.sh --build # Run build tests (=build python package)
|
||||||
./run_tests.sh --pep8 # pep8 checks
|
./run_tests.sh --format # format checks (black)
|
||||||
./run_tests.sh --stats # print some code stats
|
./run_tests.sh --stats # print some code stats
|
||||||
./run_tests.sh --git # inception: run gitlint against itself
|
./run_tests.sh --git # inception: run gitlint against itself
|
||||||
./run_tests.sh --lint # run pylint checks
|
./run_tests.sh --lint # run pylint checks
|
||||||
./run_tests.sh --all # Run unit, integration, pep8 and gitlint checks
|
./run_tests.sh --all # Run unit, integration, format and gitlint checks
|
||||||
```
|
```
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
We use [black](https://black.readthedocs.io/en/stable/) for code formatting.
|
||||||
|
To use it, just run black against the code you modified:
|
||||||
|
|
||||||
The `Vagrantfile` comes with `virtualenv`s for python 3.6, 3.7, 3.8, 3.9 and pypy3.6.
|
|
||||||
You can easily run tests against specific python environments by using the following commands *inside* of the Vagrant VM:
|
|
||||||
```sh
|
```sh
|
||||||
./run_tests.sh --envs 36 # Run the unit tests against Python 3.6
|
black . # format all python code
|
||||||
./run_tests.sh --envs 36,37,pypy36 # Run the unit tests against Python 3.6, Python 3.7 and Pypy3.6
|
black gitlint-core/gitlint/lint.py # format a specific file
|
||||||
./run_tests.sh --envs 36,37 --pep8 # Run pep8 checks against Python 3.6 and Python 3.7 (also works for --git, --integration, --pep8, --stats and --lint.
|
|
||||||
./run_tests.sh --envs all --all # Run all tests against all environments
|
|
||||||
./run_tests.sh --all-env --all # Idem: Run all tests against all environments
|
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! important
|
## Documentation
|
||||||
Gitlint commits and pull requests are gated on all of our tests and checks.
|
We use [mkdocs](https://www.mkdocs.org/) for generating our documentation from markdown.
|
||||||
|
|
||||||
|
To use it:
|
||||||
|
```sh
|
||||||
|
pip install -r doc-requirements.txt # install doc requirements
|
||||||
|
mkdocs serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Then access the documentation website on [http://localhost:8000]().
|
||||||
|
|
||||||
## Packaging
|
## Packaging
|
||||||
|
|
||||||
|
Gitlint consists of 2 python packages: [gitlint](https://pypi.org/project/gitlint/)
|
||||||
|
and [gitlint-core](https://pypi.org/project/gitlint-core/).
|
||||||
|
|
||||||
|
The `gitlint` package is just a wrapper package around `gitlint-core[trusted-deps]` which strictly pins gitlint
|
||||||
|
dependencies to known working versions.
|
||||||
|
|
||||||
|
There are scenarios where users (or OS package managers) may want looser dependency requirements.
|
||||||
|
In these cases, users can just install `gitlint-core` directly (`pip install gitlint-core`).
|
||||||
|
|
||||||
|
[Issue 162](https://github.com/jorisroovers/gitlint/issues/162) has all the background of how we got to the decision
|
||||||
|
to split gitlint in 2 packages.
|
||||||
|
|
||||||
|
![Gitlint package structure](images/gitlint-packages.png)
|
||||||
|
|
||||||
|
### Packaging description
|
||||||
|
|
||||||
To see the package description in HTML format
|
To see the package description in HTML format
|
||||||
```sh
|
```sh
|
||||||
pip install docutils
|
pip install docutils
|
||||||
|
@ -89,16 +143,6 @@ export LANG=en_US.UTF-8
|
||||||
python setup.py --long-description | rst2html.py > output.html
|
python setup.py --long-description | rst2html.py > output.html
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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):
|
|
||||||
```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
|
## Tools
|
||||||
We keep a small set of scripts in the `tools/` directory:
|
We keep a small set of scripts in the `tools/` directory:
|
||||||
|
@ -110,13 +154,13 @@ 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
|
Since gitlint 0.12.0, we support [Contrib rules](contrib_rules.md): community contributed rules that are part of gitlint
|
||||||
itself. Thanks for considering to add a new one to gitlint!
|
itself. Thanks for considering to add a new one to gitlint!
|
||||||
|
|
||||||
Before starting, please read all the other documentation on this page about contributing first.
|
Before starting, please read all the other documentation on this page about contributing first.
|
||||||
Then, we suggest taking the following approach to add a Contrib rule:
|
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
|
1. **Write your rule as a [user-defined rule](user_defined_rules.md)**. 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.
|
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/main/gitlint-core/gitlint/contrib/rules) directory.
|
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-core/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-core/gitlint/tests/contrib/rules).
|
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-core/gitlint/tests/contrib/rules).
|
||||||
|
@ -129,7 +173,7 @@ If you follow the steps above and follow the existing gitlint conventions wrt na
|
||||||
|
|
||||||
In case you're looking for a slightly more formal spec, here's what gitlint requires of Contrib rules.
|
In case you're looking for a slightly more formal spec, here's what gitlint requires of Contrib rules.
|
||||||
|
|
||||||
- Since Contrib rules are really just user-defined rules that live within the gitlint code-base, all the [user-rule requirements](../user_defined_rules/#rule-requirements) also apply to Contrib rules.
|
- Since Contrib rules are really just user-defined rules that live within the gitlint code-base, all the [user-rule requirements](user_defined_rules.md#rule-requirements) also apply to Contrib rules.
|
||||||
- All contrib rules **must** have associated unit tests. We *sort of* enforce this by a unit test that verifies that there's a
|
- All contrib rules **must** have associated unit tests. We *sort of* enforce this by a unit test that verifies that there's a
|
||||||
test file for each contrib file.
|
test file for each contrib file.
|
||||||
- All contrib rules **must** have names that start with `contrib-`. This is to easily distinguish them from default gitlint rules.
|
- All contrib rules **must** have names that start with `contrib-`. This is to easily distinguish them from default gitlint rules.
|
||||||
|
@ -137,4 +181,4 @@ In case you're looking for a slightly more formal spec, here's what gitlint requ
|
||||||
- All contrib rules **must** have unique names and ids.
|
- All contrib rules **must** have unique names and ids.
|
||||||
- You **can** add multiple rule classes to the same file, but classes **should** be logically grouped together in a single file that implements related rules.
|
- You **can** add multiple rule classes to the same file, but classes **should** be logically grouped together in a single file that implements related rules.
|
||||||
- Contrib rules **should** be meaningfully different from one another. If a behavior change or tweak can be added to an existing rule by adding options, that should be considered first. However, large [god classes](https://en.wikipedia.org/wiki/God_object) that implement multiple rules in a single class should obviously also be avoided.
|
- Contrib rules **should** be meaningfully different from one another. If a behavior change or tweak can be added to an existing rule by adding options, that should be considered first. However, large [god classes](https://en.wikipedia.org/wiki/God_object) that implement multiple rules in a single class should obviously also be avoided.
|
||||||
- Contrib rules **should** use [options](../user_defined_rules/#options) to make rules configurable.
|
- Contrib rules **should** use [options](user_defined_rules.md#options) to make rules configurable.
|
||||||
|
|
|
@ -1448,7 +1448,7 @@
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0.002767,
|
0.002767,
|
||||||
"\u001b[1;1H\u001b[93m 1 \r\n 2 \u001b[m\u001b[96m# Please enter the commit message for your changes. Lines starting\u001b[m\r\n\u001b[93m 3 \u001b[m\u001b[96m# with '#' will be ignored, and an empty message aborts the commit.\u001b[m\r\n\u001b[93m 4 \u001b[m\u001b[96m# On branch \u001b[m\u001b[38;5;224mmaster\u001b[m\r\n\u001b[93m 5 \u001b[m\u001b[96m# \u001b[m\u001b[38;5;81mChanges to be committed:\u001b[m\r\n\u001b[93m 6 \u001b[m\u001b[96m# \u001b[m\u001b[38;5;121mnew file\u001b[m\u001b[96m: \u001b[m\u001b[95m foo.txt\u001b[m\r\n\u001b[93m 7 \u001b[m\u001b[96m#\u001b[m\r\n\u001b[94m~ \u001b[9;1H~ \u001b[10;1H~ \u001b[11;1H~ \u001b[12;1H~ \u001b[13;1H~ "
|
"\u001b[1;1H\u001b[93m 1 \r\n 2 \u001b[m\u001b[96m# Please enter the commit message for your changes. Lines starting\u001b[m\r\n\u001b[93m 3 \u001b[m\u001b[96m# with '#' will be ignored, and an empty message aborts the commit.\u001b[m\r\n\u001b[93m 4 \u001b[m\u001b[96m# On branch \u001b[m\u001b[38;5;224mmain\u001b[m\r\n\u001b[93m 5 \u001b[m\u001b[96m# \u001b[m\u001b[38;5;81mChanges to be committed:\u001b[m\r\n\u001b[93m 6 \u001b[m\u001b[96m# \u001b[m\u001b[38;5;121mnew file\u001b[m\u001b[96m: \u001b[m\u001b[95m foo.txt\u001b[m\r\n\u001b[93m 7 \u001b[m\u001b[96m#\u001b[m\r\n\u001b[94m~ \u001b[9;1H~ \u001b[10;1H~ \u001b[11;1H~ \u001b[12;1H~ \u001b[13;1H~ "
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0.000062,
|
0.000062,
|
||||||
|
@ -2404,7 +2404,7 @@
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0.052844,
|
0.052844,
|
||||||
"1: T3 Title has trailing punctuation (!): \"WIP: This is an patchset that I need to continue working on!\"\r\n1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: This is an patchset that I need to continue working on!\"\r\n3: B6 Body message is missing\r\n"
|
"1: T3 Title has trailing punctuation (!): \"WIP: This is a patchset that I need to continue working on!\"\r\n1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: This is a patchset that I need to continue working on!\"\r\n3: B6 Body message is missing\r\n"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0.006075,
|
0.006075,
|
||||||
|
@ -2432,7 +2432,7 @@
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0.004763,
|
0.004763,
|
||||||
"[master 4b1f92d] WIP: This is an patchset that I need to continue working on!\r\n"
|
"[main 4b1f92d] WIP: This is a patchset that I need to continue working on!\r\n"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0.001504,
|
0.001504,
|
||||||
|
@ -3108,11 +3108,11 @@
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0.050694,
|
0.050694,
|
||||||
"1: T1 Title exceeds max length (60\u003e50): \"WIP: This is an patchset that I need to continue working on!\"\r\n"
|
"1: T1 Title exceeds max length (60\u003e50): \"WIP: This is a patchset that I need to continue working on!\"\r\n"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0.000006,
|
0.000006,
|
||||||
"1: T3 Title has trailing punctuation (!): \"WIP: This is an patchset that I need to continue working on!\"\r\n1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: This is an patchset that I need to continue working on!\"\r\n3: B6 Body message is missing\r\n"
|
"1: T3 Title has trailing punctuation (!): \"WIP: This is a patchset that I need to continue working on!\"\r\n1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: This is a patchset that I need to continue working on!\"\r\n3: B6 Body message is missing\r\n"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0.005418,
|
0.005418,
|
||||||
|
@ -3508,7 +3508,7 @@
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0.050989,
|
0.050989,
|
||||||
"1: T1 Title exceeds max length (60\u003e50): \"WIP: This is an patchset that I need to continue working on!\"\r\n1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: This is an patchset that I need to continue working on!\"\r\n"
|
"1: T1 Title exceeds max length (60\u003e50): \"WIP: This is a patchset that I need to continue working on!\"\r\n1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: This is a patchset that I need to continue working on!\"\r\n"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0.000025,
|
0.000025,
|
||||||
|
@ -3795,4 +3795,4 @@
|
||||||
"exit\r\n"
|
"exit\r\n"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -2,3 +2,11 @@ a.toctree-l3 {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
/* display: none; */
|
/* display: none; */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wy-nav-content {
|
||||||
|
max-width: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document hr {
|
||||||
|
border-top: 1px solid #666;
|
||||||
|
}
|
BIN
docs/images/dev-container.png
Normal file
BIN
docs/images/dev-container.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 207 KiB |
351
docs/images/gitlint-packages.drawio.svg
Normal file
351
docs/images/gitlint-packages.drawio.svg
Normal file
|
@ -0,0 +1,351 @@
|
||||||
|
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="491px" height="391px" viewBox="-0.5 -0.5 491 391" content="<mxfile><diagram id="x7jBp0SZ1TbX-vMHKkT6" name="Page-1">7Vtbb+o4EP41SGcfQEmccHks0O4e6axUbbW3p5VLDFg1MccxBc6vX9+SkNiUAEkvu62q4owvib/5ZjwzoR0wWe1+ZnC9/JXGiHQCL951wLQTBFHoi79SsNeCsB9owYLhWIv8QvCAfyAj9Ix0g2OUlgZySgnH67JwRpMEzXhJBhmj2/KwOSXlu67hAlmChxkktvRPHPOllg6DQSH/BeHFMruz3x/pnhXMBpudpEsY0+2BCNx2wIRRynVrtZsgIrHLcNHz7o705g/GUMLrTAB6wjMkG7O3TtAnYur4UTQWsrHAnOCEd2eUoaxTLJj3m43wfYbOdok5eljDmbzeCgKIQUu+IuLKF01I8CIRbaYBGj8jxrHA9sbIOZUTUjEfJ4vfDIogv5EcjnZHt+vnIAryIbpCnO3FEDMhZ5AhHsj0sC3UGAyNbHmgwiAyQmios8jXLtAVDQOwG+zQAfZl2BE05wVI39TVtN8QRiAqYzSwMfI9B0a58BqMoqOExAXhtGBOxYakkRPK1ND+9w3VA8B87nnycQqRnsvZJuUo7sZIuIqCzHqp8vJCjC/luJvTDSgHDN5SOYPT3gLtOIP/MPR9gxlKa/kLgQZ303wmUEHMgecKx7GcPhb3wD/go1rKE9drKlyV2mE07kRTudaG01SfIHLplDP6hCaGMwlN5CpzTEhF1IiuKs4mqqmroAFVDU+rCicph4R8KkthHozeTlkjS1kPdMOEbwm8iYidOsFEtr59FX/FdtRjK+x0B+Kzc/1TI8dEmd3hyHGUhpENWL8BwLJj3EHvGD9n5J0QPHuSvWA67Hk9kFsAq/p6cZAmmUwFiHqW3wt6vj2r1+sd2Mrh3APxwXOcYUTmYP+QJgSGFUZ4NiNGDgtqIrTy/fqECPpwJe1B60Zw4xxeVCd/0qO2wwhP02PQFj3sPOd+f/9VPf/sSWZ7ZzpQRjdJjOLm3KlfMR7g1zOeJo4fv386WDBZ4H8tAfRHvn1quRLAUdQAzsFHY6E/tOFpjYV2FHQps14zPRYBjc0gV6AYNsCg4Hjc8xnW1wnrA8/WVmthvW8nYaViWpQHBG9a25Aqkr9ezx/0vGPUeIWMIgRlXQHQi2xtjRw5BWhCW6G1dRQv0IO5pIwv6YImkNwWUhsBOeXl/Qceh2yBsujGDQlDBHL8XF7KtTsz9V6ng3koHpX9uJV0pSq/NLMKjG4Yg/uDYcaUz77PXc3xYbk8LRr6CQqF5ZjU848jS4cHGpKWcAdXmMi7/4FYDBNoxJPc0MDdnTIvMFap9l/GqamLv5WVRNnldHfYOd2bq8uIAAatMCF3bpmGwmEtJtgLDU4spDdjLXSJFl3JXEWt6RKuZRPOuFTcKfdUjpSOabwBJ1Y1iCB0+LDQFR4MGggP7ADzowIXOvKf1mA7/tJBZ+8H+GWHrkSiOzfO5EY9FHlGMkBScHiqX8c8stcP1ruDjq3ZgewKDYIeQVxEWl0TterOhLIVJLpfHsRdE5bJvjwyy/qw0FRiVvWy+6kezmCSzsVa2aomvvK2lMXlO+YTY5yuCTS7w4kIW8ycOaGQVxaqxiJWYLPGa7WMik1Fy84nrRjIKp5cFJg44sr6oW2F755n+E7gIyJjkbQtlH2UTEL+NBQQVXzJ0DYJZ+zaRDT0wguET5to1yaqb9rbMowj3K5vHa9jBX453w5dbzxBS1YAjpeV350VvCOm/54iBQ3DMxlBioQUyYebYVmN8L6saCrFmxSx9KdmSW2R1+H+r+HzMaO5hud5rc1B7H5bxHZFip/ErkdsQqn6LPO6+hroC5Qk34v0SnwwnD41TPV377+zNxn91+Q1sNC8vADQqZnIt560h0Ul7Ny0PbCXKi/UXNoO+hb2ZxXQ8npL1+t5Xl5l0UUXH5youqire8SweGJJeyWsp/CKFz+3chO2woHqm6qwah4NlfCqWUZ2n6OEqowHQ1DhzXUlPFDjW5JiBl6n6NzqRc3z/WzCXJnkVUuirqp34PCXTbyiAC8WTC+tgLZmdprznYOCadkU9TvEK2yxNm6ur0L+j1gqzL4tjorL4vvv2oEU/0QAbv8F</diagram></mxfile>">
|
||||||
|
<defs/>
|
||||||
|
<g>
|
||||||
|
<rect x="210" y="140" width="280" height="250" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe flex-end; width: 275px; height: 1px; padding-top: 147px; margin-left: 210px;">
|
||||||
|
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: right;">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
|
||||||
|
<b>
|
||||||
|
gitlint-core
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="485" y="159" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="end">
|
||||||
|
gitlint-core
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="235" y="190" width="100" height="100" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
|
||||||
|
<rect x="375" y="190" width="100" height="100" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 197px; margin-left: 376px;">
|
||||||
|
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
|
||||||
|
<i>
|
||||||
|
<font color="#ff0000">
|
||||||
|
trusted-deps
|
||||||
|
</font>
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="425" y="209" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
trusted-deps
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="370" y="170" width="100" height="20" fill="none" stroke="none" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 180px; margin-left: 420px;">
|
||||||
|
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: nowrap;">
|
||||||
|
<b>
|
||||||
|
extra_requires
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="420" y="184" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
extra_requires
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="229" y="170" width="100" height="20" fill="none" stroke="none" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 180px; margin-left: 279px;">
|
||||||
|
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: nowrap;">
|
||||||
|
<b>
|
||||||
|
install_requires
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="279" y="184" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
install_requires
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="230" y="310" width="245" height="60" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 243px; height: 1px; padding-top: 340px; margin-left: 231px;">
|
||||||
|
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
|
||||||
|
Source Code, CLI entry point, etc
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="353" y="344" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
Source Code, CLI entry point, etc
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="380" y="220" width="90" height="50" fill="none" stroke="none" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 1px; height: 1px; padding-top: 245px; margin-left: 382px;">
|
||||||
|
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: nowrap;">
|
||||||
|
<div>
|
||||||
|
Click==8.0.3
|
||||||
|
<br/>
|
||||||
|
<span>
|
||||||
|
arrow==1.2.1
|
||||||
|
<br/>
|
||||||
|
...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="382" y="249" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px">
|
||||||
|
Click==8.0.3...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="240" y="220" width="70" height="50" fill="none" stroke="none" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 1px; height: 1px; padding-top: 245px; margin-left: 242px;">
|
||||||
|
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: nowrap;">
|
||||||
|
<div>
|
||||||
|
Click>=8
|
||||||
|
<br/>
|
||||||
|
<span>
|
||||||
|
arrow>=1
|
||||||
|
<br/>
|
||||||
|
...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="242" y="249" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px">
|
||||||
|
Click>=8...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="180" y="130" width="90" height="20" rx="3" ry="3" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 140px; margin-left: 181px;">
|
||||||
|
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
|
||||||
|
PyPI package
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="225" y="144" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
PyPI package
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="210" y="11" width="280" height="95" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe flex-end; width: 275px; height: 1px; padding-top: 18px; margin-left: 210px;">
|
||||||
|
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: right;">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
|
||||||
|
<b>
|
||||||
|
gitlint
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="485" y="30" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="end">
|
||||||
|
gitlint
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="180" y="1" width="90" height="20" rx="3" ry="3" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 88px; height: 1px; padding-top: 11px; margin-left: 181px;">
|
||||||
|
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
|
||||||
|
PyPI package
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="225" y="15" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
PyPI package
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="235" y="46" width="200" height="45" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
|
||||||
|
<rect x="229" y="26" width="100" height="20" fill="none" stroke="none" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 36px; margin-left: 279px;">
|
||||||
|
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: nowrap;">
|
||||||
|
<b>
|
||||||
|
install_requires
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="279" y="40" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
install_requires
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="243" y="53.5" width="195" height="30" fill="none" stroke="none" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe flex-start; width: 193px; height: 1px; padding-top: 61px; margin-left: 245px;">
|
||||||
|
<div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: left;">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">
|
||||||
|
gitlint-core[
|
||||||
|
<i>
|
||||||
|
<font color="#ff0000">
|
||||||
|
trusted-deps
|
||||||
|
</font>
|
||||||
|
</i>
|
||||||
|
]==0.17.0
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="245" y="73" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px">
|
||||||
|
gitlint-core[trusted-deps]==0.17...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 350 80 L 350 230 Q 350 240 359.32 240 L 368.63 240" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||||
|
<path d="M 373.88 240 L 366.88 243.5 L 368.63 240 L 366.88 236.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<path d="M 100 68 L 193.63 68" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||||
|
<path d="M 198.88 68 L 191.88 71.5 L 193.63 68 L 191.88 64.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<path d="M 50 91.5 C 50 72.7 50 63.3 70 63.3 C 56.67 63.3 56.67 44.5 70 44.5 C 83.33 44.5 83.33 63.3 70 63.3 C 90 63.3 90 72.7 90 91.5 Z" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<path d="M 50 277 C 50 258.2 50 248.8 70 248.8 C 56.67 248.8 56.67 230 70 230 C 83.33 230 83.33 248.8 70 248.8 C 90 248.8 90 258.2 90 277 Z" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<rect x="20" y="100" width="100" height="30" fill="none" stroke="none" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 98px; height: 1px; padding-top: 115px; margin-left: 21px;">
|
||||||
|
<div data-drawio-colors="color: #000000; background-color: #FFFFFF; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: normal; overflow-wrap: normal;">
|
||||||
|
<span style="font-family: "helvetica" ; font-size: 12px ; font-weight: 400 ; letter-spacing: normal ; text-align: center ; text-indent: 0px ; text-transform: none ; word-spacing: 0px ; display: inline ; float: none">
|
||||||
|
<i>
|
||||||
|
pip install gitlint
|
||||||
|
</i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="70" y="119" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
pip install gitl...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="15" y="290" width="130" height="30" fill="none" stroke="none" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 128px; height: 1px; padding-top: 305px; margin-left: 16px;">
|
||||||
|
<div data-drawio-colors="color: #000000; background-color: #FFFFFF; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: normal; overflow-wrap: normal;">
|
||||||
|
<span style="font-family: "helvetica" ; font-size: 12px ; font-weight: 400 ; letter-spacing: normal ; text-align: center ; text-indent: 0px ; text-transform: none ; word-spacing: 0px ; display: inline ; float: none">
|
||||||
|
<i>
|
||||||
|
pip install gitlint-core
|
||||||
|
</i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="80" y="309" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
pip install gitlint-c...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="0" y="0" width="160" height="30" fill="none" stroke="none" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 15px; margin-left: 1px;">
|
||||||
|
<div data-drawio-colors="color: #000000; background-color: #FFFFFF; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: normal; overflow-wrap: normal;">
|
||||||
|
<span style="font-family: "helvetica" ; font-size: 12px ; font-weight: 400 ; letter-spacing: normal ; text-indent: 0px ; text-transform: none ; word-spacing: 0px ; display: inline ; float: none">
|
||||||
|
Use strict dependencies (most users)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="80" y="19" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
Use strict dependencies (m...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<rect x="0" y="180" width="160" height="30" fill="none" stroke="none" pointer-events="all"/>
|
||||||
|
<g transform="translate(-0.5 -0.5)">
|
||||||
|
<switch>
|
||||||
|
<foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 158px; height: 1px; padding-top: 195px; margin-left: 1px;">
|
||||||
|
<div data-drawio-colors="color: #000000; background-color: #FFFFFF; " style="box-sizing: border-box; font-size: 0px; text-align: center;">
|
||||||
|
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; background-color: rgb(255, 255, 255); white-space: normal; overflow-wrap: normal;">
|
||||||
|
<span style="font-family: "helvetica" ; font-size: 12px ; font-weight: 400 ; letter-spacing: normal ; text-indent: 0px ; text-transform: none ; word-spacing: 0px ; display: inline ; float: none">
|
||||||
|
Use loose dependencies
|
||||||
|
<br/>
|
||||||
|
(at your risk)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
<text x="80" y="199" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
|
||||||
|
Use loose dependencies...
|
||||||
|
</text>
|
||||||
|
</switch>
|
||||||
|
</g>
|
||||||
|
<path d="M 100 253.5 L 193.63 253.03" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||||
|
<path d="M 198.88 253.01 L 191.9 256.54 L 193.63 253.03 L 191.86 249.54 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<path d="M 210 250 L 215 250 Q 220 250 220 240 L 220 213 Q 220 203 224.07 203 L 228.13 203" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||||
|
<path d="M 233.38 203 L 226.38 206.5 L 228.13 203 L 226.38 199.5 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<ellipse cx="210" cy="253.5" rx="10" ry="10" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
|
||||||
|
<path d="M 220 68 L 228.64 68.29" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="stroke"/>
|
||||||
|
<path d="M 233.88 68.46 L 226.77 71.73 L 228.64 68.29 L 227 64.73 Z" fill="rgb(0, 0, 0)" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/>
|
||||||
|
<ellipse cx="210" cy="68" rx="10" ry="10" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/>
|
||||||
|
</g>
|
||||||
|
<switch>
|
||||||
|
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
|
||||||
|
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
|
||||||
|
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
|
||||||
|
Viewer does not support full SVG 1.1
|
||||||
|
</text>
|
||||||
|
</a>
|
||||||
|
</switch>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/images/gitlint-packages.png
Normal file
BIN
docs/images/gitlint-packages.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
118
docs/index.md
118
docs/index.md
|
@ -22,11 +22,11 @@ Great for use as a [commit-msg git hook](#using-gitlint-as-a-commit-msg-hook) or
|
||||||
- **Easily integrated**: Gitlint is designed to work [with your own scripts or CI system](#using-gitlint-in-a-ci-environment).
|
- **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
|
- **Sane defaults:** Many of gitlint's validations are based on
|
||||||
[well-known](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),
|
[well-known](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),
|
||||||
[community](http://addamhardy.com/2013/06/05/good-commit-messages-and-enforcing-them-with-git-hooks.html),
|
[community](https://addamhardy.com/2013-06-05-good-commit-messages-and-enforcing-them-with-git-hooks),
|
||||||
[standards](http://chris.beams.io/posts/git-commit/), others are based on checks that we've found
|
[standards](http://chris.beams.io/posts/git-commit/), others are based on checks that we've found
|
||||||
useful throughout the years.
|
useful throughout the years.
|
||||||
- **Easily configurable:** Gitlint has sane defaults, but [you can also easily customize it to your own liking](configuration.md).
|
- **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).
|
- **Community contributed rules**: Conventions that are common but not universal [can be selectively enabled](contrib_rules.md).
|
||||||
- **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).
|
- **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).
|
||||||
- **Full unicode support:** Lint your Russian, Chinese or Emoji commit messages with ease!
|
- **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,
|
- **Production-ready:** Gitlint checks a lot of the boxes you're looking for: actively maintained, high unit test coverage, integration tests,
|
||||||
|
@ -38,6 +38,10 @@ useful throughout the years.
|
||||||
# Pip is recommended to install the latest version
|
# Pip is recommended to install the latest version
|
||||||
pip install gitlint
|
pip install gitlint
|
||||||
|
|
||||||
|
# Alternative: by default, gitlint is installed with pinned dependencies.
|
||||||
|
# To install gitlint with looser dependency requirements, only install gitlint-core.
|
||||||
|
pip install gitlint-core
|
||||||
|
|
||||||
# Community maintained packages:
|
# Community maintained packages:
|
||||||
brew install gitlint # Homebrew (macOS)
|
brew install gitlint # Homebrew (macOS)
|
||||||
sudo port install gitlint # Macports (macOS)
|
sudo port install gitlint # Macports (macOS)
|
||||||
|
@ -81,6 +85,19 @@ $ cat examples/commit-message-2 | gitlint
|
||||||
!!! note
|
!!! note
|
||||||
The returned exit code equals the number of errors found. [Some exit codes are special](index.md#exit-codes).
|
The returned exit code equals the number of errors found. [Some exit codes are special](index.md#exit-codes).
|
||||||
|
|
||||||
|
### Shell completion
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Bash: add to ~/.bashrc
|
||||||
|
eval "$(_GITLINT_COMPLETE=bash_source gitlint)"
|
||||||
|
|
||||||
|
# Zsh: add to ~/.zshrc
|
||||||
|
eval "$(_GITLINT_COMPLETE=zsh_source gitlint)"
|
||||||
|
|
||||||
|
# Fish: add to ~/.config/fish/completions/foo-bar.fish
|
||||||
|
eval (env _GITLINT_COMPLETE=fish_source gitlint)
|
||||||
|
```
|
||||||
|
|
||||||
## 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.
|
For in-depth documentation of general and rule-specific configuration options, have a look at the [Configuration](configuration.md) and [Rules](rules.md) pages.
|
||||||
|
@ -93,7 +110,7 @@ Short example `.gitlint` file ([full reference](configuration.md)):
|
||||||
# their id or by their full name
|
# their id or by their full name
|
||||||
ignore=body-is-missing,T3
|
ignore=body-is-missing,T3
|
||||||
|
|
||||||
# Ignore any data send to gitlint via stdin
|
# Ignore any data sent to gitlint via stdin
|
||||||
ignore-stdin=true
|
ignore-stdin=true
|
||||||
|
|
||||||
# Configure title-max-length rule, set title length to 80 (72 = default)
|
# Configure title-max-length rule, set title length to 80 (72 = default)
|
||||||
|
@ -136,7 +153,8 @@ Options:
|
||||||
(e.g.: -c T1.line-length=80). Flag can be
|
(e.g.: -c T1.line-length=80). Flag can be
|
||||||
used multiple times to set multiple config values.
|
used multiple times to set multiple config values.
|
||||||
--commit TEXT Hash (SHA) of specific commit to lint.
|
--commit TEXT Hash (SHA) of specific commit to lint.
|
||||||
--commits TEXT The range of commits to lint. [default: HEAD]
|
--commits TEXT The range of commits (refspec or comma-separated
|
||||||
|
hashes) to lint. [default: HEAD]
|
||||||
-e, --extra-path PATH Path to a directory or python module with extra
|
-e, --extra-path PATH Path to a directory or python module with extra
|
||||||
user-defined rules
|
user-defined rules
|
||||||
--ignore TEXT Ignore rules (comma-separated by id or name).
|
--ignore TEXT Ignore rules (comma-separated by id or name).
|
||||||
|
@ -145,8 +163,9 @@ Options:
|
||||||
--msg-filename FILENAME Path to a file containing a commit-msg.
|
--msg-filename FILENAME Path to a file containing a commit-msg.
|
||||||
--ignore-stdin Ignore any stdin data. Useful for running in CI
|
--ignore-stdin Ignore any stdin data. Useful for running in CI
|
||||||
server.
|
server.
|
||||||
--staged Read staged commit meta-info from the local
|
--staged Attempt smart guesses about meta info (like
|
||||||
repository.
|
author name, email, branch, changed files, etc)
|
||||||
|
for staged commits.
|
||||||
--fail-without-commits Hard fail when the target commit range is empty.
|
--fail-without-commits Hard fail when the target commit range is empty.
|
||||||
-v, --verbose Verbosity, more v's for more verbose output
|
-v, --verbose Verbosity, more v's for more verbose output
|
||||||
(e.g.: -v, -vv, -vvv). [default: -vvv]
|
(e.g.: -v, -vv, -vvv). [default: -vvv]
|
||||||
|
@ -218,7 +237,7 @@ In case you want to change gitlint's behavior, you should either use a `.gitlint
|
||||||
your `.pre-commit-config.yaml` file like so:
|
your `.pre-commit-config.yaml` file like so:
|
||||||
```yaml
|
```yaml
|
||||||
- repo: https://github.com/jorisroovers/gitlint
|
- repo: https://github.com/jorisroovers/gitlint
|
||||||
rev: # Fill in a tag / sha here
|
rev: # Fill in a tag / sha here (e.g. v0.18.0)
|
||||||
hooks:
|
hooks:
|
||||||
- id: gitlint
|
- id: gitlint
|
||||||
args: [--contrib=CT1, --msg-filename]
|
args: [--contrib=CT1, --msg-filename]
|
||||||
|
@ -229,6 +248,36 @@ your `.pre-commit-config.yaml` file like so:
|
||||||
You need to add `--msg-filename` at the end of your custom `args` list as the gitlint-hook will fail otherwise.
|
You need to add `--msg-filename` at the end of your custom `args` list as the gitlint-hook will fail otherwise.
|
||||||
|
|
||||||
|
|
||||||
|
### gitlint and pre-commit in CI
|
||||||
|
gitlint also supports a `gitlint-ci` pre-commit hook that can be used in CI environments.
|
||||||
|
|
||||||
|
Configure it like so:
|
||||||
|
```yaml
|
||||||
|
- repo: https://github.com/jorisroovers/gitlint
|
||||||
|
rev: # insert ref, e.g. v0.18.0
|
||||||
|
hooks:
|
||||||
|
- id: gitlint # this is the regular commit-msg hook
|
||||||
|
- id: gitlint-ci # hook for CI environments
|
||||||
|
```
|
||||||
|
|
||||||
|
And invoke it in your CI environment like this:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pre-commit run --hook-stage manual gitlint-ci
|
||||||
|
```
|
||||||
|
|
||||||
|
By default this will only lint the latest commit.
|
||||||
|
If you want to lint more commits you can modify the `gitlint-ci` hook like so:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- repo: https://github.com/jorisroovers/gitlint
|
||||||
|
rev: v0.17.0
|
||||||
|
hooks:
|
||||||
|
- id: gitlint
|
||||||
|
- id: gitlint-ci
|
||||||
|
args: [--debug, --commits, mybranch] # enable debug mode, lint all commits in mybranch
|
||||||
|
```
|
||||||
|
|
||||||
## Using gitlint in a CI environment
|
## Using gitlint in a CI environment
|
||||||
By default, when just running `gitlint` without additional parameters, gitlint lints the last commit in the current
|
By default, when just running `gitlint` without additional parameters, gitlint lints the last commit in the current
|
||||||
working directory.
|
working directory.
|
||||||
|
@ -248,33 +297,48 @@ git log -1 --pretty=%B 62c0519 | gitlint
|
||||||
Note that gitlint requires that you specify `--pretty=%B` (=only print the log message, not the metadata),
|
Note that gitlint requires that you specify `--pretty=%B` (=only print the log message, not the metadata),
|
||||||
future versions of gitlint might fix this and not require the `--pretty` argument.
|
future versions of gitlint might fix this and not require the `--pretty` argument.
|
||||||
|
|
||||||
## Linting specific commits
|
## Linting specific commits or branches
|
||||||
|
|
||||||
Gitlint allows users to lint a specific commit:
|
Gitlint can lint specific commits using `--commit`:
|
||||||
```sh
|
```sh
|
||||||
gitlint --commit 019cf40580a471a3958d3c346aa8bfd265fe5e16
|
gitlint --commit 019cf40580a471a3958d3c346aa8bfd265fe5e16
|
||||||
gitlint --commit 019cf40 # short SHAs work too
|
gitlint --commit 019cf40 # short SHAs work too
|
||||||
|
gitlint --commit HEAD~2 # as do special references
|
||||||
|
gitlint --commit mybranch # lint latest commit on a branch
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also lint multiple commits at once like so:
|
You can also lint multiple commits using `--commits` (plural):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Lint a specific commit range:
|
# Lint a specific commit range:
|
||||||
gitlint --commits "019cf40...d6bc75a"
|
gitlint --commits "019cf40...d6bc75a"
|
||||||
# You can also use git's special references:
|
# Lint all commits on a branch
|
||||||
|
gitlint --commits mybranch
|
||||||
|
# Lint all commits that are different between a branch and your main branch
|
||||||
|
gitlint --commits "main..mybranch"
|
||||||
|
# Use git's special references
|
||||||
gitlint --commits "origin..HEAD"
|
gitlint --commits "origin..HEAD"
|
||||||
|
|
||||||
|
# You can also pass multiple, comma separated commit hashes:
|
||||||
|
gitlint --commits 019cf40,c50eb150,d6bc75a
|
||||||
|
# These can include special references as well
|
||||||
|
gitlint --commits HEAD~1,mybranch-name,origin/main,d6bc75a
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
by [git rev-list](https://git-scm.com/docs/git-rev-list) as a single argument will work.
|
||||||
|
|
||||||
|
Alternatively, you can pass `--commits` a comma-separated list of commit hashes (both short and full-length SHAs work,
|
||||||
|
as well as special references such as `HEAD` and branch names).
|
||||||
|
Gitlint will treat these as pointers to **single** commits and lint these in the order you passed.
|
||||||
|
|
||||||
For cases where the `--commits` option doesn't provide the flexibility you need, you can always use a simple shell
|
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.
|
script to lint an arbitrary set of commits, like shown in the example below.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
for commit in $(git rev-list master); do
|
for commit in $(git rev-list my-branch); do
|
||||||
echo "Commit $commit"
|
echo "Commit $commit"
|
||||||
gitlint --commit $commit
|
gitlint --commit $commit
|
||||||
echo "--------"
|
echo "--------"
|
||||||
|
@ -283,14 +347,14 @@ done
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
One downside to this approach is that you invoke gitlint once per commit vs. once per set of commits.
|
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
|
This means you'll incur the gitlint startup time once per commit, making it 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, squash and revert commits
|
## Merge, fixup, squash and revert commits
|
||||||
_Introduced in gitlint v0.7.0 (merge), v0.9.0 (fixup, squash) and v0.13.0 (revert)_
|
_Introduced in gitlint v0.7.0 (merge), v0.9.0 (fixup, squash), v0.13.0 (revert) and v0.18.0 (fixup=amend)_
|
||||||
|
|
||||||
**Gitlint ignores merge, revert, fixup and squash commits by default.**
|
**Gitlint ignores merge, revert, fixup, and squash commits by default.**
|
||||||
|
|
||||||
For merge and revert commits, the rationale for ignoring them is
|
For merge and revert commits, the rationale for ignoring them is
|
||||||
that most users keep git's default messages for these commits (i.e *Merge/Revert "[original commit message]"*).
|
that most users keep git's default messages for these commits (i.e *Merge/Revert "[original commit message]"*).
|
||||||
|
@ -300,14 +364,14 @@ For example, a common case is that *"Merge:"* being auto-prepended triggers a
|
||||||
[title-max-length](rules.md#t1-title-max-length) violation. Most users don't want this, so we disable linting
|
[title-max-length](rules.md#t1-title-max-length) violation. Most users don't want this, so we disable linting
|
||||||
on Merge and Revert commits by default.
|
on Merge and Revert commits by default.
|
||||||
|
|
||||||
For [squash](https://git-scm.com/docs/git-commit#git-commit---squashltcommitgt) and [fixup](https://git-scm.com/docs/git-commit#git-commit---fixupltcommitgt) commits, the rationale is that these are temporary
|
For [squash](https://git-scm.com/docs/git-commit#git-commit---squashltcommitgt) and [fixup](https://git-scm.com/docs/git-commit#git-commit---fixupltcommitgt) (including [fixup=amend](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---fixupamendrewordltcommitgt)) commits, the rationale is that these are temporary
|
||||||
commits that will be squashed into a different commit, and hence the commit messages for these commits are very
|
commits that will be squashed into a different commit, and hence the commit messages for these commits are very
|
||||||
short-lived and not intended to make it into the final commit history. In addition, by prepending *"fixup!"* or
|
short-lived and not intended to make it into the final commit history. In addition, by prepending *"fixup!"*,
|
||||||
*"squash!"* to your commit message, certain gitlint rules might be violated
|
*"amend!"* or *"squash!"* to your commit message, certain gitlint rules might be violated
|
||||||
(e.g. [title-max-length](rules.md#t1-title-max-length)) which is often undesirable.
|
(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
|
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
|
general `ignore-merge-commits`, `ignore-revert-commits`, `ignore-fixup-commits`, `ignore-fixup-amend-commits` or
|
||||||
`ignore-squash-commits` option to `false`
|
`ignore-squash-commits` option to `false`
|
||||||
[using one of the various ways to configure gitlint](configuration.md).
|
[using one of the various ways to configure gitlint](configuration.md).
|
||||||
|
|
||||||
|
@ -374,7 +438,7 @@ additional unique identifier (i.e. the rule *name*) during configuration.
|
||||||
For example, by defining 2 `body-max-line-length` rules with different `line-length` options, you obviously create
|
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
|
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!
|
any configuration is non-conflicting. So caution advised!
|
||||||
|
|
||||||
Defining a named rule is easy, for example using your `.gitlint` file:
|
Defining a named rule is easy, for example using your `.gitlint` file:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
|
@ -400,7 +464,7 @@ When executing gitlint, you will see the violations from the default `title-must
|
||||||
the violations caused by the additional Named Rules.
|
the violations caused by the additional Named Rules.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ gitlint
|
$ gitlint
|
||||||
1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: foo wonderwoman hur bar"
|
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: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 'foo' (case-insensitive): "WIP: foo wonderwoman hur bar"
|
||||||
|
@ -431,8 +495,8 @@ of violations counted by the exit code is 252. Note that gitlint does not have a
|
||||||
it can detect, it will just always return with exit code 252 when the number of violations is greater than or equal
|
it can detect, it will just always return with exit code 252 when the number of violations is greater than or equal
|
||||||
to 252.
|
to 252.
|
||||||
|
|
||||||
Exit Code | Description
|
| Exit Code | Description |
|
||||||
-----------|------------------------------------------------------------
|
| --------- | ------------------------------------------ |
|
||||||
253 | Wrong invocation of the `gitlint` command.
|
| 253 | Wrong invocation of the `gitlint` command. |
|
||||||
254 | Something went wrong when invoking git.
|
| 254 | Something went wrong when invoking git. |
|
||||||
255 | Invalid gitlint configuration
|
| 255 | Invalid gitlint configuration |
|
||||||
|
|
287
docs/rules.md
287
docs/rules.md
|
@ -8,43 +8,43 @@ In addition, you can also [write your own user-defined rule](user_defined_rules.
|
||||||
what you're looking for.
|
what you're looking for.
|
||||||
|
|
||||||
|
|
||||||
ID | Name | gitlint version | Description
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-------------------|-------------------------------------------
|
| --- | --------------------------- | --------------- | ------------------------------------------------------------------------------------------- |
|
||||||
T1 | title-max-length | >= 0.1.0 | Title length must be < 72 chars.
|
| T1 | title-max-length | >= 0.1.0 | Title length must be <= 72 chars. |
|
||||||
T2 | title-trailing-whitespace | >= 0.1.0 | Title cannot have trailing whitespace (space or tab)
|
| T2 | title-trailing-whitespace | >= 0.1.0 | Title cannot have trailing whitespace (space or tab) |
|
||||||
T3 | title-trailing-punctuation | >= 0.1.0 | Title cannot have trailing punctuation (?:!.,;)
|
| T3 | title-trailing-punctuation | >= 0.1.0 | Title cannot have trailing punctuation (?:!.,;) |
|
||||||
T4 | title-hard-tab | >= 0.1.0 | Title cannot contain hard tab characters (\t)
|
| 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")
|
| 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)
|
| 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: None)
|
| T7 | title-match-regex | >= 0.5.0 | Title must match a given regex (default: None) |
|
||||||
T8 | title-min-length | >= 0.14.0 | Title length must be > 5 chars.
|
| T8 | title-min-length | >= 0.14.0 | Title length must be >= 5 chars. |
|
||||||
B1 | body-max-line-length | >= 0.1.0 | Lines in the body must be < 80 chars
|
| B1 | body-max-line-length | >= 0.1.0 | Lines in the body must be <= 80 chars |
|
||||||
B2 | body-trailing-whitespace | >= 0.1.0 | Body cannot have trailing whitespace (space or tab)
|
| 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)
|
| B3 | body-hard-tab | >= 0.1.0 | Body cannot contain hard tab characters (\t) |
|
||||||
B4 | body-first-line-empty | >= 0.1.0 | First line of the body (second line of commit message) must be empty
|
| B4 | body-first-line-empty | >= 0.1.0 | First line of the body (second line of commit message) must be empty |
|
||||||
B5 | body-min-length | >= 0.4.0 | Body length must be at least 20 characters
|
| 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
|
| 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
|
| 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)
|
| B8 | body-match-regex | >= 0.14.0 | Body must match a given regex (default: None) |
|
||||||
M1 | author-valid-email | >= 0.9.0 | Author email address must be a valid email address
|
| 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
|
| I1 | ignore-by-title | >= 0.10.0 | Ignore a commit based on matching its title |
|
||||||
I2 | ignore-by-body | >= 0.10.0 | Ignore a commit based on matching its body
|
| I2 | ignore-by-body | >= 0.10.0 | Ignore a commit based on matching its body |
|
||||||
I3 | ignore-body-lines | >= 0.14.0 | Ignore certain lines in a commit body that match a regex
|
| I3 | ignore-body-lines | >= 0.14.0 | Ignore certain lines in a commit body that match a regex |
|
||||||
I4 | ignore-by-author-name | >= 0.16.0 | Ignore a commit based on matching its author name
|
| I4 | ignore-by-author-name | >= 0.16.0 | Ignore a commit based on matching its author name |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## T1: title-max-length
|
## T1: title-max-length
|
||||||
|
|
||||||
ID | Name | gitlint version | Description
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | ---------------- | --------------- | ------------------------------------ |
|
||||||
T1 | title-max-length | >= 0.1 | Title length must be < 72 chars.
|
| T1 | title-max-length | >= 0.1 | Title length must be <= 72 chars. |
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Name | gitlint version | Default | Description
|
| Name | gitlint version | Default | Description |
|
||||||
---------------|-----------------|---------|----------------------------------
|
| ----------- | --------------- | ------- | ---------------------------- |
|
||||||
line-length | >= 0.2 | 72 | Maximum allowed title length
|
| line-length | >= 0.2 | 72 | Maximum allowed title length |
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
|
@ -59,39 +59,43 @@ line-length=72
|
||||||
[title-max-length]
|
[title-max-length]
|
||||||
line-length=120
|
line-length=120
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
## T2: title-trailing-whitespace
|
## T2: title-trailing-whitespace
|
||||||
|
|
||||||
ID | Name | gitlint version | Description
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | ------------------------- | --------------- | ---------------------------------------------------- |
|
||||||
T2 | title-trailing-whitespace | >= 0.1 | Title cannot have trailing whitespace (space or tab)
|
| 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
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | -------------------------- | --------------- | ----------------------------------------------- |
|
||||||
T3 | title-trailing-punctuation | >= 0.1 | Title cannot have trailing punctuation (?:!.,;)
|
| T3 | title-trailing-punctuation | >= 0.1 | Title cannot have trailing punctuation (?:!.,;) |
|
||||||
|
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
## T4: title-hard-tab
|
## T4: title-hard-tab
|
||||||
|
|
||||||
ID | Name | gitlint version | Description
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | -------------- | --------------- | --------------------------------------------- |
|
||||||
T4 | title-hard-tab | >= 0.1 | Title cannot contain hard tab characters (\t)
|
| 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
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | --------------------------- | --------------- | --------------------------------------------------- |
|
||||||
T5 | title-must-not-contain-word | >= 0.1 | Title cannot contain certain words (default: "WIP")
|
| T5 | title-must-not-contain-word | >= 0.1 | Title cannot contain certain words (default: "WIP") |
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Name | gitlint version | Default | Description
|
| 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
|
| words | >= 0.3 | WIP | Comma-separated list of words that should not be used in the title. Matching is case insensitive |
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
|
@ -102,25 +106,28 @@ words | >= 0.3 | WIP | Comma-separated list of words that
|
||||||
[title-must-not-contain-word]
|
[title-must-not-contain-word]
|
||||||
words=crap,darn,damn
|
words=crap,darn,damn
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
## T6: title-leading-whitespace
|
## T6: title-leading-whitespace
|
||||||
|
|
||||||
ID | Name | gitlint version | Description
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | ------------------------ | --------------- | --------------------------------------------------- |
|
||||||
T6 | title-leading-whitespace | >= 0.4 | Title cannot have leading whitespace (space or tab)
|
| 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
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | ----------------- | --------------- | -------------------------------------------- |
|
||||||
T7 | title-match-regex | >= 0.5 | Title must match a given regex (default: .*)
|
| T7 | title-match-regex | >= 0.5 | Title must match a given regex (default: .*) |
|
||||||
|
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Name | gitlint version | Default | Description
|
| Name | gitlint version | Default | Description |
|
||||||
---------------|-----------------|---------|----------------------------------
|
| ----- | --------------- | ------- | ------------------------------------------------------------------------------------ |
|
||||||
regex | >= 0.5 | .* | [Python regex](https://docs.python.org/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
|
### Examples
|
||||||
|
|
||||||
|
@ -131,19 +138,20 @@ regex | >= 0.5 | .* | [Python regex](https://docs.python.
|
||||||
[title-match-regex]
|
[title-match-regex]
|
||||||
regex=^US[1-9][0-9]*
|
regex=^US[1-9][0-9]*
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
## T8: title-min-length ##
|
## T8: title-min-length ##
|
||||||
|
|
||||||
ID | Name | gitlint version | Description
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | ---------------- | --------------- | ----------------------------------- |
|
||||||
T1 | title-min-length | >= | Title length must be > 5 chars.
|
| T8 | title-min-length | >= 0.14.0 | Title length must be >= 5 chars. |
|
||||||
|
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Name | gitlint version | Default | Description
|
| Name | gitlint version | Default | Description |
|
||||||
---------------|-----------------|---------|----------------------------------
|
| ---------- | --------------- | ------- | ----------------------------- |
|
||||||
min-length | >= 0.14.0 | 5 | Minimum required title length
|
| min-length | >= 0.14.0 | 5 | Minimum required title length |
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
|
@ -154,18 +162,19 @@ min-length | >= 0.14.0 | 5 | Minimum required title length
|
||||||
[title-min-length]
|
[title-min-length]
|
||||||
min-length=3
|
min-length=3
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
## B1: body-max-line-length
|
## B1: body-max-line-length
|
||||||
|
|
||||||
ID | Name | gitlint version | Description
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | -------------------- | --------------- | ---------------------------------------- |
|
||||||
B1 | body-max-line-length | >= 0.1 | Lines in the body must be < 80 chars
|
| B1 | body-max-line-length | >= 0.1 | Lines in the body must be <= 80 chars |
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Name | gitlint version | Default | Description
|
| Name | gitlint version | Default | Description |
|
||||||
---------------|-----------------|---------|----------------------------------
|
| ----------- | --------------- | ------- | ------------------------------------------------------ |
|
||||||
line-length | >= 0.2 | 80 | Maximum allowed line length in the commit message body
|
| line-length | >= 0.2 | 80 | Maximum allowed line length in the commit message body |
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
|
@ -180,38 +189,43 @@ line-length=120
|
||||||
[body-max-line-length]
|
[body-max-line-length]
|
||||||
line-length=72
|
line-length=72
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
## B2: body-trailing-whitespace
|
## B2: body-trailing-whitespace
|
||||||
|
|
||||||
ID | Name | gitlint version | Description
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | ------------------------ | --------------- | --------------------------------------------------- |
|
||||||
B2 | body-trailing-whitespace | >= 0.1 | Body cannot have trailing whitespace (space or tab)
|
| 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
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | ------------- | --------------- | -------------------------------------------- |
|
||||||
B3 | body-hard-tab | >= 0.1 | Body cannot contain hard tab characters (\t)
|
| 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
|
| 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
|
| 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
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | --------------- | --------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||||
B5 | body-min-length | >= 0.4 | Body length must be at least 20 characters. In versions >= 0.8.0, gitlint will not count newline characters.
|
| B5 | body-min-length | >= 0.4 | Body length must be at least 20 characters. In versions >= 0.8.0, gitlint will not count newline characters. |
|
||||||
|
|
||||||
### Options ###
|
### Options ###
|
||||||
|
|
||||||
Name | gitlint version | Default | Description
|
| Name | gitlint version | Default | Description |
|
||||||
---------------|-----------------|---------|----------------------------------
|
| ---------- | --------------- | ------- | --------------------------------------------- |
|
||||||
min-length | >= 0.4 | 20 | Minimum number of required characters in body
|
| min-length | >= 0.4 | 20 | Minimum number of required characters in body |
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
|
@ -226,31 +240,34 @@ min-length=5
|
||||||
[body-min-length]
|
[body-min-length]
|
||||||
min-length=100
|
min-length=100
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
## B6: body-is-missing
|
## B6: body-is-missing
|
||||||
|
|
||||||
ID | Name | gitlint version | Description
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | --------------- | --------------- | ------------------------------ |
|
||||||
B6 | body-is-missing | >= 0.4 | Body message must be specified
|
| B6 | body-is-missing | >= 0.4 | Body message must be specified |
|
||||||
|
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Name | gitlint version | Default | Description
|
| Name | gitlint version | Default | Description |
|
||||||
----------------------|-----------------|-----------|----------------------------------
|
| -------------------- | --------------- | ------- | ------------------------------------------------------------------------------------- |
|
||||||
ignore-merge-commits | >= 0.4 | true | Whether this rule should be ignored during merge commits. Allowed values: true,false.
|
| 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
|
| 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
|
| 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
|
| 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.
|
| 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
|
### Examples
|
||||||
|
|
||||||
|
@ -262,18 +279,19 @@ files | >= 0.4 | (empty) | Comma-separated list o
|
||||||
[body-changed-file-mention]
|
[body-changed-file-mention]
|
||||||
files=generated.xml,secrets.txt,private-key.pem
|
files=generated.xml,secrets.txt,private-key.pem
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
## B8: body-match-regex
|
## B8: body-match-regex
|
||||||
|
|
||||||
ID | Name | gitlint version | Description
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | ---------------- | --------------- | ----------------------------- |
|
||||||
B8 | body-match-regex | >= 0.14 | Body must match a given regex
|
| B8 | body-match-regex | >= 0.14 | Body must match a given regex |
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Name | gitlint version | Default | Description
|
| Name | gitlint version | Default | Description |
|
||||||
----------------------|-----------------|--------------|----------------------------------
|
| ----- | --------------- | ------- | ----------------------------------------------------------------------------------- |
|
||||||
regex | >= 0.14 | None | [Python regex](https://docs.python.org/library/re.html) that the title should match.
|
| regex | >= 0.14 | None | [Python regex](https://docs.python.org/library/re.html) that the body should match. |
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
|
@ -288,12 +306,13 @@ regex=Reviewed-By:(.*)$
|
||||||
[body-match-regex]
|
[body-match-regex]
|
||||||
regex=(*.)Foo(.*)
|
regex=(*.)Foo(.*)
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
## M1: author-valid-email
|
## M1: author-valid-email
|
||||||
|
|
||||||
ID | Name | gitlint version | Description
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | ------------------ | --------------- | -------------------------------------------------- |
|
||||||
M1 | author-valid-email | >= 0.8.3 | Author email address must be a valid email address
|
| M1 | author-valid-email | >= 0.8.3 | Author email address must be a valid email address |
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
Email addresses are [notoriously hard to validate and the official email valid spec is often too loose for any real world application](http://stackoverflow.com/a/201378/381010).
|
Email addresses are [notoriously hard to validate and the official email valid spec is often too loose for any real world application](http://stackoverflow.com/a/201378/381010).
|
||||||
|
@ -303,9 +322,9 @@ M1 | author-valid-email | >= 0.8.3 | Author email address mus
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Name | gitlint version | Default | Description
|
| Name | gitlint version | Default | Description |
|
||||||
----------------------|-------------------|------------------------------|----------------------------------
|
| ----- | --------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||||
regex | >= 0.9.0 | `[^@ ]+@[^@ ]+\.[^@ ]+` | [Python regex](https://docs.python.org/library/re.html) 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 |
|
||||||
|
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
@ -317,20 +336,21 @@ regex | >= 0.9.0 | `[^@ ]+@[^@ ]+\.[^@ ]+` | [Python
|
||||||
[author-valid-email]
|
[author-valid-email]
|
||||||
regex=[^@]+@foo.com
|
regex=[^@]+@foo.com
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
## I1: ignore-by-title
|
## I1: ignore-by-title
|
||||||
|
|
||||||
ID | Name | gitlint version | Description
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | --------------- | --------------- | -------------------------------------------- |
|
||||||
I1 | ignore-by-title | >= 0.10.0 | Ignore a commit based on matching its title.
|
| I1 | ignore-by-title | >= 0.10.0 | Ignore a commit based on matching its title. |
|
||||||
|
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Name | gitlint version | Default | Description
|
| Name | gitlint version | Default | Description |
|
||||||
----------------------|-------------------|------------------------------|----------------------------------
|
| ------ | --------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||||
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.
|
| 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.
|
| ignore | >= 0.10.0 | all | Comma-separated list of rule names or ids to ignore when this rule is matched. |
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
|
@ -345,20 +365,21 @@ ignore=title-max-length,body-min-length
|
||||||
# ignore all rules by setting ignore to 'all'
|
# ignore all rules by setting ignore to 'all'
|
||||||
# ignore=all
|
# ignore=all
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
## I2: ignore-by-body
|
## I2: ignore-by-body
|
||||||
|
|
||||||
ID | Name | gitlint version | Description
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | -------------- | --------------- | ------------------------------------------- |
|
||||||
I2 | ignore-by-body | >= 0.10.0 | Ignore a commit based on matching its body.
|
| I2 | ignore-by-body | >= 0.10.0 | Ignore a commit based on matching its body. |
|
||||||
|
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Name | gitlint version | Default | Description
|
| Name | gitlint version | Default | Description |
|
||||||
----------------------|-------------------|------------------------------|----------------------------------
|
| ------ | --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
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.
|
| 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.
|
| ignore | >= 0.10.0 | all | Comma-separated list of rule names or ids to ignore when this rule is matched. |
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
|
@ -376,19 +397,20 @@ ignore=all
|
||||||
regex=(.*)release(.*)
|
regex=(.*)release(.*)
|
||||||
ignore=T1,body-min-length,B6
|
ignore=T1,body-min-length,B6
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
## I3: ignore-body-lines
|
## I3: ignore-body-lines
|
||||||
|
|
||||||
ID | Name | gitlint version | Description
|
| ID | Name | gitlint version | Description |
|
||||||
------|-----------------------------|-----------------|-------------------------------------------
|
| --- | ----------------- | --------------- | --------------------------------------------------------- |
|
||||||
I3 | ignore-body-lines | >= 0.14.0 | Ignore certain lines in a commit body that match a regex.
|
| I3 | ignore-body-lines | >= 0.14.0 | Ignore certain lines in a commit body that match a regex. |
|
||||||
|
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Name | gitlint version | Default | Description
|
| 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).
|
| 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
|
### Examples
|
||||||
|
|
||||||
|
@ -407,19 +429,20 @@ regex=(^Co-Authored-By)|(^Signed-off-by)
|
||||||
[ignore-body-lines]
|
[ignore-body-lines]
|
||||||
regex=(.*)foobar(.*)
|
regex=(.*)foobar(.*)
|
||||||
```
|
```
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
## I4: ignore-by-author-name
|
## I4: ignore-by-author-name
|
||||||
|
|
||||||
ID | Name | gitlint version | Description
|
| ID | Name | gitlint version | Description |
|
||||||
------|---------------------------|-----------------|-------------------------------------------
|
| --- | --------------------- | --------------- | -------------------------------------------------- |
|
||||||
I4 | ignore-by-author-name | >= 0.16.0 | Ignore a commit based on matching its author name.
|
| I4 | ignore-by-author-name | >= 0.16.0 | Ignore a commit based on matching its author name. |
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
Name | gitlint version | Default | Description
|
| Name | gitlint version | Default | Description |
|
||||||
----------------------|-------------------|------------------------------|----------------------------------
|
| ------ | --------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
regex | >= 0.16.0 | None | [Python regex](https://docs.python.org/library/re.html) to match against the commit author name. On match, the commit will be ignored.
|
| regex | >= 0.16.0 | None | [Python regex](https://docs.python.org/library/re.html) to match against the commit author name. On match, the commit will be ignored. |
|
||||||
ignore | >= 0.16.0 | all | Comma-separated list of rule names or ids to ignore when this rule is matched.
|
| ignore | >= 0.16.0 | all | Comma-separated list of rule names or ids to ignore when this rule is matched. |
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
|
@ -435,4 +458,4 @@ regex=dependabot
|
||||||
[ignore-by-author-name]
|
[ignore-by-author-name]
|
||||||
regex=(.*)\[bot\](.*)
|
regex=(.*)\[bot\](.*)
|
||||||
ignore=T1,body-min-length,B6
|
ignore=T1,body-min-length,B6
|
||||||
```
|
```
|
||||||
|
|
|
@ -179,27 +179,33 @@ Both `CommitRule`s and `LineRule`s take a `commit` object in their `validate(...
|
||||||
The table below outlines the various attributes of that commit object that can be used during validation.
|
The table below outlines the various attributes of that commit object that can be used during validation.
|
||||||
|
|
||||||
|
|
||||||
Property | Type | Description
|
| Property | Type | Description |
|
||||||
-------------------------------| ---------------|-------------------
|
| -------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------ |
|
||||||
commit.message | object | Python object representing the commit message
|
| commit | `GitCommit` | Python object representing the commit |
|
||||||
commit.message.original | string | Original commit message as returned by git
|
| commit.message | `GitCommitMessage` | Python object representing the commit message |
|
||||||
commit.message.full | string | Full commit message, with comments (lines starting with #) removed.
|
| commit.message.original | `str` | Original commit message as returned by git |
|
||||||
commit.message.title | string | Title/subject of the commit message: the first line
|
| commit.message.full | `str` | Full commit message, with comments (lines starting with #) removed. |
|
||||||
commit.message.body | string[] | List of lines in the body of the commit message (i.e. starting from the second line)
|
| commit.message.title | `str` | Title/subject of the commit message: the first line |
|
||||||
commit.author_name | string | Name of the author, result of `git log --pretty=%aN`
|
| commit.message.body | `str[]` | List of lines in the body of the commit message (i.e. starting from the second line) |
|
||||||
commit.author_email | string | Email of the author, result of `git log --pretty=%aE`
|
| commit.author_name | `str` | Name of the author, result of `git log --pretty=%aN` |
|
||||||
commit.date | datetime | Python `datetime` object representing the time of commit
|
| commit.author_email | `str` | Email of the author, result of `git log --pretty=%aE` |
|
||||||
commit.is_merge_commit | boolean | Boolean indicating whether the commit is a merge commit or not.
|
| commit.date | `datetime.datetime` | Python `datetime` object representing the time of commit |
|
||||||
commit.is_revert_commit | boolean | Boolean indicating whether the commit is a revert commit or not.
|
| commit.is_merge_commit | `bool` | Boolean indicating whether the commit is a merge commit or not. |
|
||||||
commit.is_fixup_commit | boolean | Boolean indicating whether the commit is a fixup commit or not.
|
| commit.is_revert_commit | `bool` | Boolean indicating whether the commit is a revert commit or not. |
|
||||||
commit.is_squash_commit | boolean | Boolean indicating whether the commit is a squash commit or not.
|
| commit.is_fixup_commit | `bool` | Boolean indicating whether the commit is a fixup commit or not. |
|
||||||
commit.parents | string[] | List of parent commit `sha`s (only for merge commits).
|
| commit.is_fixup_amend_commit | `bool` | Boolean indicating whether the commit is a (fixup) amend commit or not. |
|
||||||
commit.changed_files | string[] | List of files changed in the commit (relative paths).
|
| commit.is_squash_commit | `bool` | Boolean indicating whether the commit is a squash commit or not. |
|
||||||
commit.branches | string[] | List of branch names the commit is part of
|
| commit.parents | `str[]` | List of parent commit `sha`s (only for merge commits). |
|
||||||
commit.context | object | Object pointing to the bigger git context that the commit is part of
|
| commit.changed_files | `str[]` | List of files changed in the commit (relative paths). |
|
||||||
commit.context.current_branch | string | Name of the currently active branch (of local repo)
|
| commit.changed_files_stats | `dict[str, GitChangedFilesStats]` | Dictionary mapping the changed files to a `GitChangedFilesStats` objects |
|
||||||
commit.context.repository_path | string | Absolute path pointing to the git repository being linted
|
| commit.changed_files_stats["path"].filepath | `pathlib.Path` | Relative path (compared to repo root) of the file that was changed. |
|
||||||
commit.context.commits | object[] | List of commits gitlint is acting on, NOT all commits in the repo.
|
| commit.changed_files_stats["path"].additions | `int` | Number of additions in the file. |
|
||||||
|
| commit.changed_files_stats["path"].deletions | `int` | Number of deletions in the file. |
|
||||||
|
| commit.branches | `str[]` | List of branch names the commit is part of |
|
||||||
|
| commit.context | `GitContext` | Object pointing to the bigger git context that the commit is part of |
|
||||||
|
| commit.context.current_branch | `str` | Name of the currently active branch (of local repo) |
|
||||||
|
| commit.context.repository_path | `str` | Absolute path pointing to the git repository being linted |
|
||||||
|
| commit.context.commits | `GitCommit[]` | List of commits gitlint is acting on, NOT all commits in the repo. |
|
||||||
|
|
||||||
## Violations
|
## Violations
|
||||||
In order to let gitlint know that there is a violation in the commit being linted, users should have the `validate(...)`
|
In order to let gitlint know that there is a violation in the commit being linted, users should have the `validate(...)`
|
||||||
|
@ -216,12 +222,12 @@ RuleViolation(rule_id, message, content=None, line_nr=None):
|
||||||
```
|
```
|
||||||
With the parameters meaning the following:
|
With the parameters meaning the following:
|
||||||
|
|
||||||
Parameter | Type | Description
|
| Parameter | Type | Description |
|
||||||
--------------|---------|--------------------------------
|
| --------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
rule_id | string | Rule's unique string id
|
| rule_id | `str` | Rule's unique string id |
|
||||||
message | string | Short description of the violation
|
| message | `str` | Short description of the violation |
|
||||||
content | string | (optional) the violating part of commit or line
|
| content | `str` | (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
|
```python
|
||||||
|
@ -281,14 +287,14 @@ As `options_spec` is a list, you can obviously have multiple options per rule. T
|
||||||
|
|
||||||
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
|
| Option Class | Use for |
|
||||||
------------------|--------------
|
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
`StrOption ` | Strings
|
| `StrOption ` | Strings |
|
||||||
`IntOption` | Integers. `IntOption` takes an optional `allow_negative` parameter if you want to allow negative integers.
|
| `IntOption` | Integers. `IntOption` takes an optional `allow_negative` parameter if you want to allow negative integers. |
|
||||||
`BoolOption` | Booleans. Valid values: `true`, `false`. Case-insensitive.
|
| `BoolOption` | Booleans. Valid values: `true`, `false`. Case-insensitive. |
|
||||||
`ListOption` | List of strings. Comma separated.
|
| `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`).
|
| `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.
|
| `RegexOption` | String representing a [Python-style regex](https://docs.python.org/library/re.html) - compiled and validated before rules are applied. |
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
Gitlint currently does not support options for all possible types (e.g. float, list of int, etc).
|
Gitlint currently does not support options for all possible types (e.g. float, list of int, etc).
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from gitlint.rules import CommitRule, RuleViolation
|
from gitlint.rules import CommitRule, RuleViolation
|
||||||
from gitlint.options import IntOption, ListOption
|
from gitlint.options import IntOption, ListOption
|
||||||
|
|
||||||
|
@ -27,20 +25,20 @@ class BodyMaxLineCount(CommitRule):
|
||||||
id = "UC1"
|
id = "UC1"
|
||||||
|
|
||||||
# A rule MAY have an option_spec if its behavior should be configurable.
|
# A rule MAY have an option_spec if its behavior should be configurable.
|
||||||
options_spec = [IntOption('max-line-count', 3, "Maximum body line count")]
|
options_spec = [IntOption("max-line-count", 3, "Maximum body line count")]
|
||||||
|
|
||||||
def validate(self, commit):
|
def validate(self, commit):
|
||||||
self.log.debug("BodyMaxLineCount: This will be visible when running `gitlint --debug`")
|
self.log.debug("BodyMaxLineCount: This will be visible when running `gitlint --debug`")
|
||||||
|
|
||||||
line_count = len(commit.message.body)
|
line_count = len(commit.message.body)
|
||||||
max_line_count = self.options['max-line-count'].value
|
max_line_count = self.options["max-line-count"].value
|
||||||
if line_count > max_line_count:
|
if line_count > max_line_count:
|
||||||
message = f"Body contains too many lines ({line_count} > {max_line_count})"
|
message = f"Body contains too many lines ({line_count} > {max_line_count})"
|
||||||
return [RuleViolation(self.id, message, line_nr=1)]
|
return [RuleViolation(self.id, message, line_nr=1)]
|
||||||
|
|
||||||
|
|
||||||
class SignedOffBy(CommitRule):
|
class SignedOffBy(CommitRule):
|
||||||
""" This rule will enforce that each commit contains a "Signed-off-by" line.
|
"""This rule will enforce that each commit contains a "Signed-off-by" line.
|
||||||
We keep things simple here and just check whether the commit body contains a line that starts with "Signed-off-by".
|
We keep things simple here and just check whether the commit body contains a line that starts with "Signed-off-by".
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -61,8 +59,8 @@ class SignedOffBy(CommitRule):
|
||||||
|
|
||||||
|
|
||||||
class BranchNamingConventions(CommitRule):
|
class BranchNamingConventions(CommitRule):
|
||||||
""" This rule will enforce that a commit is part of a branch that meets certain naming conventions.
|
"""This rule will enforce that a commit is part of a branch that meets certain naming conventions.
|
||||||
See GitFlow for real-world example of this: https://nvie.com/posts/a-successful-git-branching-model/
|
See GitFlow for real-world example of this: https://nvie.com/posts/a-successful-git-branching-model/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# A rule MUST have a human friendly name
|
# A rule MUST have a human friendly name
|
||||||
|
@ -72,13 +70,13 @@ class BranchNamingConventions(CommitRule):
|
||||||
id = "UC3"
|
id = "UC3"
|
||||||
|
|
||||||
# A rule MAY have an option_spec if its behavior should be configurable.
|
# A rule MAY have an option_spec if its behavior should be configurable.
|
||||||
options_spec = [ListOption('branch-prefixes', ["feature/", "hotfix/", "release/"], "Allowed branch prefixes")]
|
options_spec = [ListOption("branch-prefixes", ["feature/", "hotfix/", "release/"], "Allowed branch prefixes")]
|
||||||
|
|
||||||
def validate(self, commit):
|
def validate(self, commit):
|
||||||
self.log.debug("BranchNamingConventions: This line will be visible when running `gitlint --debug`")
|
self.log.debug("BranchNamingConventions: This line will be visible when running `gitlint --debug`")
|
||||||
|
|
||||||
violations = []
|
violations = []
|
||||||
allowed_branch_prefixes = self.options['branch-prefixes'].value
|
allowed_branch_prefixes = self.options["branch-prefixes"].value
|
||||||
for branch in commit.branches:
|
for branch in commit.branches:
|
||||||
valid_branch_name = False
|
valid_branch_name = False
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from gitlint.rules import ConfigurationRule
|
from gitlint.rules import ConfigurationRule
|
||||||
from gitlint.options import IntOption
|
from gitlint.options import IntOption
|
||||||
|
|
||||||
|
@ -36,7 +34,7 @@ class ReleaseConfigurationRule(ConfigurationRule):
|
||||||
id = "UCR1"
|
id = "UCR1"
|
||||||
|
|
||||||
# A rule MAY have an option_spec if its behavior should be configurable.
|
# A rule MAY have an option_spec if its behavior should be configurable.
|
||||||
options_spec = [IntOption('custom-verbosity', 2, "Gitlint verbosity for release commits")]
|
options_spec = [IntOption("custom-verbosity", 2, "Gitlint verbosity for release commits")]
|
||||||
|
|
||||||
def apply(self, config, commit):
|
def apply(self, config, commit):
|
||||||
self.log.debug("ReleaseConfigurationRule: This will be visible when running `gitlint --debug`")
|
self.log.debug("ReleaseConfigurationRule: This will be visible when running `gitlint --debug`")
|
||||||
|
@ -44,7 +42,6 @@ class ReleaseConfigurationRule(ConfigurationRule):
|
||||||
# If the commit title starts with 'Release', we want to modify
|
# If the commit title starts with 'Release', we want to modify
|
||||||
# how all subsequent rules interpret that commit
|
# how all subsequent rules interpret that commit
|
||||||
if commit.message.title.startswith("Release"):
|
if commit.message.title.startswith("Release"):
|
||||||
|
|
||||||
# If your Release commit messages are auto-generated, the
|
# If your Release commit messages are auto-generated, the
|
||||||
# body might contain trailing whitespace. Let's ignore that
|
# body might contain trailing whitespace. Let's ignore that
|
||||||
config.ignore.append("body-trailing-whitespace")
|
config.ignore.append("body-trailing-whitespace")
|
||||||
|
@ -60,7 +57,7 @@ class ReleaseConfigurationRule(ConfigurationRule):
|
||||||
# config.set_general_option(<general-option>, <value>)
|
# config.set_general_option(<general-option>, <value>)
|
||||||
config.set_general_option("verbosity", 2)
|
config.set_general_option("verbosity", 2)
|
||||||
# Wwe can also use custom options to make this configurable
|
# Wwe can also use custom options to make this configurable
|
||||||
config.set_general_option("verbosity", self.options['custom-verbosity'].value)
|
config.set_general_option("verbosity", self.options["custom-verbosity"].value)
|
||||||
|
|
||||||
# Strip any lines starting with $ from the commit message
|
# Strip any lines starting with $ from the commit message
|
||||||
# (this only affects how gitlint sees your commit message, it does
|
# (this only affects how gitlint sees your commit message, it does
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from gitlint.rules import LineRule, RuleViolation, CommitMessageTitle
|
from gitlint.rules import LineRule, RuleViolation, CommitMessageTitle
|
||||||
from gitlint.options import ListOption
|
from gitlint.options import ListOption
|
||||||
|
|
||||||
|
@ -21,8 +19,8 @@ that fits your needs.
|
||||||
|
|
||||||
|
|
||||||
class SpecialChars(LineRule):
|
class SpecialChars(LineRule):
|
||||||
""" This rule will enforce that the commit message title does not contain any of the following characters:
|
"""This rule will enforce that the commit message title does not contain any of the following characters:
|
||||||
$^%@!*() """
|
$^%@!*()"""
|
||||||
|
|
||||||
# A rule MUST have a human friendly name
|
# A rule MUST have a human friendly name
|
||||||
name = "title-no-special-chars"
|
name = "title-no-special-chars"
|
||||||
|
@ -35,15 +33,20 @@ class SpecialChars(LineRule):
|
||||||
target = CommitMessageTitle
|
target = CommitMessageTitle
|
||||||
|
|
||||||
# A rule MAY have an option_spec if its behavior should be configurable.
|
# A rule MAY have an option_spec if its behavior should be configurable.
|
||||||
options_spec = [ListOption('special-chars', ['$', '^', '%', '@', '!', '*', '(', ')'],
|
options_spec = [
|
||||||
"Comma separated list of characters that should not occur in the title")]
|
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`")
|
self.log.debug("SpecialChars: This will be visible when running `gitlint --debug`")
|
||||||
|
|
||||||
violations = []
|
violations = []
|
||||||
# options can be accessed by looking them up by their name in self.options
|
# options can be accessed by looking them up by their name in self.options
|
||||||
for char in self.options['special-chars'].value:
|
for char in self.options["special-chars"].value:
|
||||||
if char in line:
|
if char in line:
|
||||||
msg = f"Title contains the special character '{char}'"
|
msg = f"Title contains the special character '{char}'"
|
||||||
violation = RuleViolation(self.id, msg, line)
|
violation = RuleViolation(self.id, msg, line)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
__version__ = "0.17.0"
|
__version__ = "0.18.0"
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
class PropertyCache:
|
class PropertyCache:
|
||||||
""" Mixin class providing a simple cache. """
|
"""Mixin class providing a simple cache."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._cache = {}
|
self._cache = {}
|
||||||
|
|
||||||
def _try_cache(self, cache_key, cache_populate_func):
|
def _try_cache(self, cache_key, cache_populate_func):
|
||||||
""" Tries to get a value from the cache identified by `cache_key`.
|
"""Tries to get a value from the cache identified by `cache_key`.
|
||||||
If no value is found in the cache, do a function call to `cache_populate_func` to populate the cache
|
If no value is found in the cache, do a function call to `cache_populate_func` to populate the cache
|
||||||
and then return the value from the cache. """
|
and then return the value from the cache."""
|
||||||
if cache_key not in self._cache:
|
if cache_key not in self._cache:
|
||||||
cache_populate_func()
|
cache_populate_func()
|
||||||
return self._cache[cache_key]
|
return self._cache[cache_key]
|
||||||
|
|
||||||
|
|
||||||
def cache(original_func=None, cachekey=None): # pylint: disable=unused-argument
|
def cache(original_func=None, cachekey=None): # pylint: disable=unused-argument
|
||||||
""" Cache decorator. Caches function return values.
|
"""Cache decorator. Caches function return values.
|
||||||
Requires the parent class to extend and initialize PropertyCache.
|
Requires the parent class to extend and initialize PropertyCache.
|
||||||
Usage:
|
Usage:
|
||||||
# Use function name as cache key
|
# Use function name as cache key
|
||||||
@cache
|
@cache
|
||||||
def myfunc(args):
|
def myfunc(args):
|
||||||
...
|
...
|
||||||
|
|
||||||
# Specify cache key
|
# Specify cache key
|
||||||
@cache(cachekey="foobar")
|
@cache(cachekey="foobar")
|
||||||
def myfunc(args):
|
def myfunc(args):
|
||||||
...
|
...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Decorators with optional arguments are a bit convoluted in python, see some of the links below for details.
|
# Decorators with optional arguments are a bit convoluted in python, see some of the links below for details.
|
||||||
|
@ -41,6 +41,7 @@ def cache(original_func=None, cachekey=None): # pylint: disable=unused-argument
|
||||||
def cache_func_result():
|
def cache_func_result():
|
||||||
# Call decorated function and store its result in the cache
|
# Call decorated function and store its result in the cache
|
||||||
args[0]._cache[cachekey] = func(*args)
|
args[0]._cache[cachekey] = func(*args)
|
||||||
|
|
||||||
return args[0]._try_cache(cachekey, cache_func_result)
|
return args[0]._try_cache(cachekey, cache_func_result)
|
||||||
|
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
|
@ -11,6 +11,7 @@ import click
|
||||||
import gitlint
|
import gitlint
|
||||||
from gitlint.lint import GitLinter
|
from gitlint.lint import GitLinter
|
||||||
from gitlint.config import LintConfigBuilder, LintConfigError, LintConfigGenerator
|
from gitlint.config import LintConfigBuilder, LintConfigError, LintConfigGenerator
|
||||||
|
from gitlint.deprecation import LOG as DEPRECATED_LOG, DEPRECATED_LOG_FORMAT
|
||||||
from gitlint.git import GitContext, GitContextError, git_version
|
from gitlint.git import GitContext, GitContextError, git_version
|
||||||
from gitlint import hooks
|
from gitlint import hooks
|
||||||
from gitlint.shell import shell
|
from gitlint.shell import shell
|
||||||
|
@ -37,19 +38,29 @@ LOG = logging.getLogger("gitlint.cli")
|
||||||
|
|
||||||
|
|
||||||
class GitLintUsageError(GitlintError):
|
class GitLintUsageError(GitlintError):
|
||||||
""" Exception indicating there is an issue with how gitlint is used. """
|
"""Exception indicating there is an issue with how gitlint is used."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def setup_logging():
|
def setup_logging():
|
||||||
""" Setup gitlint logging """
|
"""Setup gitlint logging"""
|
||||||
|
|
||||||
|
# Root log, mostly used for debug
|
||||||
root_log = logging.getLogger("gitlint")
|
root_log = logging.getLogger("gitlint")
|
||||||
root_log.propagate = False # Don't propagate to child loggers, the gitlint root logger handles everything
|
root_log.propagate = False # Don't propagate to child loggers, the gitlint root logger handles everything
|
||||||
|
root_log.setLevel(logging.ERROR)
|
||||||
handler = logging.StreamHandler()
|
handler = logging.StreamHandler()
|
||||||
formatter = logging.Formatter(LOG_FORMAT)
|
formatter = logging.Formatter(LOG_FORMAT)
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
root_log.addHandler(handler)
|
root_log.addHandler(handler)
|
||||||
root_log.setLevel(logging.ERROR)
|
|
||||||
|
# Deprecated log, to log deprecation warnings
|
||||||
|
DEPRECATED_LOG.propagate = False # Don't propagate to child logger
|
||||||
|
DEPRECATED_LOG.setLevel(logging.WARNING)
|
||||||
|
deprecated_log_handler = logging.StreamHandler()
|
||||||
|
deprecated_log_handler.setFormatter(logging.Formatter(DEPRECATED_LOG_FORMAT))
|
||||||
|
DEPRECATED_LOG.addHandler(deprecated_log_handler)
|
||||||
|
|
||||||
|
|
||||||
def log_system_info():
|
def log_system_info():
|
||||||
|
@ -62,10 +73,20 @@ def log_system_info():
|
||||||
|
|
||||||
|
|
||||||
def build_config( # pylint: disable=too-many-arguments
|
def build_config( # pylint: disable=too-many-arguments
|
||||||
target, config_path, c, extra_path, ignore, contrib, ignore_stdin, staged, fail_without_commits, verbose,
|
target,
|
||||||
silent, debug
|
config_path,
|
||||||
|
c,
|
||||||
|
extra_path,
|
||||||
|
ignore,
|
||||||
|
contrib,
|
||||||
|
ignore_stdin,
|
||||||
|
staged,
|
||||||
|
fail_without_commits,
|
||||||
|
verbose,
|
||||||
|
silent,
|
||||||
|
debug,
|
||||||
):
|
):
|
||||||
""" Creates a LintConfig object based on a set of commandline parameters. """
|
"""Creates a LintConfig object based on a set of commandline parameters."""
|
||||||
config_builder = LintConfigBuilder()
|
config_builder = LintConfigBuilder()
|
||||||
# Config precedence:
|
# Config precedence:
|
||||||
# First, load default config or config from configfile
|
# First, load default config or config from configfile
|
||||||
|
@ -79,33 +100,33 @@ def build_config( # pylint: disable=too-many-arguments
|
||||||
|
|
||||||
# Finally, overwrite with any convenience commandline flags
|
# Finally, overwrite with any convenience commandline flags
|
||||||
if ignore:
|
if ignore:
|
||||||
config_builder.set_option('general', 'ignore', ignore)
|
config_builder.set_option("general", "ignore", ignore)
|
||||||
|
|
||||||
if contrib:
|
if contrib:
|
||||||
config_builder.set_option('general', 'contrib', contrib)
|
config_builder.set_option("general", "contrib", contrib)
|
||||||
|
|
||||||
if ignore_stdin:
|
if ignore_stdin:
|
||||||
config_builder.set_option('general', 'ignore-stdin', ignore_stdin)
|
config_builder.set_option("general", "ignore-stdin", ignore_stdin)
|
||||||
|
|
||||||
if silent:
|
if silent:
|
||||||
config_builder.set_option('general', 'verbosity', 0)
|
config_builder.set_option("general", "verbosity", 0)
|
||||||
elif verbose > 0:
|
elif verbose > 0:
|
||||||
config_builder.set_option('general', 'verbosity', verbose)
|
config_builder.set_option("general", "verbosity", verbose)
|
||||||
|
|
||||||
if extra_path:
|
if extra_path:
|
||||||
config_builder.set_option('general', 'extra-path', extra_path)
|
config_builder.set_option("general", "extra-path", extra_path)
|
||||||
|
|
||||||
if target:
|
if target:
|
||||||
config_builder.set_option('general', 'target', target)
|
config_builder.set_option("general", "target", target)
|
||||||
|
|
||||||
if debug:
|
if debug:
|
||||||
config_builder.set_option('general', 'debug', debug)
|
config_builder.set_option("general", "debug", debug)
|
||||||
|
|
||||||
if staged:
|
if staged:
|
||||||
config_builder.set_option('general', 'staged', staged)
|
config_builder.set_option("general", "staged", staged)
|
||||||
|
|
||||||
if fail_without_commits:
|
if fail_without_commits:
|
||||||
config_builder.set_option('general', 'fail-without-commits', fail_without_commits)
|
config_builder.set_option("general", "fail-without-commits", fail_without_commits)
|
||||||
|
|
||||||
config = config_builder.build()
|
config = config_builder.build()
|
||||||
|
|
||||||
|
@ -113,7 +134,7 @@ def build_config( # pylint: disable=too-many-arguments
|
||||||
|
|
||||||
|
|
||||||
def get_stdin_data():
|
def get_stdin_data():
|
||||||
""" Helper function that returns data send to stdin or False if nothing is send """
|
"""Helper function that returns data sent to stdin or False if nothing is sent"""
|
||||||
# STDIN can only be 3 different types of things ("modes")
|
# STDIN can only be 3 different types of things ("modes")
|
||||||
# 1. An interactive terminal device (i.e. a TTY -> sys.stdin.isatty() or stat.S_ISCHR)
|
# 1. An interactive terminal device (i.e. a TTY -> sys.stdin.isatty() or stat.S_ISCHR)
|
||||||
# 2. A (named) pipe (stat.S_ISFIFO)
|
# 2. A (named) pipe (stat.S_ISFIFO)
|
||||||
|
@ -145,13 +166,17 @@ def get_stdin_data():
|
||||||
|
|
||||||
|
|
||||||
def build_git_context(lint_config, msg_filename, commit_hash, refspec):
|
def build_git_context(lint_config, msg_filename, commit_hash, refspec):
|
||||||
""" Builds a git context based on passed parameters and order of precedence """
|
"""Builds a git context based on passed parameters and order of precedence"""
|
||||||
|
|
||||||
# Determine which GitContext method to use if a custom message is passed
|
# Determine which GitContext method to use if a custom message is passed
|
||||||
from_commit_msg = GitContext.from_commit_msg
|
from_commit_msg = GitContext.from_commit_msg
|
||||||
if lint_config.staged:
|
if lint_config.staged:
|
||||||
LOG.debug("Fetching additional meta-data from staged commit")
|
LOG.debug("Fetching additional meta-data from staged commit")
|
||||||
from_commit_msg = lambda message: GitContext.from_staged_commit(message, lint_config.target) # noqa
|
from_commit_msg = (
|
||||||
|
lambda message: GitContext.from_staged_commit( # pylint: disable=unnecessary-lambda-assignment
|
||||||
|
message, lint_config.target
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Order of precedence:
|
# Order of precedence:
|
||||||
# 1. Any data specified via --msg-filename
|
# 1. Any data specified via --msg-filename
|
||||||
|
@ -168,8 +193,10 @@ def build_git_context(lint_config, msg_filename, commit_hash, refspec):
|
||||||
return from_commit_msg(stdin_input)
|
return from_commit_msg(stdin_input)
|
||||||
|
|
||||||
if lint_config.staged:
|
if lint_config.staged:
|
||||||
raise GitLintUsageError("The 'staged' option (--staged) can only be used when using '--msg-filename' or "
|
raise GitLintUsageError(
|
||||||
"when piping data to gitlint via stdin.")
|
"The 'staged' option (--staged) can only be used when using '--msg-filename' or "
|
||||||
|
"when piping data to gitlint via stdin."
|
||||||
|
)
|
||||||
|
|
||||||
# 3. Fallback to reading from local repository
|
# 3. Fallback to reading from local repository
|
||||||
LOG.debug("No --msg-filename flag, no or empty data passed to stdin. Using the local repo.")
|
LOG.debug("No --msg-filename flag, no or empty data passed to stdin. Using the local repo.")
|
||||||
|
@ -177,11 +204,25 @@ def build_git_context(lint_config, msg_filename, commit_hash, refspec):
|
||||||
if commit_hash and refspec:
|
if commit_hash and refspec:
|
||||||
raise GitLintUsageError("--commit and --commits are mutually exclusive, use one or the other.")
|
raise GitLintUsageError("--commit and --commits are mutually exclusive, use one or the other.")
|
||||||
|
|
||||||
return GitContext.from_local_repository(lint_config.target, refspec=refspec, commit_hash=commit_hash)
|
# 3.1 Linting a range of commits
|
||||||
|
if refspec:
|
||||||
|
# 3.1.1 Not real refspec, but comma-separated list of commit hashes
|
||||||
|
if "," in refspec:
|
||||||
|
commit_hashes = [hash.strip() for hash in refspec.split(",")]
|
||||||
|
return GitContext.from_local_repository(lint_config.target, commit_hashes=commit_hashes)
|
||||||
|
# 3.1.2 Real refspec
|
||||||
|
return GitContext.from_local_repository(lint_config.target, refspec=refspec)
|
||||||
|
|
||||||
|
# 3.2 Linting a specific commit
|
||||||
|
if commit_hash:
|
||||||
|
return GitContext.from_local_repository(lint_config.target, commit_hashes=[commit_hash])
|
||||||
|
|
||||||
|
# 3.3 Fallback to linting the current HEAD
|
||||||
|
return GitContext.from_local_repository(lint_config.target)
|
||||||
|
|
||||||
|
|
||||||
def handle_gitlint_error(ctx, exc):
|
def handle_gitlint_error(ctx, exc):
|
||||||
""" Helper function to handle exceptions """
|
"""Helper function to handle exceptions"""
|
||||||
if isinstance(exc, GitContextError):
|
if isinstance(exc, GitContextError):
|
||||||
click.echo(exc)
|
click.echo(exc)
|
||||||
ctx.exit(GIT_CONTEXT_ERROR_CODE)
|
ctx.exit(GIT_CONTEXT_ERROR_CODE)
|
||||||
|
@ -194,7 +235,7 @@ def handle_gitlint_error(ctx, exc):
|
||||||
|
|
||||||
|
|
||||||
class ContextObj:
|
class ContextObj:
|
||||||
""" Simple class to hold data that is passed between Click commands via the Click context. """
|
"""Simple class to hold data that is passed between Click commands via the Click context."""
|
||||||
|
|
||||||
def __init__(self, config, config_builder, commit_hash, refspec, msg_filename, gitcontext=None):
|
def __init__(self, config, config_builder, commit_hash, refspec, msg_filename, gitcontext=None):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
@ -205,29 +246,34 @@ class ContextObj:
|
||||||
self.gitcontext = gitcontext
|
self.gitcontext = gitcontext
|
||||||
|
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
@click.group(invoke_without_command=True, context_settings={'max_content_width': 120},
|
@click.group(invoke_without_command=True, context_settings={'max_content_width': 120},
|
||||||
epilog="When no COMMAND is specified, gitlint defaults to 'gitlint lint'.")
|
epilog="When no COMMAND is specified, gitlint defaults to 'gitlint lint'.")
|
||||||
@click.option('--target', envvar='GITLINT_TARGET',
|
@click.option('--target', envvar='GITLINT_TARGET',
|
||||||
type=click.Path(exists=True, resolve_path=True, file_okay=False, readable=True),
|
type=click.Path(exists=True, resolve_path=True, file_okay=False, readable=True),
|
||||||
help="Path of the target git repository. [default: current working directory]")
|
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),
|
@click.option('-C', '--config', envvar='GITLINT_CONFIG',
|
||||||
|
type=click.Path(exists=True, dir_okay=False, readable=True, resolve_path=True),
|
||||||
help=f"Config file location [default: {DEFAULT_CONFIG_FILE}]")
|
help=f"Config file location [default: {DEFAULT_CONFIG_FILE}]")
|
||||||
@click.option('-c', multiple=True,
|
@click.option('-c', multiple=True,
|
||||||
help="Config flags in format <rule>.<option>=<value> (e.g.: -c T1.line-length=80). " +
|
help="Config flags in format <rule>.<option>=<value> (e.g.: -c T1.line-length=80). " +
|
||||||
"Flag can be used multiple times to set multiple config values.") # pylint: disable=bad-continuation
|
"Flag can be used multiple times to set multiple config values.") # pylint: disable=bad-continuation
|
||||||
@click.option('--commit', envvar='GITLINT_COMMIT', default=None, help="Hash (SHA) of specific commit to lint.")
|
@click.option('--commit', envvar='GITLINT_COMMIT', default=None, help="Hash (SHA) of specific commit to lint.")
|
||||||
@click.option('--commits', envvar='GITLINT_COMMITS', default=None, help="The range of commits to lint. [default: HEAD]")
|
@click.option('--commits', envvar='GITLINT_COMMITS', default=None,
|
||||||
|
help="The range of commits (refspec or comma-separated hashes) to lint. [default: HEAD]")
|
||||||
@click.option('-e', '--extra-path', envvar='GITLINT_EXTRA_PATH',
|
@click.option('-e', '--extra-path', envvar='GITLINT_EXTRA_PATH',
|
||||||
help="Path to a directory or python module with extra user-defined rules",
|
help="Path to a directory or python module with extra user-defined rules",
|
||||||
type=click.Path(exists=True, resolve_path=True, readable=True))
|
type=click.Path(exists=True, resolve_path=True, readable=True))
|
||||||
@click.option('--ignore', envvar='GITLINT_IGNORE', default="", help="Ignore rules (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="",
|
@click.option('--contrib', envvar='GITLINT_CONTRIB', default="",
|
||||||
help="Contrib rules to enable (comma-separated by id or name).")
|
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('--msg-filename', type=click.File(encoding=gitlint.utils.DEFAULT_ENCODING),
|
||||||
|
help="Path to a file containing a commit-msg.")
|
||||||
@click.option('--ignore-stdin', envvar='GITLINT_IGNORE_STDIN', is_flag=True,
|
@click.option('--ignore-stdin', envvar='GITLINT_IGNORE_STDIN', is_flag=True,
|
||||||
help="Ignore any stdin data. Useful for running in CI server.")
|
help="Ignore any stdin data. Useful for running in CI server.")
|
||||||
@click.option('--staged', envvar='GITLINT_STAGED', is_flag=True,
|
@click.option('--staged', envvar='GITLINT_STAGED', is_flag=True,
|
||||||
help="Read staged commit meta-info from the local repository.")
|
help="Attempt smart guesses about meta info (like author name, email, branch, changed files, etc) " +
|
||||||
|
"for staged commits.")
|
||||||
@click.option('--fail-without-commits', envvar='GITLINT_FAIL_WITHOUT_COMMITS', is_flag=True,
|
@click.option('--fail-without-commits', envvar='GITLINT_FAIL_WITHOUT_COMMITS', is_flag=True,
|
||||||
help="Hard fail when the target commit range is empty.")
|
help="Hard fail when the target commit range is empty.")
|
||||||
@click.option('-v', '--verbose', envvar='GITLINT_VERBOSITY', count=True, default=0,
|
@click.option('-v', '--verbose', envvar='GITLINT_VERBOSITY', count=True, default=0,
|
||||||
|
@ -246,18 +292,18 @@ def cli( # pylint: disable=too-many-arguments
|
||||||
|
|
||||||
Documentation: http://jorisroovers.github.io/gitlint
|
Documentation: http://jorisroovers.github.io/gitlint
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if debug:
|
if debug:
|
||||||
logging.getLogger("gitlint").setLevel(logging.DEBUG)
|
logging.getLogger("gitlint").setLevel(logging.DEBUG)
|
||||||
|
DEPRECATED_LOG.setLevel(logging.DEBUG)
|
||||||
LOG.debug("To report issues, please visit https://github.com/jorisroovers/gitlint/issues")
|
LOG.debug("To report issues, please visit https://github.com/jorisroovers/gitlint/issues")
|
||||||
|
|
||||||
log_system_info()
|
log_system_info()
|
||||||
|
|
||||||
# Get the lint config from the commandline parameters and
|
# Get the lint config from the commandline parameters and
|
||||||
# store it in the context (click allows storing an arbitrary object in ctx.obj).
|
# store it in the context (click allows storing an arbitrary object in ctx.obj).
|
||||||
config, config_builder = build_config(target, config, c, extra_path, ignore, contrib, ignore_stdin, staged,
|
config, config_builder = build_config(target, config, c, extra_path, ignore, contrib, ignore_stdin,
|
||||||
fail_without_commits, verbose, silent, debug)
|
staged, fail_without_commits, verbose, silent, debug)
|
||||||
LOG.debug("Configuration\n%s", config)
|
LOG.debug("Configuration\n%s", config)
|
||||||
|
|
||||||
ctx.obj = ContextObj(config, config_builder, commit, commits, msg_filename)
|
ctx.obj = ContextObj(config, config_builder, commit, commits, msg_filename)
|
||||||
|
@ -268,12 +314,13 @@ def cli( # pylint: disable=too-many-arguments
|
||||||
|
|
||||||
except GitlintError as e:
|
except GitlintError as e:
|
||||||
handle_gitlint_error(ctx, e)
|
handle_gitlint_error(ctx, e)
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
@cli.command("lint")
|
@cli.command("lint")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def lint(ctx):
|
def lint(ctx):
|
||||||
""" Lints a git repository [default command] """
|
"""Lints a git repository [default command]"""
|
||||||
lint_config = ctx.obj.config
|
lint_config = ctx.obj.config
|
||||||
refspec = ctx.obj.refspec
|
refspec = ctx.obj.refspec
|
||||||
commit_hash = ctx.obj.commit_hash
|
commit_hash = ctx.obj.commit_hash
|
||||||
|
@ -295,7 +342,7 @@ def lint(ctx):
|
||||||
raise GitLintUsageError(f'No commits in range "{refspec}"')
|
raise GitLintUsageError(f'No commits in range "{refspec}"')
|
||||||
ctx.exit(GITLINT_SUCCESS)
|
ctx.exit(GITLINT_SUCCESS)
|
||||||
|
|
||||||
LOG.debug('Linting %d commit(s)', number_of_commits)
|
LOG.debug("Linting %d commit(s)", number_of_commits)
|
||||||
general_config_builder = ctx.obj.config_builder
|
general_config_builder = ctx.obj.config_builder
|
||||||
last_commit = gitcontext.commits[-1]
|
last_commit = gitcontext.commits[-1]
|
||||||
|
|
||||||
|
@ -334,7 +381,7 @@ def lint(ctx):
|
||||||
@cli.command("install-hook")
|
@cli.command("install-hook")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def install_hook(ctx):
|
def install_hook(ctx):
|
||||||
""" Install gitlint as a git commit-msg hook. """
|
"""Install gitlint as a git commit-msg hook."""
|
||||||
try:
|
try:
|
||||||
hooks.GitHookInstaller.install_commit_msg_hook(ctx.obj.config)
|
hooks.GitHookInstaller.install_commit_msg_hook(ctx.obj.config)
|
||||||
hook_path = hooks.GitHookInstaller.commit_msg_hook_path(ctx.obj.config)
|
hook_path = hooks.GitHookInstaller.commit_msg_hook_path(ctx.obj.config)
|
||||||
|
@ -348,7 +395,7 @@ def install_hook(ctx):
|
||||||
@cli.command("uninstall-hook")
|
@cli.command("uninstall-hook")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def uninstall_hook(ctx):
|
def uninstall_hook(ctx):
|
||||||
""" Uninstall gitlint commit-msg hook. """
|
"""Uninstall gitlint commit-msg hook."""
|
||||||
try:
|
try:
|
||||||
hooks.GitHookInstaller.uninstall_commit_msg_hook(ctx.obj.config)
|
hooks.GitHookInstaller.uninstall_commit_msg_hook(ctx.obj.config)
|
||||||
hook_path = hooks.GitHookInstaller.commit_msg_hook_path(ctx.obj.config)
|
hook_path = hooks.GitHookInstaller.commit_msg_hook_path(ctx.obj.config)
|
||||||
|
@ -362,7 +409,7 @@ def uninstall_hook(ctx):
|
||||||
@cli.command("run-hook")
|
@cli.command("run-hook")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def run_hook(ctx):
|
def run_hook(ctx):
|
||||||
""" Runs the gitlint commit-msg hook. """
|
"""Runs the gitlint commit-msg hook."""
|
||||||
|
|
||||||
exit_code = 1
|
exit_code = 1
|
||||||
while exit_code > 0:
|
while exit_code > 0:
|
||||||
|
@ -378,16 +425,18 @@ def run_hook(ctx):
|
||||||
|
|
||||||
exit_code = e.exit_code
|
exit_code = e.exit_code
|
||||||
if exit_code == GITLINT_SUCCESS:
|
if exit_code == GITLINT_SUCCESS:
|
||||||
click.echo("gitlint: " + click.style("OK", fg='green') + " (no violations in commit message)")
|
click.echo("gitlint: " + click.style("OK", fg="green") + " (no violations in commit message)")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
click.echo("-----------------------------------------------")
|
click.echo("-----------------------------------------------")
|
||||||
click.echo("gitlint: " + click.style("Your commit message contains violations.", fg='red'))
|
click.echo("gitlint: " + click.style("Your commit message contains violations.", fg="red"))
|
||||||
|
|
||||||
value = None
|
value = None
|
||||||
while value not in ["y", "n", "e"]:
|
while value not in ["y", "n", "e"]:
|
||||||
click.echo("Continue with commit anyways (this keeps the current commit message)? "
|
click.echo(
|
||||||
"[y(es)/n(no)/e(dit)] ", nl=False)
|
"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
|
# 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.
|
# input(). However, those functions currently don't support getting answers from stdin.
|
||||||
|
@ -431,15 +480,15 @@ def run_hook(ctx):
|
||||||
@cli.command("generate-config")
|
@cli.command("generate-config")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def generate_config(ctx):
|
def generate_config(ctx):
|
||||||
""" Generates a sample gitlint config file. """
|
"""Generates a sample gitlint config file."""
|
||||||
path = click.prompt('Please specify a location for the sample gitlint config file', default=DEFAULT_CONFIG_FILE)
|
path = click.prompt("Please specify a location for the sample gitlint config file", default=DEFAULT_CONFIG_FILE)
|
||||||
path = os.path.realpath(path)
|
path = os.path.realpath(path)
|
||||||
dir_name = os.path.dirname(path)
|
dir_name = os.path.dirname(path)
|
||||||
if not os.path.exists(dir_name):
|
if not os.path.exists(dir_name):
|
||||||
click.echo(f"Error: Directory '{dir_name}' does not exist.", err=True)
|
click.echo(f"Error: Directory '{dir_name}' does not exist.", err=True)
|
||||||
ctx.exit(USAGE_ERROR_CODE)
|
ctx.exit(USAGE_ERROR_CODE)
|
||||||
elif os.path.exists(path):
|
elif os.path.exists(path):
|
||||||
click.echo(f"Error: File \"{path}\" already exists.", err=True)
|
click.echo(f'Error: File "{path}" already exists.', err=True)
|
||||||
ctx.exit(USAGE_ERROR_CODE)
|
ctx.exit(USAGE_ERROR_CODE)
|
||||||
|
|
||||||
LintConfigGenerator.generate_config(path)
|
LintConfigGenerator.generate_config(path)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from configparser import ConfigParser, Error as ConfigParserError
|
from configparser import ConfigParser, Error as ConfigParserError
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import io
|
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
@ -16,8 +15,8 @@ from gitlint.exception import GitlintError
|
||||||
|
|
||||||
|
|
||||||
def handle_option_error(func):
|
def handle_option_error(func):
|
||||||
""" Decorator that calls given method/function and handles any RuleOptionError gracefully by converting it to a
|
"""Decorator that calls given method/function and handles any RuleOptionError gracefully by converting it to a
|
||||||
LintConfigError. """
|
LintConfigError."""
|
||||||
|
|
||||||
def wrapped(*args):
|
def wrapped(*args):
|
||||||
try:
|
try:
|
||||||
|
@ -32,53 +31,62 @@ class LintConfigError(GitlintError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class LintConfig:
|
class LintConfig: # pylint: disable=too-many-instance-attributes
|
||||||
""" Class representing gitlint configuration.
|
"""Class representing gitlint configuration.
|
||||||
Contains active config as well as number of methods to easily get/set the config.
|
Contains active config as well as number of methods to easily get/set the config.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Default tuple of rule classes (tuple because immutable).
|
# Default tuple of rule classes (tuple because immutable).
|
||||||
default_rule_classes = (rules.IgnoreByTitle,
|
default_rule_classes = (
|
||||||
rules.IgnoreByBody,
|
rules.IgnoreByTitle,
|
||||||
rules.IgnoreBodyLines,
|
rules.IgnoreByBody,
|
||||||
rules.IgnoreByAuthorName,
|
rules.IgnoreBodyLines,
|
||||||
rules.TitleMaxLength,
|
rules.IgnoreByAuthorName,
|
||||||
rules.TitleTrailingWhitespace,
|
rules.TitleMaxLength,
|
||||||
rules.TitleLeadingWhitespace,
|
rules.TitleTrailingWhitespace,
|
||||||
rules.TitleTrailingPunctuation,
|
rules.TitleLeadingWhitespace,
|
||||||
rules.TitleHardTab,
|
rules.TitleTrailingPunctuation,
|
||||||
rules.TitleMustNotContainWord,
|
rules.TitleHardTab,
|
||||||
rules.TitleRegexMatches,
|
rules.TitleMustNotContainWord,
|
||||||
rules.TitleMinLength,
|
rules.TitleRegexMatches,
|
||||||
rules.BodyMaxLineLength,
|
rules.TitleMinLength,
|
||||||
rules.BodyMinLength,
|
rules.BodyMaxLineLength,
|
||||||
rules.BodyMissing,
|
rules.BodyMinLength,
|
||||||
rules.BodyTrailingWhitespace,
|
rules.BodyMissing,
|
||||||
rules.BodyHardTab,
|
rules.BodyTrailingWhitespace,
|
||||||
rules.BodyFirstLineEmpty,
|
rules.BodyHardTab,
|
||||||
rules.BodyChangedFileMention,
|
rules.BodyFirstLineEmpty,
|
||||||
rules.BodyRegexMatches,
|
rules.BodyChangedFileMention,
|
||||||
rules.AuthorValidEmail)
|
rules.BodyRegexMatches,
|
||||||
|
rules.AuthorValidEmail,
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.rules = RuleCollection(self.default_rule_classes)
|
self.rules = RuleCollection(self.default_rule_classes)
|
||||||
self._verbosity = options.IntOption('verbosity', 3, "Verbosity")
|
self._verbosity = options.IntOption("verbosity", 3, "Verbosity")
|
||||||
self._ignore_merge_commits = options.BoolOption('ignore-merge-commits', True, "Ignore merge commits")
|
self._ignore_merge_commits = options.BoolOption("ignore-merge-commits", True, "Ignore merge commits")
|
||||||
self._ignore_fixup_commits = options.BoolOption('ignore-fixup-commits', True, "Ignore fixup commits")
|
self._ignore_fixup_commits = options.BoolOption("ignore-fixup-commits", True, "Ignore fixup commits")
|
||||||
self._ignore_squash_commits = options.BoolOption('ignore-squash-commits', True, "Ignore squash commits")
|
self._ignore_fixup_amend_commits = options.BoolOption(
|
||||||
self._ignore_revert_commits = options.BoolOption('ignore-revert-commits', True, "Ignore revert commits")
|
"ignore-fixup-amend-commits", True, "Ignore fixup amend commits"
|
||||||
self._debug = options.BoolOption('debug', False, "Enable debug mode")
|
)
|
||||||
|
self._ignore_squash_commits = options.BoolOption("ignore-squash-commits", True, "Ignore squash commits")
|
||||||
|
self._ignore_revert_commits = options.BoolOption("ignore-revert-commits", True, "Ignore revert commits")
|
||||||
|
self._debug = options.BoolOption("debug", False, "Enable debug mode")
|
||||||
self._extra_path = None
|
self._extra_path = None
|
||||||
target_description = "Path of the target git repository (default=current working directory)"
|
target_description = "Path of the target git repository (default=current working directory)"
|
||||||
self._target = options.PathOption('target', os.path.realpath(os.getcwd()), target_description)
|
self._target = options.PathOption("target", os.path.realpath(os.getcwd()), target_description)
|
||||||
self._ignore = options.ListOption('ignore', [], 'List of rule-ids to ignore')
|
self._ignore = options.ListOption("ignore", [], "List of rule-ids to ignore")
|
||||||
self._contrib = options.ListOption('contrib', [], 'List of contrib-rules to enable')
|
self._contrib = options.ListOption("contrib", [], "List of contrib-rules to enable")
|
||||||
self._config_path = None
|
self._config_path = None
|
||||||
ignore_stdin_description = "Ignore any stdin data. Useful for running in CI server."
|
ignore_stdin_description = "Ignore any stdin data. Useful for running in CI server."
|
||||||
self._ignore_stdin = options.BoolOption('ignore-stdin', False, ignore_stdin_description)
|
self._ignore_stdin = options.BoolOption("ignore-stdin", False, ignore_stdin_description)
|
||||||
self._staged = options.BoolOption('staged', False, "Read staged commit meta-info from the local repository.")
|
self._staged = options.BoolOption("staged", False, "Read staged commit meta-info from the local repository.")
|
||||||
self._fail_without_commits = options.BoolOption('fail-without-commits', False,
|
self._fail_without_commits = options.BoolOption(
|
||||||
"Hard fail when the target commit range is empty")
|
"fail-without-commits", False, "Hard fail when the target commit range is empty"
|
||||||
|
)
|
||||||
|
self._regex_style_search = options.BoolOption(
|
||||||
|
"regex-style-search", False, "Use `search` instead of `match` semantics for regex rules"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target(self):
|
def target(self):
|
||||||
|
@ -118,6 +126,15 @@ class LintConfig:
|
||||||
def ignore_fixup_commits(self, value):
|
def ignore_fixup_commits(self, value):
|
||||||
return self._ignore_fixup_commits.set(value)
|
return self._ignore_fixup_commits.set(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ignore_fixup_amend_commits(self):
|
||||||
|
return self._ignore_fixup_amend_commits.value
|
||||||
|
|
||||||
|
@ignore_fixup_amend_commits.setter
|
||||||
|
@handle_option_error
|
||||||
|
def ignore_fixup_amend_commits(self, value):
|
||||||
|
return self._ignore_fixup_amend_commits.set(value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ignore_squash_commits(self):
|
def ignore_squash_commits(self):
|
||||||
return self._ignore_squash_commits.value
|
return self._ignore_squash_commits.value
|
||||||
|
@ -182,6 +199,15 @@ class LintConfig:
|
||||||
def fail_without_commits(self, value):
|
def fail_without_commits(self, value):
|
||||||
return self._fail_without_commits.set(value)
|
return self._fail_without_commits.set(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def regex_style_search(self):
|
||||||
|
return self._regex_style_search.value
|
||||||
|
|
||||||
|
@regex_style_search.setter
|
||||||
|
@handle_option_error
|
||||||
|
def regex_style_search(self, value):
|
||||||
|
return self._regex_style_search.set(value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_path(self):
|
def extra_path(self):
|
||||||
return self._extra_path.value if self._extra_path else None
|
return self._extra_path.value if self._extra_path else None
|
||||||
|
@ -193,9 +219,7 @@ class LintConfig:
|
||||||
self._extra_path.set(value)
|
self._extra_path.set(value)
|
||||||
else:
|
else:
|
||||||
self._extra_path = options.PathOption(
|
self._extra_path = options.PathOption(
|
||||||
'extra-path', value,
|
"extra-path", value, "Path to a directory or module with extra user-defined rules", type="both"
|
||||||
"Path to a directory or module with extra user-defined rules",
|
|
||||||
type='both'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Make sure we unload any previously loaded extra-path rules
|
# Make sure we unload any previously loaded extra-path rules
|
||||||
|
@ -203,7 +227,7 @@ class LintConfig:
|
||||||
|
|
||||||
# Find rules in the new extra-path and add them to the existing rules
|
# Find rules in the new extra-path and add them to the existing rules
|
||||||
rule_classes = rule_finder.find_rule_classes(self.extra_path)
|
rule_classes = rule_finder.find_rule_classes(self.extra_path)
|
||||||
self.rules.add_rules(rule_classes, {'is_user_defined': True})
|
self.rules.add_rules(rule_classes, {"is_user_defined": True})
|
||||||
|
|
||||||
except (options.RuleOptionError, rules.UserRuleError) as e:
|
except (options.RuleOptionError, rules.UserRuleError) as e:
|
||||||
raise LintConfigError(str(e)) from e
|
raise LintConfigError(str(e)) from e
|
||||||
|
@ -226,12 +250,11 @@ class LintConfig:
|
||||||
|
|
||||||
# For each specified contrib rule, check whether it exists among the contrib classes
|
# For each specified contrib rule, check whether it exists among the contrib classes
|
||||||
for rule_id_or_name in self.contrib:
|
for rule_id_or_name in self.contrib:
|
||||||
rule_class = next((rc for rc in rule_classes if
|
rule_class = next((rc for rc in rule_classes if rule_id_or_name in (rc.id, rc.name)), False)
|
||||||
rule_id_or_name in (rc.id, rc.name)), False)
|
|
||||||
|
|
||||||
# If contrib rule exists, instantiate it and add it to the rules list
|
# If contrib rule exists, instantiate it and add it to the rules list
|
||||||
if rule_class:
|
if rule_class:
|
||||||
self.rules.add_rule(rule_class, rule_class.id, {'is_contrib': True})
|
self.rules.add_rule(rule_class, rule_class.id, {"is_contrib": True})
|
||||||
else:
|
else:
|
||||||
raise LintConfigError(f"No contrib rule with id or name '{rule_id_or_name}' found.")
|
raise LintConfigError(f"No contrib rule with id or name '{rule_id_or_name}' found.")
|
||||||
|
|
||||||
|
@ -250,14 +273,14 @@ class LintConfig:
|
||||||
return option
|
return option
|
||||||
|
|
||||||
def get_rule_option(self, rule_name_or_id, option_name):
|
def get_rule_option(self, rule_name_or_id, option_name):
|
||||||
""" Returns the value of a given option for a given rule. LintConfigErrors will be raised if the
|
"""Returns the value of a given option for a given rule. LintConfigErrors will be raised if the
|
||||||
rule or option don't exist. """
|
rule or option don't exist."""
|
||||||
option = self._get_option(rule_name_or_id, option_name)
|
option = self._get_option(rule_name_or_id, option_name)
|
||||||
return option.value
|
return option.value
|
||||||
|
|
||||||
def set_rule_option(self, rule_name_or_id, option_name, option_value):
|
def set_rule_option(self, rule_name_or_id, option_name, option_value):
|
||||||
""" Attempts to set a given value for a given option for a given rule.
|
"""Attempts to set a given value for a given option for a given rule.
|
||||||
LintConfigErrors will be raised if the rule or option don't exist or if the value is invalid. """
|
LintConfigErrors will be raised if the rule or option don't exist or if the value is invalid."""
|
||||||
option = self._get_option(rule_name_or_id, option_name)
|
option = self._get_option(rule_name_or_id, option_name)
|
||||||
try:
|
try:
|
||||||
option.set(option_value)
|
option.set(option_value)
|
||||||
|
@ -275,45 +298,53 @@ class LintConfig:
|
||||||
setattr(self, attr_name, option_value)
|
setattr(self, attr_name, option_value)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return isinstance(other, LintConfig) and \
|
return (
|
||||||
self.rules == other.rules and \
|
isinstance(other, LintConfig)
|
||||||
self.verbosity == other.verbosity and \
|
and self.rules == other.rules
|
||||||
self.target == other.target and \
|
and self.verbosity == other.verbosity
|
||||||
self.extra_path == other.extra_path and \
|
and self.target == other.target
|
||||||
self.contrib == other.contrib and \
|
and self.extra_path == other.extra_path
|
||||||
self.ignore_merge_commits == other.ignore_merge_commits and \
|
and self.contrib == other.contrib
|
||||||
self.ignore_fixup_commits == other.ignore_fixup_commits and \
|
and self.ignore_merge_commits == other.ignore_merge_commits
|
||||||
self.ignore_squash_commits == other.ignore_squash_commits and \
|
and self.ignore_fixup_commits == other.ignore_fixup_commits
|
||||||
self.ignore_revert_commits == other.ignore_revert_commits and \
|
and self.ignore_fixup_amend_commits == other.ignore_fixup_amend_commits
|
||||||
self.ignore_stdin == other.ignore_stdin and \
|
and self.ignore_squash_commits == other.ignore_squash_commits
|
||||||
self.staged == other.staged and \
|
and self.ignore_revert_commits == other.ignore_revert_commits
|
||||||
self.fail_without_commits == other.fail_without_commits and \
|
and self.ignore_stdin == other.ignore_stdin
|
||||||
self.debug == other.debug and \
|
and self.staged == other.staged
|
||||||
self.ignore == other.ignore and \
|
and self.fail_without_commits == other.fail_without_commits
|
||||||
self._config_path == other._config_path # noqa
|
and self.regex_style_search == other.regex_style_search
|
||||||
|
and self.debug == other.debug
|
||||||
|
and self.ignore == other.ignore
|
||||||
|
and self._config_path == other._config_path
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
# config-path is not a user exposed variable, so don't print it under the general section
|
# config-path is not a user exposed variable, so don't print it under the general section
|
||||||
return (f"config-path: {self._config_path}\n"
|
return (
|
||||||
f"[GENERAL]\n"
|
f"config-path: {self._config_path}\n"
|
||||||
f"extra-path: {self.extra_path}\n"
|
"[GENERAL]\n"
|
||||||
f"contrib: {self.contrib}\n"
|
f"extra-path: {self.extra_path}\n"
|
||||||
f"ignore: {','.join(self.ignore)}\n"
|
f"contrib: {self.contrib}\n"
|
||||||
f"ignore-merge-commits: {self.ignore_merge_commits}\n"
|
f"ignore: {','.join(self.ignore)}\n"
|
||||||
f"ignore-fixup-commits: {self.ignore_fixup_commits}\n"
|
f"ignore-merge-commits: {self.ignore_merge_commits}\n"
|
||||||
f"ignore-squash-commits: {self.ignore_squash_commits}\n"
|
f"ignore-fixup-commits: {self.ignore_fixup_commits}\n"
|
||||||
f"ignore-revert-commits: {self.ignore_revert_commits}\n"
|
f"ignore-fixup-amend-commits: {self.ignore_fixup_amend_commits}\n"
|
||||||
f"ignore-stdin: {self.ignore_stdin}\n"
|
f"ignore-squash-commits: {self.ignore_squash_commits}\n"
|
||||||
f"staged: {self.staged}\n"
|
f"ignore-revert-commits: {self.ignore_revert_commits}\n"
|
||||||
f"fail-without-commits: {self.fail_without_commits}\n"
|
f"ignore-stdin: {self.ignore_stdin}\n"
|
||||||
f"verbosity: {self.verbosity}\n"
|
f"staged: {self.staged}\n"
|
||||||
f"debug: {self.debug}\n"
|
f"fail-without-commits: {self.fail_without_commits}\n"
|
||||||
f"target: {self.target}\n"
|
f"regex-style-search: {self.regex_style_search}\n"
|
||||||
f"[RULES]\n{self.rules}")
|
f"verbosity: {self.verbosity}\n"
|
||||||
|
f"debug: {self.debug}\n"
|
||||||
|
f"target: {self.target}\n"
|
||||||
|
f"[RULES]\n{self.rules}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RuleCollection:
|
class RuleCollection:
|
||||||
""" Class representing an ordered list of rules. Methods are provided to easily retrieve, add or delete rules. """
|
"""Class representing an ordered list of rules. Methods are provided to easily retrieve, add or delete rules."""
|
||||||
|
|
||||||
def __init__(self, rule_classes=None, rule_attrs=None):
|
def __init__(self, rule_classes=None, rule_attrs=None):
|
||||||
# Use an ordered dict so that the order in which rules are applied is always the same
|
# Use an ordered dict so that the order in which rules are applied is always the same
|
||||||
|
@ -329,13 +360,13 @@ class RuleCollection:
|
||||||
return rule
|
return rule
|
||||||
|
|
||||||
def add_rule(self, rule_class, rule_id, rule_attrs=None):
|
def add_rule(self, rule_class, rule_id, rule_attrs=None):
|
||||||
""" Instantiates and adds a rule to RuleCollection.
|
"""Instantiates and adds a rule to RuleCollection.
|
||||||
Note: There can be multiple instantiations of the same rule_class in the RuleCollection, as long as the
|
Note: There can be multiple instantiations of the same rule_class in the RuleCollection, as long as the
|
||||||
rule_id is unique.
|
rule_id is unique.
|
||||||
:param rule_class python class representing the rule
|
:param rule_class python class representing the rule
|
||||||
:param rule_id unique identifier for the rule. If not unique, it will
|
:param rule_id unique identifier for the rule. If not unique, it will
|
||||||
overwrite the existing rule with that id
|
overwrite the existing rule with that id
|
||||||
:param rule_attrs dictionary of attributes to set on the instantiated rule obj
|
:param rule_attrs dictionary of attributes to set on the instantiated rule obj
|
||||||
"""
|
"""
|
||||||
rule_obj = rule_class()
|
rule_obj = rule_class()
|
||||||
rule_obj.id = rule_id
|
rule_obj.id = rule_id
|
||||||
|
@ -345,12 +376,12 @@ class RuleCollection:
|
||||||
self._rules[rule_obj.id] = rule_obj
|
self._rules[rule_obj.id] = rule_obj
|
||||||
|
|
||||||
def add_rules(self, rule_classes, rule_attrs=None):
|
def add_rules(self, rule_classes, rule_attrs=None):
|
||||||
""" Convenience method to add multiple rules at once based on a list of rule classes. """
|
"""Convenience method to add multiple rules at once based on a list of rule classes."""
|
||||||
for rule_class in rule_classes:
|
for rule_class in rule_classes:
|
||||||
self.add_rule(rule_class, rule_class.id, rule_attrs)
|
self.add_rule(rule_class, rule_class.id, rule_attrs)
|
||||||
|
|
||||||
def delete_rules_by_attr(self, attr_name, attr_val):
|
def delete_rules_by_attr(self, attr_name, attr_val):
|
||||||
""" Deletes all rules from the collection that match a given attribute name and value """
|
"""Deletes all rules from the collection that match a given attribute name and value"""
|
||||||
# Create a new list based on _rules.values() because in python 3, values() is a ValuesView as opposed to a list
|
# Create a new list based on _rules.values() because in python 3, values() is a ValuesView as opposed to a list
|
||||||
# This means you can't modify the ValueView while iterating over it.
|
# This means you can't modify the ValueView while iterating over it.
|
||||||
for rule in [r for r in self._rules.values()]: # pylint: disable=unnecessary-comprehension
|
for rule in [r for r in self._rules.values()]: # pylint: disable=unnecessary-comprehension
|
||||||
|
@ -358,8 +389,7 @@ class RuleCollection:
|
||||||
del self._rules[rule.id]
|
del self._rules[rule.id]
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
for rule in self._rules.values():
|
yield from self._rules.values()
|
||||||
yield rule
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return isinstance(other, RuleCollection) and self._rules == other._rules
|
return isinstance(other, RuleCollection) and self._rules == other._rules
|
||||||
|
@ -385,7 +415,7 @@ class RuleCollection:
|
||||||
|
|
||||||
|
|
||||||
class LintConfigBuilder:
|
class LintConfigBuilder:
|
||||||
""" Factory class that can build gitlint config.
|
"""Factory class that can build gitlint config.
|
||||||
This is primarily useful to deal with complex configuration scenarios where configuration can be set and overridden
|
This is primarily useful to deal with complex configuration scenarios where configuration can be set and overridden
|
||||||
from various sources (typically according to certain precedence rules) before the actual config should be
|
from various sources (typically according to certain precedence rules) before the actual config should be
|
||||||
normalized, validated and build. Example usage can be found in gitlint.cli.
|
normalized, validated and build. Example usage can be found in gitlint.cli.
|
||||||
|
@ -403,19 +433,19 @@ class LintConfigBuilder:
|
||||||
self._config_blueprint[section][option_name] = option_value
|
self._config_blueprint[section][option_name] = option_value
|
||||||
|
|
||||||
def set_config_from_commit(self, commit):
|
def set_config_from_commit(self, commit):
|
||||||
""" Given a git commit, applies config specified in the commit message.
|
"""Given a git commit, applies config specified in the commit message.
|
||||||
Supported:
|
Supported:
|
||||||
- gitlint-ignore: all
|
- gitlint-ignore: all
|
||||||
"""
|
"""
|
||||||
for line in commit.message.body:
|
for line in commit.message.body:
|
||||||
pattern = re.compile(r"^gitlint-ignore:\s*(.*)")
|
pattern = re.compile(r"^gitlint-ignore:\s*(.*)")
|
||||||
matches = pattern.match(line)
|
matches = pattern.match(line)
|
||||||
if matches and len(matches.groups()) == 1:
|
if matches and len(matches.groups()) == 1:
|
||||||
self.set_option('general', 'ignore', matches.group(1))
|
self.set_option("general", "ignore", matches.group(1))
|
||||||
|
|
||||||
def set_config_from_string_list(self, config_options):
|
def set_config_from_string_list(self, config_options):
|
||||||
""" Given a list of config options of the form "<rule>.<option>=<value>", parses out the correct rule and option
|
"""Given a list of config options of the form "<rule>.<option>=<value>", parses out the correct rule and option
|
||||||
and sets the value accordingly in this factory object. """
|
and sets the value accordingly in this factory object."""
|
||||||
for config_option in config_options:
|
for config_option in config_options:
|
||||||
try:
|
try:
|
||||||
config_name, option_value = config_option.split("=", 1)
|
config_name, option_value = config_option.split("=", 1)
|
||||||
|
@ -425,17 +455,18 @@ class LintConfigBuilder:
|
||||||
self.set_option(rule_name, option_name, option_value)
|
self.set_option(rule_name, option_name, option_value)
|
||||||
except ValueError as e: # raised if the config string is invalid
|
except ValueError as e: # raised if the config string is invalid
|
||||||
raise LintConfigError(
|
raise LintConfigError(
|
||||||
f"'{config_option}' is an invalid configuration option. Use '<rule>.<option>=<value>'") from e
|
f"'{config_option}' is an invalid configuration option. Use '<rule>.<option>=<value>'"
|
||||||
|
) from e
|
||||||
|
|
||||||
def set_from_config_file(self, filename):
|
def set_from_config_file(self, filename):
|
||||||
""" Loads lint config from an ini-style config file """
|
"""Loads lint config from an ini-style config file"""
|
||||||
if not os.path.exists(filename):
|
if not os.path.exists(filename):
|
||||||
raise LintConfigError(f"Invalid file path: {filename}")
|
raise LintConfigError(f"Invalid file path: {filename}")
|
||||||
self._config_path = os.path.realpath(filename)
|
self._config_path = os.path.realpath(filename)
|
||||||
try:
|
try:
|
||||||
parser = ConfigParser()
|
parser = ConfigParser()
|
||||||
|
|
||||||
with io.open(filename, encoding=DEFAULT_ENCODING) as config_file:
|
with open(filename, encoding=DEFAULT_ENCODING) as config_file:
|
||||||
parser.read_file(config_file, filename)
|
parser.read_file(config_file, filename)
|
||||||
|
|
||||||
for section_name in parser.sections():
|
for section_name in parser.sections():
|
||||||
|
@ -446,8 +477,8 @@ class LintConfigBuilder:
|
||||||
raise LintConfigError(str(e)) from e
|
raise LintConfigError(str(e)) from e
|
||||||
|
|
||||||
def _add_named_rule(self, config, qualified_rule_name):
|
def _add_named_rule(self, config, qualified_rule_name):
|
||||||
""" Adds a Named Rule to a given LintConfig object.
|
"""Adds a Named Rule to a given LintConfig object.
|
||||||
IMPORTANT: This method does *NOT* overwrite existing Named Rules with the same canonical id.
|
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,
|
# Split up named rule in its parts: the name/id that specifies the parent rule,
|
||||||
|
@ -475,13 +506,13 @@ class LintConfigBuilder:
|
||||||
|
|
||||||
# Add the rule to the collection of rules if it's not there already
|
# Add the rule to the collection of rules if it's not there already
|
||||||
if not config.rules.find_rule(canonical_id):
|
if not config.rules.find_rule(canonical_id):
|
||||||
config.rules.add_rule(parent_rule.__class__, canonical_id, {'is_named': True, 'name': canonical_name})
|
config.rules.add_rule(parent_rule.__class__, canonical_id, {"is_named": True, "name": canonical_name})
|
||||||
|
|
||||||
return canonical_id
|
return canonical_id
|
||||||
|
|
||||||
def build(self, config=None):
|
def build(self, config=None):
|
||||||
""" Build a real LintConfig object by normalizing and validating the options that were previously set on this
|
"""Build a real LintConfig object by normalizing and validating the options that were previously set on this
|
||||||
factory. """
|
factory."""
|
||||||
# If we are passed a config object, then rebuild that object instead of building a new lintconfig object from
|
# If we are passed a config object, then rebuild that object instead of building a new lintconfig object from
|
||||||
# scratch
|
# scratch
|
||||||
if not config:
|
if not config:
|
||||||
|
@ -490,7 +521,7 @@ class LintConfigBuilder:
|
||||||
config._config_path = self._config_path
|
config._config_path = self._config_path
|
||||||
|
|
||||||
# Set general options first as this might change the behavior or validity of the other options
|
# Set general options first as this might change the behavior or validity of the other options
|
||||||
general_section = self._config_blueprint.get('general')
|
general_section = self._config_blueprint.get("general")
|
||||||
if general_section:
|
if general_section:
|
||||||
for option_name, option_value in general_section.items():
|
for option_name, option_value in general_section.items():
|
||||||
config.set_general_option(option_name, option_value)
|
config.set_general_option(option_name, option_value)
|
||||||
|
@ -499,7 +530,6 @@ class LintConfigBuilder:
|
||||||
for option_name, option_value in section_dict.items():
|
for option_name, option_value in section_dict.items():
|
||||||
# Skip over the general section, as we've already done that above
|
# Skip over the general section, as we've already done that above
|
||||||
if section_name != "general":
|
if section_name != "general":
|
||||||
|
|
||||||
# If the section name contains a colon (:), then this section is defining a Named Rule
|
# 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.
|
# Which means we need to instantiate that Named Rule in the config.
|
||||||
if self.RULE_QUALIFIER_SYMBOL in section_name:
|
if self.RULE_QUALIFIER_SYMBOL in section_name:
|
||||||
|
@ -510,7 +540,7 @@ class LintConfigBuilder:
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def clone(self):
|
def clone(self):
|
||||||
""" Creates an exact copy of a LintConfigBuilder. """
|
"""Creates an exact copy of a LintConfigBuilder."""
|
||||||
builder = LintConfigBuilder()
|
builder = LintConfigBuilder()
|
||||||
builder._config_blueprint = copy.deepcopy(self._config_blueprint)
|
builder._config_blueprint = copy.deepcopy(self._config_blueprint)
|
||||||
builder._config_path = self._config_path
|
builder._config_path = self._config_path
|
||||||
|
@ -523,6 +553,6 @@ GITLINT_CONFIG_TEMPLATE_SRC_PATH = os.path.join(os.path.dirname(os.path.realpath
|
||||||
class LintConfigGenerator:
|
class LintConfigGenerator:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_config(dest):
|
def generate_config(dest):
|
||||||
""" Generates a gitlint config file at the given destination location.
|
"""Generates a gitlint config file at the given destination location.
|
||||||
Expects that the given ```dest``` points to a valid destination. """
|
Expects that the given ```dest``` points to a valid destination."""
|
||||||
shutil.copyfile(GITLINT_CONFIG_TEMPLATE_SRC_PATH, dest)
|
shutil.copyfile(GITLINT_CONFIG_TEMPLATE_SRC_PATH, dest)
|
||||||
|
|
46
gitlint-core/gitlint/contrib/rules/authors_commit.py
Normal file
46
gitlint-core/gitlint/contrib/rules/authors_commit.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
|
||||||
|
from gitlint.rules import CommitRule, RuleViolation
|
||||||
|
|
||||||
|
|
||||||
|
class AllowedAuthors(CommitRule):
|
||||||
|
"""Enforce that only authors listed in the AUTHORS file are allowed to commit."""
|
||||||
|
|
||||||
|
authors_file_names = ("AUTHORS", "AUTHORS.txt", "AUTHORS.md")
|
||||||
|
parse_authors = re.compile(r"^(?P<name>.*) <(?P<email>.*)>$", re.MULTILINE)
|
||||||
|
|
||||||
|
name = "contrib-allowed-authors"
|
||||||
|
|
||||||
|
id = "CC3"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _read_authors_from_file(cls, git_ctx) -> Tuple[str, str]:
|
||||||
|
for file_name in cls.authors_file_names:
|
||||||
|
path = Path(git_ctx.repository_path) / file_name
|
||||||
|
if path.exists():
|
||||||
|
authors_file = path
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise FileNotFoundError("No AUTHORS file found!")
|
||||||
|
|
||||||
|
authors_file_content = authors_file.read_text("utf-8")
|
||||||
|
authors = re.findall(cls.parse_authors, authors_file_content)
|
||||||
|
|
||||||
|
return set(authors), authors_file.name
|
||||||
|
|
||||||
|
def validate(self, commit):
|
||||||
|
registered_authors, authors_file_name = AllowedAuthors._read_authors_from_file(commit.message.context)
|
||||||
|
|
||||||
|
author = (commit.author_name, commit.author_email.lower())
|
||||||
|
|
||||||
|
if author not in registered_authors:
|
||||||
|
return [
|
||||||
|
RuleViolation(
|
||||||
|
self.id,
|
||||||
|
f"Author not in '{authors_file_name}' file: " f'"{commit.author_name} <{commit.author_email}>"',
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return []
|
|
@ -7,7 +7,7 @@ RULE_REGEX = re.compile(r"([^(]+?)(\([^)]+?\))?!?: .+")
|
||||||
|
|
||||||
|
|
||||||
class ConventionalCommit(LineRule):
|
class ConventionalCommit(LineRule):
|
||||||
""" This rule enforces the spec at https://www.conventionalcommits.org/. """
|
"""This rule enforces the spec at https://www.conventionalcommits.org/."""
|
||||||
|
|
||||||
name = "contrib-title-conventional-commits"
|
name = "contrib-title-conventional-commits"
|
||||||
id = "CT1"
|
id = "CT1"
|
||||||
|
@ -31,7 +31,7 @@ class ConventionalCommit(LineRule):
|
||||||
else:
|
else:
|
||||||
line_commit_type = match.group(1)
|
line_commit_type = match.group(1)
|
||||||
if line_commit_type not in self.options["types"].value:
|
if line_commit_type not in self.options["types"].value:
|
||||||
opt_str = ', '.join(self.options['types'].value)
|
opt_str = ", ".join(self.options["types"].value)
|
||||||
violations.append(RuleViolation(self.id, f"Title does not start with one of {opt_str}", line))
|
violations.append(RuleViolation(self.id, f"Title does not start with one of {opt_str}", line))
|
||||||
|
|
||||||
return violations
|
return violations
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
from gitlint.rules import CommitRule, RuleViolation
|
||||||
|
|
||||||
|
|
||||||
|
class DisallowCleanupCommits(CommitRule):
|
||||||
|
"""This rule checks the commits for "fixup!"/"squash!"/"amend!" commits
|
||||||
|
and rejects them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "contrib-disallow-cleanup-commits"
|
||||||
|
id = "CC2"
|
||||||
|
|
||||||
|
def validate(self, commit):
|
||||||
|
if commit.is_fixup_commit:
|
||||||
|
return [RuleViolation(self.id, "Fixup commits are not allowed", line_nr=1)]
|
||||||
|
|
||||||
|
if commit.is_squash_commit:
|
||||||
|
return [RuleViolation(self.id, "Squash commits are not allowed", line_nr=1)]
|
||||||
|
|
||||||
|
if commit.is_fixup_amend_commit:
|
||||||
|
return [RuleViolation(self.id, "Amend commits are not allowed", line_nr=1)]
|
||||||
|
|
||||||
|
return []
|
|
@ -1,9 +1,8 @@
|
||||||
|
|
||||||
from gitlint.rules import CommitRule, RuleViolation
|
from gitlint.rules import CommitRule, RuleViolation
|
||||||
|
|
||||||
|
|
||||||
class SignedOffBy(CommitRule):
|
class SignedOffBy(CommitRule):
|
||||||
""" This rule will enforce that each commit body contains a "Signed-off-by" line.
|
"""This rule will enforce that each commit body contains a "Signed-off-by" line.
|
||||||
We keep things simple here and just check whether the commit body contains a line that starts with "Signed-off-by".
|
We keep things simple here and just check whether the commit body contains a line that starts with "Signed-off-by".
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
40
gitlint-core/gitlint/deprecation.py
Normal file
40
gitlint-core/gitlint/deprecation.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger("gitlint.deprecated")
|
||||||
|
DEPRECATED_LOG_FORMAT = "%(levelname)s: %(message)s"
|
||||||
|
|
||||||
|
|
||||||
|
class Deprecation:
|
||||||
|
"""Singleton class that handles deprecation warnings and behavior."""
|
||||||
|
|
||||||
|
# LintConfig class that is used to determine deprecation behavior
|
||||||
|
config = None
|
||||||
|
|
||||||
|
# Set of warning messages that have already been logged, to prevent duplicate warnings
|
||||||
|
warning_msgs = set()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_regex_method(cls, rule, regex_option):
|
||||||
|
"""Returns the regex method to be used for a given rule based on general.regex-style-search option.
|
||||||
|
Logs a warning if the deprecated re.match method is returned."""
|
||||||
|
|
||||||
|
# if general.regex-style-search is set, just return re.search
|
||||||
|
if cls.config.regex_style_search:
|
||||||
|
return regex_option.value.search
|
||||||
|
|
||||||
|
warning_msg = (
|
||||||
|
f"{rule.id} - {rule.name}: gitlint will be switching from using Python regex 'match' (match beginning) to "
|
||||||
|
"'search' (match anywhere) semantics. "
|
||||||
|
f"Please review your {rule.name}.regex option accordingly. "
|
||||||
|
"To remove this warning, set general.regex-style-search=True. "
|
||||||
|
"More details: https://jorisroovers.github.io/gitlint/configuration/#regex-style-search"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only log warnings once
|
||||||
|
if warning_msg not in cls.warning_msgs:
|
||||||
|
log = logging.getLogger("gitlint.deprecated.regex_style_search")
|
||||||
|
log.warning(warning_msg)
|
||||||
|
cls.warning_msgs.add(warning_msg)
|
||||||
|
|
||||||
|
return regex_option.value.match
|
|
@ -2,14 +2,14 @@ from sys import stdout, stderr
|
||||||
|
|
||||||
|
|
||||||
class Display:
|
class Display:
|
||||||
""" Utility class to print stuff to an output stream (stdout by default) based on the config's verbosity """
|
"""Utility class to print stuff to an output stream (stdout by default) based on the config's verbosity"""
|
||||||
|
|
||||||
def __init__(self, lint_config):
|
def __init__(self, lint_config):
|
||||||
self.config = lint_config
|
self.config = lint_config
|
||||||
|
|
||||||
def _output(self, message, verbosity, exact, stream):
|
def _output(self, message, verbosity, exact, stream):
|
||||||
""" Output a message if the config's verbosity is >= to the given verbosity. If exact == True, the message
|
"""Output a message if the config's verbosity is >= to the given verbosity. If exact == True, the message
|
||||||
will only be outputted if the given verbosity exactly matches the config's verbosity. """
|
will only be outputted if the given verbosity exactly matches the config's verbosity."""
|
||||||
if exact:
|
if exact:
|
||||||
if self.config.verbosity == verbosity:
|
if self.config.verbosity == verbosity:
|
||||||
stream.write(message + "\n")
|
stream.write(message + "\n")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
|
|
||||||
class GitlintError(Exception):
|
class GitlintError(Exception):
|
||||||
""" Based Exception class for all gitlint exceptions """
|
"""Based Exception class for all gitlint exceptions"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -14,13 +14,14 @@
|
||||||
# verbosity should be a value between 1 and 3, the commandline -v flags take precedence over this
|
# verbosity should be a value between 1 and 3, the commandline -v flags take precedence over this
|
||||||
# verbosity = 2
|
# verbosity = 2
|
||||||
|
|
||||||
# By default gitlint will ignore merge, revert, fixup and squash commits.
|
# By default gitlint will ignore merge, revert, fixup, fixup=amend, and squash commits.
|
||||||
# ignore-merge-commits=true
|
# ignore-merge-commits=true
|
||||||
# ignore-revert-commits=true
|
# ignore-revert-commits=true
|
||||||
# ignore-fixup-commits=true
|
# ignore-fixup-commits=true
|
||||||
|
# ignore-fixup-amend-commits=true
|
||||||
# ignore-squash-commits=true
|
# ignore-squash-commits=true
|
||||||
|
|
||||||
# Ignore any data send to gitlint via stdin
|
# Ignore any data sent to gitlint via stdin
|
||||||
# ignore-stdin=true
|
# ignore-stdin=true
|
||||||
|
|
||||||
# Fetch additional meta-data from the local repository when manually passing a
|
# Fetch additional meta-data from the local repository when manually passing a
|
||||||
|
@ -33,6 +34,11 @@
|
||||||
# Disabled by default.
|
# Disabled by default.
|
||||||
# fail-without-commits=true
|
# fail-without-commits=true
|
||||||
|
|
||||||
|
# Whether to use Python `search` instead of `match` semantics in rules that use
|
||||||
|
# regexes. Context: https://github.com/jorisroovers/gitlint/issues/254
|
||||||
|
# Disabled by default, but will be enabled by default in the future.
|
||||||
|
# regex-style-search=true
|
||||||
|
|
||||||
# Enable debug mode (prints more output). Disabled by default.
|
# Enable debug mode (prints more output). Disabled by default.
|
||||||
# debug=true
|
# debug=true
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
|
|
||||||
from gitlint import shell as sh
|
from gitlint import shell as sh
|
||||||
|
|
||||||
# import exceptions separately, this makes it a little easier to mock them out in the unit tests
|
# import exceptions separately, this makes it a little easier to mock them out in the unit tests
|
||||||
from gitlint.shell import CommandNotFound, ErrorReturnCode
|
from gitlint.shell import CommandNotFound, ErrorReturnCode
|
||||||
|
|
||||||
|
@ -18,15 +20,17 @@ LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class GitContextError(GitlintError):
|
class GitContextError(GitlintError):
|
||||||
""" Exception indicating there is an issue with the git context """
|
"""Exception indicating there is an issue with the git context"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class GitNotInstalledError(GitContextError):
|
class GitNotInstalledError(GitContextError):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
"'git' command not found. You need to install git to use gitlint on a local repository. " +
|
"'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.")
|
"See https://git-scm.com/book/en/v2/Getting-Started-Installing-Git on how to install git."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GitExitCodeError(GitContextError):
|
class GitExitCodeError(GitContextError):
|
||||||
|
@ -37,8 +41,8 @@ class GitExitCodeError(GitContextError):
|
||||||
|
|
||||||
|
|
||||||
def _git(*command_parts, **kwargs):
|
def _git(*command_parts, **kwargs):
|
||||||
""" Convenience function for running git commands. Automatically deals with exceptions and unicode. """
|
"""Convenience function for running git commands. Automatically deals with exceptions and unicode."""
|
||||||
git_kwargs = {'_tty_out': False}
|
git_kwargs = {"_tty_out": False}
|
||||||
git_kwargs.update(kwargs)
|
git_kwargs.update(kwargs)
|
||||||
try:
|
try:
|
||||||
LOG.debug(command_parts)
|
LOG.debug(command_parts)
|
||||||
|
@ -46,7 +50,7 @@ def _git(*command_parts, **kwargs):
|
||||||
# If we reach this point and the result has an exit_code that is larger than 0, this means that we didn't
|
# 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
|
# get an exception (which is the default sh behavior for non-zero exit codes) and so the user is expecting
|
||||||
# a non-zero exit code -> just return the entire result
|
# a non-zero exit code -> just return the entire result
|
||||||
if hasattr(result, 'exit_code') and result.exit_code > 0:
|
if hasattr(result, "exit_code") and result.exit_code > 0:
|
||||||
return result
|
return result
|
||||||
return str(result)
|
return str(result)
|
||||||
except CommandNotFound as e:
|
except CommandNotFound as e:
|
||||||
|
@ -54,11 +58,13 @@ def _git(*command_parts, **kwargs):
|
||||||
except ErrorReturnCode as e: # Something went wrong while executing the git command
|
except ErrorReturnCode as e: # Something went wrong while executing the git command
|
||||||
error_msg = e.stderr.strip()
|
error_msg = e.stderr.strip()
|
||||||
error_msg_lower = error_msg.lower()
|
error_msg_lower = error_msg.lower()
|
||||||
if '_cwd' in git_kwargs and b"not a git repository" in error_msg_lower:
|
if "_cwd" in git_kwargs and b"not a git repository" in error_msg_lower:
|
||||||
raise GitContextError(f"{git_kwargs['_cwd']} is not a git repository.") from e
|
raise GitContextError(f"{git_kwargs['_cwd']} is not a git repository.") from e
|
||||||
|
|
||||||
if (b"does not have any commits yet" in error_msg_lower or
|
if (
|
||||||
b"ambiguous argument 'head': unknown revision" in error_msg_lower):
|
b"does not have any commits yet" in error_msg_lower
|
||||||
|
or b"ambiguous argument 'head': unknown revision" in error_msg_lower
|
||||||
|
):
|
||||||
msg = "Current branch has no commits. Gitlint requires at least one commit to function."
|
msg = "Current branch has no commits. Gitlint requires at least one commit to function."
|
||||||
raise GitContextError(msg) from e
|
raise GitContextError(msg) from e
|
||||||
|
|
||||||
|
@ -66,34 +72,54 @@ def _git(*command_parts, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
def git_version():
|
def git_version():
|
||||||
""" Determine the git version installed on this host by calling git --version"""
|
"""Determine the git version installed on this host by calling git --version"""
|
||||||
return _git("--version").replace("\n", "")
|
return _git("--version").replace("\n", "")
|
||||||
|
|
||||||
|
|
||||||
def git_commentchar(repository_path=None):
|
def git_commentchar(repository_path=None):
|
||||||
""" Shortcut for retrieving comment char from git config """
|
"""Shortcut for retrieving comment char from git config"""
|
||||||
commentchar = _git("config", "--get", "core.commentchar", _cwd=repository_path, _ok_code=[0, 1])
|
commentchar = _git("config", "--get", "core.commentchar", _cwd=repository_path, _ok_code=[0, 1])
|
||||||
# git will return an exit code of 1 if it can't find a config value, in this case we fall-back to # as commentchar
|
# git will return an exit code of 1 if it can't find a config value, in this case we fall-back to # as commentchar
|
||||||
if hasattr(commentchar, 'exit_code') and commentchar.exit_code == 1: # pylint: disable=no-member
|
if hasattr(commentchar, "exit_code") and commentchar.exit_code == 1: # pylint: disable=no-member
|
||||||
commentchar = "#"
|
commentchar = "#"
|
||||||
return commentchar.replace("\n", "")
|
return commentchar.replace("\n", "")
|
||||||
|
|
||||||
|
|
||||||
def git_hooks_dir(repository_path):
|
def git_hooks_dir(repository_path):
|
||||||
""" Determine hooks directory for a given target dir """
|
"""Determine hooks directory for a given target dir"""
|
||||||
hooks_dir = _git("rev-parse", "--git-path", "hooks", _cwd=repository_path)
|
hooks_dir = _git("rev-parse", "--git-path", "hooks", _cwd=repository_path)
|
||||||
hooks_dir = hooks_dir.replace("\n", "")
|
hooks_dir = hooks_dir.replace("\n", "")
|
||||||
return os.path.realpath(os.path.join(repository_path, hooks_dir))
|
return os.path.realpath(os.path.join(repository_path, hooks_dir))
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_git_changed_file_stats(changed_files_stats_raw):
|
||||||
|
"""Parse the output of git diff --numstat and return a dict of:
|
||||||
|
dict[filename: GitChangedFileStats(filename, additions, deletions)]"""
|
||||||
|
changed_files_stats_lines = changed_files_stats_raw.split("\n")
|
||||||
|
changed_files_stats = {}
|
||||||
|
for line in changed_files_stats_lines[:-1]: # drop last empty line
|
||||||
|
line_stats = line.split()
|
||||||
|
|
||||||
|
# If the file is binary, numstat will show "-"
|
||||||
|
# See https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---numstat
|
||||||
|
additions = int(line_stats[0]) if line_stats[0] != "-" else None
|
||||||
|
deletions = int(line_stats[1]) if line_stats[1] != "-" else None
|
||||||
|
|
||||||
|
changed_file_stat = GitChangedFileStats(line_stats[2], additions, deletions)
|
||||||
|
changed_files_stats[line_stats[2]] = changed_file_stat
|
||||||
|
|
||||||
|
return changed_files_stats
|
||||||
|
|
||||||
|
|
||||||
class GitCommitMessage:
|
class GitCommitMessage:
|
||||||
""" Class representing a git commit message. A commit message consists of the following:
|
"""Class representing a git commit message. A commit message consists of the following:
|
||||||
- context: The `GitContext` this commit message is part of
|
- context: The `GitContext` this commit message is part of
|
||||||
- original: The actual commit message as returned by `git log`
|
- original: The actual commit message as returned by `git log`
|
||||||
- full: original, but stripped of any comments
|
- full: original, but stripped of any comments
|
||||||
- title: the first line of full
|
- title: the first line of full
|
||||||
- body: all lines following the title
|
- body: all lines following the title
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, context, original=None, full=None, title=None, body=None):
|
def __init__(self, context, original=None, full=None, title=None, body=None):
|
||||||
self.context = context
|
self.context = context
|
||||||
self.original = original
|
self.original = original
|
||||||
|
@ -103,7 +129,7 @@ class GitCommitMessage:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_full_message(context, commit_msg_str):
|
def from_full_message(context, commit_msg_str):
|
||||||
""" Parses a full git commit message by parsing a given string into the different parts of a commit message """
|
"""Parses a full git commit message by parsing a given string into the different parts of a commit message"""
|
||||||
all_lines = commit_msg_str.splitlines()
|
all_lines = commit_msg_str.splitlines()
|
||||||
cutline = f"{context.commentchar} ------------------------ >8 ------------------------"
|
cutline = f"{context.commentchar} ------------------------ >8 ------------------------"
|
||||||
try:
|
try:
|
||||||
|
@ -120,19 +146,59 @@ class GitCommitMessage:
|
||||||
return self.full
|
return self.full
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return (isinstance(other, GitCommitMessage) and self.original == other.original
|
return (
|
||||||
and self.full == other.full and self.title == other.title and self.body == other.body) # noqa
|
isinstance(other, GitCommitMessage)
|
||||||
|
and self.original == other.original
|
||||||
|
and self.full == other.full
|
||||||
|
and self.title == other.title
|
||||||
|
and self.body == other.body
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GitChangedFileStats:
|
||||||
|
"""Class representing the stats for a changed file in git"""
|
||||||
|
|
||||||
|
def __init__(self, filepath, additions, deletions):
|
||||||
|
self.filepath = Path(filepath)
|
||||||
|
self.additions = additions
|
||||||
|
self.deletions = deletions
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (
|
||||||
|
isinstance(other, GitChangedFileStats)
|
||||||
|
and self.filepath == other.filepath
|
||||||
|
and self.additions == other.additions
|
||||||
|
and self.deletions == other.deletions
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.filepath}: {self.additions} additions, {self.deletions} deletions"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f'GitChangedFileStats(filepath="{self.filepath}", additions={self.additions}, deletions={self.deletions})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GitCommit:
|
class GitCommit:
|
||||||
""" Class representing a git commit.
|
"""Class representing a git commit.
|
||||||
A commit consists of: context, message, author name, author email, date, list of parent commit shas,
|
A commit consists of: context, message, author name, author email, date, list of parent commit shas,
|
||||||
list of changed files, list of branch names.
|
list of changed files, list of branch names.
|
||||||
In the context of gitlint, only the git context and commit message are required.
|
In the context of gitlint, only the git context and commit message are required.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, context, message, sha=None, date=None, author_name=None, # pylint: disable=too-many-arguments
|
def __init__(
|
||||||
author_email=None, parents=None, changed_files=None, branches=None):
|
self,
|
||||||
|
context,
|
||||||
|
message,
|
||||||
|
sha=None,
|
||||||
|
date=None,
|
||||||
|
author_name=None, # pylint: disable=too-many-arguments
|
||||||
|
author_email=None,
|
||||||
|
parents=None,
|
||||||
|
changed_files_stats=None,
|
||||||
|
branches=None,
|
||||||
|
):
|
||||||
self.context = context
|
self.context = context
|
||||||
self.message = message
|
self.message = message
|
||||||
self.sha = sha
|
self.sha = sha
|
||||||
|
@ -140,7 +206,7 @@ class GitCommit:
|
||||||
self.author_name = author_name
|
self.author_name = author_name
|
||||||
self.author_email = author_email
|
self.author_email = author_email
|
||||||
self.parents = parents or [] # parent commit hashes
|
self.parents = parents or [] # parent commit hashes
|
||||||
self.changed_files = changed_files or []
|
self.changed_files_stats = changed_files_stats or {}
|
||||||
self.branches = branches or []
|
self.branches = branches or []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -155,57 +221,87 @@ class GitCommit:
|
||||||
def is_squash_commit(self):
|
def is_squash_commit(self):
|
||||||
return self.message.title.startswith("squash!")
|
return self.message.title.startswith("squash!")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_fixup_amend_commit(self):
|
||||||
|
return self.message.title.startswith("amend!")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_revert_commit(self):
|
def is_revert_commit(self):
|
||||||
return self.message.title.startswith("Revert")
|
return self.message.title.startswith("Revert")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def changed_files(self):
|
||||||
|
return list(self.changed_files_stats.keys())
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
date_str = arrow.get(self.date).format(GIT_TIMEFORMAT) if self.date else None
|
date_str = arrow.get(self.date).format(GIT_TIMEFORMAT) if self.date else None
|
||||||
return (f"--- Commit Message ----\n{self.message}\n"
|
|
||||||
"--- Meta info ---------\n"
|
if len(self.changed_files_stats) > 0:
|
||||||
f"Author: {self.author_name} <{self.author_email}>\n"
|
changed_files_stats_str = "\n " + "\n ".join([str(stats) for stats in self.changed_files_stats.values()])
|
||||||
f"Date: {date_str}\n"
|
else:
|
||||||
f"is-merge-commit: {self.is_merge_commit}\n"
|
changed_files_stats_str = " {}"
|
||||||
f"is-fixup-commit: {self.is_fixup_commit}\n"
|
|
||||||
f"is-squash-commit: {self.is_squash_commit}\n"
|
return (
|
||||||
f"is-revert-commit: {self.is_revert_commit}\n"
|
f"--- Commit Message ----\n{self.message}\n"
|
||||||
f"Branches: {self.branches}\n"
|
"--- Meta info ---------\n"
|
||||||
f"Changed Files: {self.changed_files}\n"
|
f"Author: {self.author_name} <{self.author_email}>\n"
|
||||||
"-----------------------")
|
f"Date: {date_str}\n"
|
||||||
|
f"is-merge-commit: {self.is_merge_commit}\n"
|
||||||
|
f"is-fixup-commit: {self.is_fixup_commit}\n"
|
||||||
|
f"is-fixup-amend-commit: {self.is_fixup_amend_commit}\n"
|
||||||
|
f"is-squash-commit: {self.is_squash_commit}\n"
|
||||||
|
f"is-revert-commit: {self.is_revert_commit}\n"
|
||||||
|
f"Parents: {self.parents}\n"
|
||||||
|
f"Branches: {self.branches}\n"
|
||||||
|
f"Changed Files: {self.changed_files}\n"
|
||||||
|
f"Changed Files Stats:{changed_files_stats_str}\n"
|
||||||
|
"-----------------------"
|
||||||
|
)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
# skip checking the context as context refers back to this obj, this will trigger a cyclic dependency
|
# skip checking the context as context refers back to this obj, this will trigger a cyclic dependency
|
||||||
return (isinstance(other, GitCommit) and self.message == other.message
|
return (
|
||||||
and self.sha == other.sha and self.author_name == other.author_name
|
isinstance(other, GitCommit)
|
||||||
and self.author_email == other.author_email
|
and self.message == other.message
|
||||||
and self.date == other.date and self.parents == other.parents
|
and self.sha == other.sha
|
||||||
and self.is_merge_commit == other.is_merge_commit and self.is_fixup_commit == other.is_fixup_commit
|
and self.author_name == other.author_name
|
||||||
and self.is_squash_commit == other.is_squash_commit and self.is_revert_commit == other.is_revert_commit
|
and self.author_email == other.author_email
|
||||||
and self.changed_files == other.changed_files and self.branches == other.branches) # noqa
|
and self.date == other.date
|
||||||
|
and self.parents == other.parents
|
||||||
|
and self.is_merge_commit == other.is_merge_commit
|
||||||
|
and self.is_fixup_commit == other.is_fixup_commit
|
||||||
|
and self.is_fixup_amend_commit == other.is_fixup_amend_commit
|
||||||
|
and self.is_squash_commit == other.is_squash_commit
|
||||||
|
and self.is_revert_commit == other.is_revert_commit
|
||||||
|
and self.changed_files == other.changed_files
|
||||||
|
and self.changed_files_stats == other.changed_files_stats
|
||||||
|
and self.branches == other.branches
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LocalGitCommit(GitCommit, PropertyCache):
|
class LocalGitCommit(GitCommit, PropertyCache):
|
||||||
""" Class representing a git commit that exists in the local git repository.
|
"""Class representing a git commit that exists in the local git repository.
|
||||||
This class uses lazy loading: it defers reading information from the local git repository until the associated
|
This class uses lazy loading: it defers reading information from the local git repository until the associated
|
||||||
property is accessed for the first time. Properties are then cached for subsequent access.
|
property is accessed for the first time. Properties are then cached for subsequent access.
|
||||||
|
|
||||||
|
This approach ensures that we don't do 'expensive' git calls when certain properties are not actually used.
|
||||||
|
In addition, reading the required info when it's needed rather than up front avoids adding delay during gitlint
|
||||||
|
startup time and reduces gitlint's memory footprint.
|
||||||
|
"""
|
||||||
|
|
||||||
This approach ensures that we don't do 'expensive' git calls when certain properties are not actually used.
|
|
||||||
In addition, reading the required info when it's needed rather than up front avoids adding delay during gitlint
|
|
||||||
startup time and reduces gitlint's memory footprint.
|
|
||||||
"""
|
|
||||||
def __init__(self, context, sha): # pylint: disable=super-init-not-called
|
def __init__(self, context, sha): # pylint: disable=super-init-not-called
|
||||||
PropertyCache.__init__(self)
|
PropertyCache.__init__(self)
|
||||||
self.context = context
|
self.context = context
|
||||||
self.sha = sha
|
self.sha = sha
|
||||||
|
|
||||||
def _log(self):
|
def _log(self):
|
||||||
""" Does a call to `git log` to determine a bunch of information about the commit. """
|
"""Does a call to `git log` to determine a bunch of information about the commit."""
|
||||||
long_format = "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B"
|
long_format = "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B"
|
||||||
raw_commit = _git("log", self.sha, "-1", long_format, _cwd=self.context.repository_path).split("\n")
|
raw_commit = _git("log", self.sha, "-1", long_format, _cwd=self.context.repository_path).split("\n")
|
||||||
|
|
||||||
(name, email, date, parents), commit_msg = raw_commit[0].split('\x00'), "\n".join(raw_commit[1:])
|
(name, email, date, parents), commit_msg = raw_commit[0].split("\x00"), "\n".join(raw_commit[1:])
|
||||||
|
|
||||||
commit_parents = parents.split(" ")
|
commit_parents = [] if parents == "" else parents.split(" ")
|
||||||
commit_is_merge_commit = len(commit_parents) > 1
|
commit_is_merge_commit = len(commit_parents) > 1
|
||||||
|
|
||||||
# "YYYY-MM-DD HH:mm:ss Z" -> ISO 8601-like format
|
# "YYYY-MM-DD HH:mm:ss Z" -> ISO 8601-like format
|
||||||
|
@ -216,8 +312,16 @@ class LocalGitCommit(GitCommit, PropertyCache):
|
||||||
# Create Git commit object with the retrieved info
|
# Create Git commit object with the retrieved info
|
||||||
commit_msg_obj = GitCommitMessage.from_full_message(self.context, commit_msg)
|
commit_msg_obj = GitCommitMessage.from_full_message(self.context, commit_msg)
|
||||||
|
|
||||||
self._cache.update({'message': commit_msg_obj, 'author_name': name, 'author_email': email, 'date': commit_date,
|
self._cache.update(
|
||||||
'parents': commit_parents, 'is_merge_commit': commit_is_merge_commit})
|
{
|
||||||
|
"message": commit_msg_obj,
|
||||||
|
"author_name": name,
|
||||||
|
"author_email": email,
|
||||||
|
"date": commit_date,
|
||||||
|
"parents": commit_parents,
|
||||||
|
"is_merge_commit": commit_is_merge_commit,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def message(self):
|
def message(self):
|
||||||
|
@ -251,7 +355,7 @@ class LocalGitCommit(GitCommit, PropertyCache):
|
||||||
# safely do this since git branches cannot contain '*' anywhere, so if we find an '*' we know it's output
|
# safely do this since git branches cannot contain '*' anywhere, so if we find an '*' we know it's output
|
||||||
# from the git CLI and not part of the branch name. See https://git-scm.com/docs/git-check-ref-format
|
# from the git CLI and not part of the branch name. See https://git-scm.com/docs/git-check-ref-format
|
||||||
# We also drop the last empty line from the output.
|
# We also drop the last empty line from the output.
|
||||||
self._cache['branches'] = [branch.replace("*", "").strip() for branch in branches[:-1]]
|
self._cache["branches"] = [branch.replace("*", "").strip() for branch in branches[:-1]]
|
||||||
|
|
||||||
return self._try_cache("branches", cache_branches)
|
return self._try_cache("branches", cache_branches)
|
||||||
|
|
||||||
|
@ -260,20 +364,22 @@ class LocalGitCommit(GitCommit, PropertyCache):
|
||||||
return self._try_cache("is_merge_commit", self._log)
|
return self._try_cache("is_merge_commit", self._log)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def changed_files(self):
|
def changed_files_stats(self):
|
||||||
def cache_changed_files():
|
def cache_changed_files_stats():
|
||||||
self._cache['changed_files'] = _git("diff-tree", "--no-commit-id", "--name-only", "-r", "--root",
|
changed_files_stats_raw = _git(
|
||||||
self.sha, _cwd=self.context.repository_path).split()
|
"diff-tree", "--no-commit-id", "--numstat", "-r", "--root", self.sha, _cwd=self.context.repository_path
|
||||||
|
)
|
||||||
|
self._cache["changed_files_stats"] = _parse_git_changed_file_stats(changed_files_stats_raw)
|
||||||
|
|
||||||
return self._try_cache("changed_files", cache_changed_files)
|
return self._try_cache("changed_files_stats", cache_changed_files_stats)
|
||||||
|
|
||||||
|
|
||||||
class StagedLocalGitCommit(GitCommit, PropertyCache):
|
class StagedLocalGitCommit(GitCommit, PropertyCache):
|
||||||
""" Class representing a git commit that has been staged, but not committed.
|
"""Class representing a git commit that has been staged, but not committed.
|
||||||
|
|
||||||
Other than the commit message itself (and changed files), a lot of information is actually not known at staging
|
Other than the commit message itself (and changed files), a lot of information is actually not known at staging
|
||||||
time, since the commit hasn't happened yet. However, we can make educated guesses based on existing repository
|
time, since the commit hasn't happened yet. However, we can make educated guesses based on existing repository
|
||||||
information.
|
information.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, context, commit_message): # pylint: disable=super-init-not-called
|
def __init__(self, context, commit_message): # pylint: disable=super-init-not-called
|
||||||
|
@ -315,12 +421,16 @@ class StagedLocalGitCommit(GitCommit, PropertyCache):
|
||||||
return [self.context.current_branch]
|
return [self.context.current_branch]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def changed_files(self):
|
def changed_files_stats(self):
|
||||||
return _git("diff", "--staged", "--name-only", "-r", _cwd=self.context.repository_path).split()
|
def cache_changed_files_stats():
|
||||||
|
changed_files_stats_raw = _git("diff", "--staged", "--numstat", "-r", _cwd=self.context.repository_path)
|
||||||
|
self._cache["changed_files_stats"] = _parse_git_changed_file_stats(changed_files_stats_raw)
|
||||||
|
|
||||||
|
return self._try_cache("changed_files_stats", cache_changed_files_stats)
|
||||||
|
|
||||||
|
|
||||||
class GitContext(PropertyCache):
|
class GitContext(PropertyCache):
|
||||||
""" Class representing the git context in which gitlint is operating: a data object storing information about
|
"""Class representing the git context in which gitlint is operating: a data object storing information about
|
||||||
the git repository that gitlint is linting.
|
the git repository that gitlint is linting.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -337,12 +447,16 @@ class GitContext(PropertyCache):
|
||||||
@property
|
@property
|
||||||
@cache
|
@cache
|
||||||
def current_branch(self):
|
def current_branch(self):
|
||||||
current_branch = _git("rev-parse", "--abbrev-ref", "HEAD", _cwd=self.repository_path).strip()
|
try:
|
||||||
|
current_branch = _git("rev-parse", "--abbrev-ref", "HEAD", _cwd=self.repository_path).strip()
|
||||||
|
except GitContextError:
|
||||||
|
# Maybe there is no commit. Try another way to get current branch (need Git 2.22+)
|
||||||
|
current_branch = _git("branch", "--show-current", _cwd=self.repository_path).strip()
|
||||||
return current_branch
|
return current_branch
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_commit_msg(commit_msg_str):
|
def from_commit_msg(commit_msg_str):
|
||||||
""" Determines git context based on a commit message.
|
"""Determines git context based on a commit message.
|
||||||
:param commit_msg_str: Full git commit message.
|
:param commit_msg_str: Full git commit message.
|
||||||
"""
|
"""
|
||||||
context = GitContext()
|
context = GitContext()
|
||||||
|
@ -353,7 +467,7 @@ class GitContext(PropertyCache):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_staged_commit(commit_msg_str, repository_path):
|
def from_staged_commit(commit_msg_str, repository_path):
|
||||||
""" Determines git context based on a commit message that is a staged commit for a local git repository.
|
"""Determines git context based on a commit message that is a staged commit for a local git repository.
|
||||||
:param commit_msg_str: Full git commit message.
|
:param commit_msg_str: Full git commit message.
|
||||||
:param repository_path: Path to the git repository to retrieve the context from
|
:param repository_path: Path to the git repository to retrieve the context from
|
||||||
"""
|
"""
|
||||||
|
@ -364,8 +478,8 @@ class GitContext(PropertyCache):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_local_repository(repository_path, refspec=None, commit_hash=None):
|
def from_local_repository(repository_path, refspec=None, commit_hashes=None):
|
||||||
""" Retrieves the git context from a local git repository.
|
"""Retrieves the git context from a local git repository.
|
||||||
:param repository_path: Path to the git repository to retrieve the context from
|
:param repository_path: Path to the git repository to retrieve the context from
|
||||||
:param refspec: The commit(s) to retrieve (mutually exclusive with `commit_hash`)
|
:param refspec: The commit(s) to retrieve (mutually exclusive with `commit_hash`)
|
||||||
:param commit_hash: Hash of the commit to retrieve (mutually exclusive with `refspec`)
|
:param commit_hash: Hash of the commit to retrieve (mutually exclusive with `refspec`)
|
||||||
|
@ -375,11 +489,13 @@ class GitContext(PropertyCache):
|
||||||
|
|
||||||
if refspec:
|
if refspec:
|
||||||
sha_list = _git("rev-list", refspec, _cwd=repository_path).split()
|
sha_list = _git("rev-list", refspec, _cwd=repository_path).split()
|
||||||
elif commit_hash: # Single commit, just pass it to `git log -1`
|
elif commit_hashes: # One or more commit hashes, just pass it to `git log -1`
|
||||||
# Even though we have already been passed the commit hash, we ask git to retrieve this hash and
|
# Even though we have already been passed the commit hash, we ask git to retrieve this hash and
|
||||||
# return it to us. This way we verify that the passed hash is a valid hash for the target repo and we
|
# return it to us. This way we verify that the passed hash is a valid hash for the target repo and we
|
||||||
# also convert it to the full hash format (we might have been passed a short hash).
|
# also convert it to the full hash format (we might have been passed a short hash).
|
||||||
sha_list = [_git("log", "-1", commit_hash, "--pretty=%H", _cwd=repository_path).replace("\n", "")]
|
sha_list = []
|
||||||
|
for commit_hash in commit_hashes:
|
||||||
|
sha_list.append(_git("log", "-1", commit_hash, "--pretty=%H", _cwd=repository_path).replace("\n", ""))
|
||||||
else: # If no refspec is defined, fallback to the last commit on the current branch
|
else: # If no refspec is defined, fallback to the last commit on the current branch
|
||||||
# We tried many things here e.g.: defaulting to e.g. HEAD or HEAD^... (incl. dealing with
|
# We tried many things here e.g.: defaulting to e.g. HEAD or HEAD^... (incl. dealing with
|
||||||
# repos that only have a single commit - HEAD^... doesn't work there), but then we still get into
|
# repos that only have a single commit - HEAD^... doesn't work there), but then we still get into
|
||||||
|
@ -393,6 +509,10 @@ class GitContext(PropertyCache):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return (isinstance(other, GitContext) and self.commits == other.commits
|
return (
|
||||||
and self.repository_path == other.repository_path
|
isinstance(other, GitContext)
|
||||||
and self.commentchar == other.commentchar and self.current_branch == other.current_branch) # noqa
|
and self.commits == other.commits
|
||||||
|
and self.repository_path == other.repository_path
|
||||||
|
and self.commentchar == other.commentchar
|
||||||
|
and self.current_branch == other.current_branch
|
||||||
|
)
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import io
|
|
||||||
import shutil
|
import shutil
|
||||||
import os
|
import os
|
||||||
import stat
|
import stat
|
||||||
|
@ -17,7 +16,7 @@ class GitHookInstallerError(GitlintError):
|
||||||
|
|
||||||
|
|
||||||
class GitHookInstaller:
|
class GitHookInstaller:
|
||||||
""" Utility class that provides methods for installing and uninstalling the gitlint commitmsg hook. """
|
"""Utility class that provides methods for installing and uninstalling the gitlint commitmsg hook."""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def commit_msg_hook_path(lint_config):
|
def commit_msg_hook_path(lint_config):
|
||||||
|
@ -25,7 +24,7 @@ class GitHookInstaller:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _assert_git_repo(target):
|
def _assert_git_repo(target):
|
||||||
""" Asserts that a given target directory is a git repository """
|
"""Asserts that a given target directory is a git repository"""
|
||||||
hooks_dir = git_hooks_dir(target)
|
hooks_dir = git_hooks_dir(target)
|
||||||
if not os.path.isdir(hooks_dir):
|
if not os.path.isdir(hooks_dir):
|
||||||
raise GitHookInstallerError(f"{target} is not a git repository.")
|
raise GitHookInstallerError(f"{target} is not a git repository.")
|
||||||
|
@ -36,8 +35,9 @@ class GitHookInstaller:
|
||||||
dest_path = GitHookInstaller.commit_msg_hook_path(lint_config)
|
dest_path = GitHookInstaller.commit_msg_hook_path(lint_config)
|
||||||
if os.path.exists(dest_path):
|
if os.path.exists(dest_path):
|
||||||
raise GitHookInstallerError(
|
raise GitHookInstallerError(
|
||||||
f"There is already a commit-msg hook file present in {dest_path}.\n" +
|
f"There is already a commit-msg hook file present in {dest_path}.\n"
|
||||||
"gitlint currently does not support appending to an existing commit-msg file.")
|
"gitlint currently does not support appending to an existing commit-msg file."
|
||||||
|
)
|
||||||
|
|
||||||
# copy hook file
|
# copy hook file
|
||||||
shutil.copy(COMMIT_MSG_HOOK_SRC_PATH, dest_path)
|
shutil.copy(COMMIT_MSG_HOOK_SRC_PATH, dest_path)
|
||||||
|
@ -52,11 +52,13 @@ class GitHookInstaller:
|
||||||
if not os.path.exists(dest_path):
|
if not os.path.exists(dest_path):
|
||||||
raise GitHookInstallerError(f"There is no commit-msg hook present in {dest_path}.")
|
raise GitHookInstallerError(f"There is no commit-msg hook present in {dest_path}.")
|
||||||
|
|
||||||
with io.open(dest_path, encoding=DEFAULT_ENCODING) as fp:
|
with open(dest_path, encoding=DEFAULT_ENCODING) as fp:
|
||||||
lines = fp.readlines()
|
lines = fp.readlines()
|
||||||
if len(lines) < 2 or lines[1] != GITLINT_HOOK_IDENTIFIER:
|
if len(lines) < 2 or lines[1] != GITLINT_HOOK_IDENTIFIER:
|
||||||
msg = f"The commit-msg hook in {dest_path} was not installed by gitlint (or it was modified).\n" + \
|
msg = (
|
||||||
"Uninstallation of 3th party or modified gitlint hooks is not supported."
|
f"The commit-msg hook in {dest_path} was not installed by gitlint (or it was modified).\n"
|
||||||
|
"Uninstallation of 3th party or modified gitlint hooks is not supported."
|
||||||
|
)
|
||||||
raise GitHookInstallerError(msg)
|
raise GitHookInstallerError(msg)
|
||||||
|
|
||||||
# If we are sure it's a gitlint hook, go ahead and remove it
|
# If we are sure it's a gitlint hook, go ahead and remove it
|
||||||
|
|
|
@ -2,13 +2,14 @@
|
||||||
import logging
|
import logging
|
||||||
from gitlint import rules as gitlint_rules
|
from gitlint import rules as gitlint_rules
|
||||||
from gitlint import display
|
from gitlint import display
|
||||||
|
from gitlint.deprecation import Deprecation
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
logging.basicConfig()
|
logging.basicConfig()
|
||||||
|
|
||||||
|
|
||||||
class GitLinter:
|
class GitLinter:
|
||||||
""" Main linter class. This is where rules actually get applied. See the lint() method. """
|
"""Main linter class. This is where rules actually get applied. See the lint() method."""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
@ -16,34 +17,48 @@ class GitLinter:
|
||||||
self.display = display.Display(config)
|
self.display = display.Display(config)
|
||||||
|
|
||||||
def should_ignore_rule(self, rule):
|
def should_ignore_rule(self, rule):
|
||||||
""" Determines whether a rule should be ignored based on the general list of commits to ignore """
|
"""Determines whether a rule should be ignored based on the general list of commits to ignore"""
|
||||||
return rule.id in self.config.ignore or rule.name in self.config.ignore
|
return rule.id in self.config.ignore or rule.name in self.config.ignore
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def configuration_rules(self):
|
def configuration_rules(self):
|
||||||
return [rule for rule in self.config.rules if
|
return [
|
||||||
isinstance(rule, gitlint_rules.ConfigurationRule) and not self.should_ignore_rule(rule)]
|
rule
|
||||||
|
for rule in self.config.rules
|
||||||
|
if isinstance(rule, gitlint_rules.ConfigurationRule) and not self.should_ignore_rule(rule)
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title_line_rules(self):
|
def title_line_rules(self):
|
||||||
return [rule for rule in self.config.rules if
|
return [
|
||||||
isinstance(rule, gitlint_rules.LineRule) and
|
rule
|
||||||
rule.target == gitlint_rules.CommitMessageTitle and not self.should_ignore_rule(rule)]
|
for rule in self.config.rules
|
||||||
|
if isinstance(rule, gitlint_rules.LineRule)
|
||||||
|
and rule.target == gitlint_rules.CommitMessageTitle
|
||||||
|
and not self.should_ignore_rule(rule)
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def body_line_rules(self):
|
def body_line_rules(self):
|
||||||
return [rule for rule in self.config.rules if
|
return [
|
||||||
isinstance(rule, gitlint_rules.LineRule) and
|
rule
|
||||||
rule.target == gitlint_rules.CommitMessageBody and not self.should_ignore_rule(rule)]
|
for rule in self.config.rules
|
||||||
|
if isinstance(rule, gitlint_rules.LineRule)
|
||||||
|
and rule.target == gitlint_rules.CommitMessageBody
|
||||||
|
and not self.should_ignore_rule(rule)
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def commit_rules(self):
|
def commit_rules(self):
|
||||||
return [rule for rule in self.config.rules if isinstance(rule, gitlint_rules.CommitRule) and
|
return [
|
||||||
not self.should_ignore_rule(rule)]
|
rule
|
||||||
|
for rule in self.config.rules
|
||||||
|
if isinstance(rule, gitlint_rules.CommitRule) and not self.should_ignore_rule(rule)
|
||||||
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _apply_line_rules(lines, commit, rules, line_nr_start):
|
def _apply_line_rules(lines, commit, rules, line_nr_start):
|
||||||
""" Iterates over the lines in a given list of lines and validates a given list of rules against each line """
|
"""Iterates over the lines in a given list of lines and validates a given list of rules against each line"""
|
||||||
all_violations = []
|
all_violations = []
|
||||||
line_nr = line_nr_start
|
line_nr = line_nr_start
|
||||||
for line in lines:
|
for line in lines:
|
||||||
|
@ -58,7 +73,7 @@ class GitLinter:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _apply_commit_rules(rules, commit):
|
def _apply_commit_rules(rules, commit):
|
||||||
""" Applies a set of rules against a given commit and gitcontext """
|
"""Applies a set of rules against a given commit and gitcontext"""
|
||||||
all_violations = []
|
all_violations = []
|
||||||
for rule in rules:
|
for rule in rules:
|
||||||
violations = rule.validate(commit)
|
violations = rule.validate(commit)
|
||||||
|
@ -67,19 +82,21 @@ class GitLinter:
|
||||||
return all_violations
|
return all_violations
|
||||||
|
|
||||||
def lint(self, commit):
|
def lint(self, commit):
|
||||||
""" Lint the last commit in a given git context by applying all ignore, title, body and commit rules. """
|
"""Lint the last commit in a given git context by applying all ignore, title, body and commit rules."""
|
||||||
LOG.debug("Linting commit %s", commit.sha or "[SHA UNKNOWN]")
|
LOG.debug("Linting commit %s", commit.sha or "[SHA UNKNOWN]")
|
||||||
LOG.debug("Commit Object\n" + str(commit))
|
LOG.debug("Commit Object\n" + str(commit))
|
||||||
|
|
||||||
|
# Ensure the Deprecation class has a reference to the config currently being used
|
||||||
|
Deprecation.config = self.config
|
||||||
|
|
||||||
# Apply config rules
|
# Apply config rules
|
||||||
for rule in self.configuration_rules:
|
for rule in self.configuration_rules:
|
||||||
rule.apply(self.config, commit)
|
rule.apply(self.config, commit)
|
||||||
|
|
||||||
# Skip linting if this is a special commit type that is configured to be ignored
|
# Skip linting if this is a special commit type that is configured to be ignored
|
||||||
ignore_commit_types = ["merge", "squash", "fixup", "revert"]
|
ignore_commit_types = ["merge", "squash", "fixup", "fixup_amend", "revert"]
|
||||||
for commit_type in ignore_commit_types:
|
for commit_type in ignore_commit_types:
|
||||||
if getattr(commit, f"is_{commit_type}_commit") and \
|
if getattr(commit, f"is_{commit_type}_commit") and getattr(self.config, f"ignore_{commit_type}_commits"):
|
||||||
getattr(self.config, f"ignore_{commit_type}_commits"):
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
violations = []
|
violations = []
|
||||||
|
@ -95,12 +112,12 @@ class GitLinter:
|
||||||
return violations
|
return violations
|
||||||
|
|
||||||
def print_violations(self, violations):
|
def print_violations(self, violations):
|
||||||
""" Print a given set of violations to the standard error output """
|
"""Print a given set of violations to the standard error output"""
|
||||||
for v in violations:
|
for v in violations:
|
||||||
line_nr = v.line_nr if v.line_nr else "-"
|
line_nr = v.line_nr if v.line_nr else "-"
|
||||||
self.display.e(f"{line_nr}: {v.rule_id}", exact=True)
|
self.display.e(f"{line_nr}: {v.rule_id}", exact=True)
|
||||||
self.display.ee(f"{line_nr}: {v.rule_id} {v.message}", exact=True)
|
self.display.ee(f"{line_nr}: {v.rule_id} {v.message}", exact=True)
|
||||||
if v.content:
|
if v.content:
|
||||||
self.display.eee(f"{line_nr}: {v.rule_id} {v.message}: \"{v.content}\"", exact=True)
|
self.display.eee(f'{line_nr}: {v.rule_id} {v.message}: "{v.content}"', exact=True)
|
||||||
else:
|
else:
|
||||||
self.display.eee(f"{line_nr}: {v.rule_id} {v.message}", exact=True)
|
self.display.eee(f"{line_nr}: {v.rule_id} {v.message}", exact=True)
|
||||||
|
|
|
@ -6,7 +6,7 @@ from gitlint.exception import GitlintError
|
||||||
|
|
||||||
|
|
||||||
def allow_none(func):
|
def allow_none(func):
|
||||||
""" Decorator that sets option value to None if the passed value is None, otherwise calls the regular set method """
|
"""Decorator that sets option value to None if the passed value is None, otherwise calls the regular set method"""
|
||||||
|
|
||||||
def wrapped(obj, value):
|
def wrapped(obj, value):
|
||||||
if value is None:
|
if value is None:
|
||||||
|
@ -22,10 +22,10 @@ class RuleOptionError(GitlintError):
|
||||||
|
|
||||||
|
|
||||||
class RuleOption:
|
class RuleOption:
|
||||||
""" Base class representing a configurable part (i.e. option) of a rule (e.g. the max-length of the title-max-line
|
"""Base class representing a configurable part (i.e. option) of a rule (e.g. the max-length of the title-max-line
|
||||||
rule).
|
rule).
|
||||||
This class should not be used directly. Instead, use on the derived classes like StrOption, IntOption to set
|
This class should not be used directly. Instead, use on the derived classes like StrOption, IntOption to set
|
||||||
options of a particular type like int, str, etc.
|
options of a particular type like int, str, etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name, value, description):
|
def __init__(self, name, value, description):
|
||||||
|
@ -36,7 +36,7 @@ class RuleOption:
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def set(self, value):
|
def set(self, value):
|
||||||
""" Validates and sets the option's value """
|
"""Validates and sets the option's value"""
|
||||||
pass # pragma: no cover
|
pass # pragma: no cover
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -76,18 +76,16 @@ class IntOption(RuleOption):
|
||||||
|
|
||||||
|
|
||||||
class BoolOption(RuleOption):
|
class BoolOption(RuleOption):
|
||||||
|
|
||||||
# explicit choice to not annotate with @allow_none: Booleans must be False or True, they cannot be unset.
|
# explicit choice to not annotate with @allow_none: Booleans must be False or True, they cannot be unset.
|
||||||
def set(self, value):
|
def set(self, value):
|
||||||
value = str(value).strip().lower()
|
value = str(value).strip().lower()
|
||||||
if value not in ['true', 'false']:
|
if value not in ["true", "false"]:
|
||||||
raise RuleOptionError(f"Option '{self.name}' must be either 'true' or 'false'")
|
raise RuleOptionError(f"Option '{self.name}' must be either 'true' or 'false'")
|
||||||
self.value = value == 'true'
|
self.value = value == "true"
|
||||||
|
|
||||||
|
|
||||||
class ListOption(RuleOption):
|
class ListOption(RuleOption):
|
||||||
""" Option that is either a given list or a comma-separated string that can be split into a list when being set.
|
"""Option that is either a given list or a comma-separated string that can be split into a list when being set."""
|
||||||
"""
|
|
||||||
|
|
||||||
@allow_none
|
@allow_none
|
||||||
def set(self, value):
|
def set(self, value):
|
||||||
|
@ -100,7 +98,7 @@ class ListOption(RuleOption):
|
||||||
|
|
||||||
|
|
||||||
class PathOption(RuleOption):
|
class PathOption(RuleOption):
|
||||||
""" Option that accepts either a directory or both a directory and a file. """
|
"""Option that accepts either a directory or both a directory and a file."""
|
||||||
|
|
||||||
def __init__(self, name, value, description, type="dir"):
|
def __init__(self, name, value, description, type="dir"):
|
||||||
self.type = type
|
self.type = type
|
||||||
|
@ -112,16 +110,17 @@ class PathOption(RuleOption):
|
||||||
|
|
||||||
error_msg = ""
|
error_msg = ""
|
||||||
|
|
||||||
if self.type == 'dir':
|
if self.type == "dir":
|
||||||
if not os.path.isdir(value):
|
if not os.path.isdir(value):
|
||||||
error_msg = f"Option {self.name} must be an existing directory (current value: '{value}')"
|
error_msg = f"Option {self.name} must be an existing directory (current value: '{value}')"
|
||||||
elif self.type == 'file':
|
elif self.type == "file":
|
||||||
if not os.path.isfile(value):
|
if not os.path.isfile(value):
|
||||||
error_msg = f"Option {self.name} must be an existing file (current value: '{value}')"
|
error_msg = f"Option {self.name} must be an existing file (current value: '{value}')"
|
||||||
elif self.type == 'both':
|
elif self.type == "both":
|
||||||
if not os.path.isdir(value) and not os.path.isfile(value):
|
if not os.path.isdir(value) and not os.path.isfile(value):
|
||||||
error_msg = (f"Option {self.name} must be either an existing directory or file "
|
error_msg = (
|
||||||
f"(current value: '{value}')")
|
f"Option {self.name} must be either an existing directory or file (current value: '{value}')"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
error_msg = f"Option {self.name} type must be one of: 'file', 'dir', 'both' (current: '{self.type}')"
|
error_msg = f"Option {self.name} type must be one of: 'file', 'dir', 'both' (current: '{self.type}')"
|
||||||
|
|
||||||
|
@ -132,7 +131,6 @@ class PathOption(RuleOption):
|
||||||
|
|
||||||
|
|
||||||
class RegexOption(RuleOption):
|
class RegexOption(RuleOption):
|
||||||
|
|
||||||
@allow_none
|
@allow_none
|
||||||
def set(self, value):
|
def set(self, value):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -31,7 +31,7 @@ def find_rule_classes(extra_path):
|
||||||
|
|
||||||
# Filter out files that are not python modules
|
# Filter out files that are not python modules
|
||||||
for filename in files:
|
for filename in files:
|
||||||
if fnmatch.fnmatch(filename, '*.py'):
|
if fnmatch.fnmatch(filename, "*.py"):
|
||||||
# We have to treat __init__ files a bit special: add the parent dir instead of the filename, and also
|
# We have to treat __init__ files a bit special: add the parent dir instead of the filename, and also
|
||||||
# add their parent dir to the sys.path (this fixes import issues with pypy2).
|
# add their parent dir to the sys.path (this fixes import issues with pypy2).
|
||||||
if filename == "__init__.py":
|
if filename == "__init__.py":
|
||||||
|
@ -61,13 +61,19 @@ def find_rule_classes(extra_path):
|
||||||
# 1) is it a class, if not, skip
|
# 1) is it a class, if not, skip
|
||||||
# 2) is the parent path the current module. If not, we are dealing with an imported class, skip
|
# 2) is the parent path the current module. If not, we are dealing with an imported class, skip
|
||||||
# 3) is it a subclass of rule
|
# 3) is it a subclass of rule
|
||||||
rule_classes.extend([clazz for _, clazz in inspect.getmembers(sys.modules[module])
|
rule_classes.extend(
|
||||||
if
|
[
|
||||||
inspect.isclass(clazz) and # check isclass to ensure clazz.__module__ exists
|
clazz
|
||||||
clazz.__module__ == module and # ignore imported classes
|
for _, clazz in inspect.getmembers(sys.modules[module])
|
||||||
(issubclass(clazz, rules.LineRule) or
|
if inspect.isclass(clazz) # check isclass to ensure clazz.__module__ exists
|
||||||
issubclass(clazz, rules.CommitRule) or
|
and clazz.__module__ == module # ignore imported classes
|
||||||
issubclass(clazz, rules.ConfigurationRule))])
|
and (
|
||||||
|
issubclass(clazz, rules.LineRule)
|
||||||
|
or issubclass(clazz, rules.CommitRule)
|
||||||
|
or issubclass(clazz, rules.ConfigurationRule)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# validate that the rule classes are valid user-defined rules
|
# validate that the rule classes are valid user-defined rules
|
||||||
for rule_class in rule_classes:
|
for rule_class in rule_classes:
|
||||||
|
@ -91,55 +97,66 @@ def assert_valid_rule_class(clazz, rule_type="User-defined"): # pylint: disable
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Rules must extend from LineRule, CommitRule or ConfigurationRule
|
# Rules must extend from LineRule, CommitRule or ConfigurationRule
|
||||||
if not (issubclass(clazz, rules.LineRule) or issubclass(clazz, rules.CommitRule)
|
if not (
|
||||||
or issubclass(clazz, rules.ConfigurationRule)):
|
issubclass(clazz, rules.LineRule)
|
||||||
msg = f"{rule_type} rule class '{clazz.__name__}' " + \
|
or issubclass(clazz, rules.CommitRule)
|
||||||
f"must extend from {rules.CommitRule.__module__}.{rules.LineRule.__name__}, " + \
|
or issubclass(clazz, rules.ConfigurationRule)
|
||||||
f"{rules.CommitRule.__module__}.{rules.CommitRule.__name__} or " + \
|
):
|
||||||
f"{rules.CommitRule.__module__}.{rules.ConfigurationRule.__name__}"
|
msg = (
|
||||||
|
f"{rule_type} rule class '{clazz.__name__}' "
|
||||||
|
f"must extend from {rules.CommitRule.__module__}.{rules.LineRule.__name__}, "
|
||||||
|
f"{rules.CommitRule.__module__}.{rules.CommitRule.__name__} or "
|
||||||
|
f"{rules.CommitRule.__module__}.{rules.ConfigurationRule.__name__}"
|
||||||
|
)
|
||||||
raise rules.UserRuleError(msg)
|
raise rules.UserRuleError(msg)
|
||||||
|
|
||||||
# Rules must have an id attribute
|
# Rules must have an id attribute
|
||||||
if not hasattr(clazz, 'id') or clazz.id is None or not clazz.id:
|
if not hasattr(clazz, "id") or clazz.id is None or not clazz.id:
|
||||||
raise rules.UserRuleError(f"{rule_type} rule class '{clazz.__name__}' must have an 'id' attribute")
|
raise rules.UserRuleError(f"{rule_type} rule class '{clazz.__name__}' must have an 'id' attribute")
|
||||||
|
|
||||||
# Rule id's cannot start with gitlint reserved letters
|
# Rule id's cannot start with gitlint reserved letters
|
||||||
if clazz.id[0].upper() in ['R', 'T', 'B', 'M', 'I']:
|
if clazz.id[0].upper() in ["R", "T", "B", "M", "I"]:
|
||||||
msg = f"The id '{clazz.id[0]}' of '{clazz.__name__}' is invalid. Gitlint reserves ids starting with R,T,B,M,I"
|
msg = f"The id '{clazz.id[0]}' of '{clazz.__name__}' is invalid. Gitlint reserves ids starting with R,T,B,M,I"
|
||||||
raise rules.UserRuleError(msg)
|
raise rules.UserRuleError(msg)
|
||||||
|
|
||||||
# Rules must have a name attribute
|
# Rules must have a name attribute
|
||||||
if not hasattr(clazz, 'name') or clazz.name is None or not clazz.name:
|
if not hasattr(clazz, "name") or clazz.name is None or not clazz.name:
|
||||||
raise rules.UserRuleError(f"{rule_type} rule class '{clazz.__name__}' must have a 'name' attribute")
|
raise rules.UserRuleError(f"{rule_type} rule class '{clazz.__name__}' must have a 'name' attribute")
|
||||||
|
|
||||||
# if set, options_spec must be a list of RuleOption
|
# if set, options_spec must be a list of RuleOption
|
||||||
if not isinstance(clazz.options_spec, list):
|
if not isinstance(clazz.options_spec, list):
|
||||||
msg = f"The options_spec attribute of {rule_type.lower()} rule class '{clazz.__name__}' " + \
|
msg = (
|
||||||
f"must be a list of {options.RuleOption.__module__}.{options.RuleOption.__name__}"
|
f"The options_spec attribute of {rule_type.lower()} rule class '{clazz.__name__}' "
|
||||||
|
f"must be a list of {options.RuleOption.__module__}.{options.RuleOption.__name__}"
|
||||||
|
)
|
||||||
raise rules.UserRuleError(msg)
|
raise rules.UserRuleError(msg)
|
||||||
|
|
||||||
# check that all items in options_spec are actual gitlint options
|
# check that all items in options_spec are actual gitlint options
|
||||||
for option in clazz.options_spec:
|
for option in clazz.options_spec:
|
||||||
if not isinstance(option, options.RuleOption):
|
if not isinstance(option, options.RuleOption):
|
||||||
msg = f"The options_spec attribute of {rule_type.lower()} rule class '{clazz.__name__}' " + \
|
msg = (
|
||||||
f"must be a list of {options.RuleOption.__module__}.{options.RuleOption.__name__}"
|
f"The options_spec attribute of {rule_type.lower()} rule class '{clazz.__name__}' "
|
||||||
|
f"must be a list of {options.RuleOption.__module__}.{options.RuleOption.__name__}"
|
||||||
|
)
|
||||||
raise rules.UserRuleError(msg)
|
raise rules.UserRuleError(msg)
|
||||||
|
|
||||||
# Line/Commit rules must have a `validate` method
|
# 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
|
# 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 issubclass(clazz, rules.LineRule) or issubclass(clazz, rules.CommitRule):
|
||||||
if not hasattr(clazz, 'validate') or not inspect.isroutine(clazz.validate):
|
if not hasattr(clazz, "validate") or not inspect.isroutine(clazz.validate):
|
||||||
raise rules.UserRuleError(f"{rule_type} rule class '{clazz.__name__}' must have a 'validate' method")
|
raise rules.UserRuleError(f"{rule_type} rule class '{clazz.__name__}' must have a 'validate' method")
|
||||||
# Configuration rules must have an `apply` method
|
# Configuration rules must have an `apply` method
|
||||||
elif issubclass(clazz, rules.ConfigurationRule):
|
elif issubclass(clazz, rules.ConfigurationRule):
|
||||||
if not hasattr(clazz, 'apply') or not inspect.isroutine(clazz.apply):
|
if not hasattr(clazz, "apply") or not inspect.isroutine(clazz.apply):
|
||||||
msg = f"{rule_type} Configuration rule class '{clazz.__name__}' must have an 'apply' method"
|
msg = f"{rule_type} Configuration rule class '{clazz.__name__}' must have an 'apply' method"
|
||||||
raise rules.UserRuleError(msg)
|
raise rules.UserRuleError(msg)
|
||||||
|
|
||||||
# LineRules must have a valid target: rules.CommitMessageTitle or rules.CommitMessageBody
|
# LineRules must have a valid target: rules.CommitMessageTitle or rules.CommitMessageBody
|
||||||
if issubclass(clazz, rules.LineRule):
|
if issubclass(clazz, rules.LineRule):
|
||||||
if clazz.target not in [rules.CommitMessageTitle, rules.CommitMessageBody]:
|
if clazz.target not in [rules.CommitMessageTitle, rules.CommitMessageBody]:
|
||||||
msg = f"The target attribute of the {rule_type.lower()} LineRule class '{clazz.__name__}' " + \
|
msg = (
|
||||||
f"must be either {rules.CommitMessageTitle.__module__}.{rules.CommitMessageTitle.__name__} " + \
|
f"The target attribute of the {rule_type.lower()} LineRule class '{clazz.__name__}' "
|
||||||
f"or {rules.CommitMessageTitle.__module__}.{rules.CommitMessageBody.__name__}"
|
f"must be either {rules.CommitMessageTitle.__module__}.{rules.CommitMessageTitle.__name__} "
|
||||||
|
f"or {rules.CommitMessageTitle.__module__}.{rules.CommitMessageBody.__name__}"
|
||||||
|
)
|
||||||
raise rules.UserRuleError(msg)
|
raise rules.UserRuleError(msg)
|
||||||
|
|
|
@ -5,15 +5,18 @@ import re
|
||||||
|
|
||||||
from gitlint.options import IntOption, BoolOption, StrOption, ListOption, RegexOption
|
from gitlint.options import IntOption, BoolOption, StrOption, ListOption, RegexOption
|
||||||
from gitlint.exception import GitlintError
|
from gitlint.exception import GitlintError
|
||||||
|
from gitlint.deprecation import Deprecation
|
||||||
|
|
||||||
|
|
||||||
class Rule:
|
class Rule:
|
||||||
""" Class representing gitlint rules. """
|
"""Class representing gitlint rules."""
|
||||||
|
|
||||||
options_spec = []
|
options_spec = []
|
||||||
id = None
|
id = None
|
||||||
name = None
|
name = None
|
||||||
target = None
|
target = None
|
||||||
_log = None
|
_log = None
|
||||||
|
_log_deprecated_regex_style_search = None
|
||||||
|
|
||||||
def __init__(self, opts=None):
|
def __init__(self, opts=None):
|
||||||
if not opts:
|
if not opts:
|
||||||
|
@ -33,48 +36,58 @@ class Rule:
|
||||||
return self._log
|
return self._log
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.id == other.id and self.name == other.name and \
|
return (
|
||||||
self.options == other.options and self.target == other.target # noqa
|
self.id == other.id
|
||||||
|
and self.name == other.name
|
||||||
|
and self.options == other.options
|
||||||
|
and self.target == other.target
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.id} {self.name}" # pragma: no cover
|
return f"{self.id} {self.name}" # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationRule(Rule):
|
class ConfigurationRule(Rule):
|
||||||
""" Class representing rules that can dynamically change the configuration of gitlint during runtime. """
|
"""Class representing rules that can dynamically change the configuration of gitlint during runtime."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CommitRule(Rule):
|
class CommitRule(Rule):
|
||||||
""" Class representing rules that act on an entire commit at once """
|
"""Class representing rules that act on an entire commit at once"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class LineRule(Rule):
|
class LineRule(Rule):
|
||||||
""" Class representing rules that act on a line by line basis """
|
"""Class representing rules that act on a line by line basis"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class LineRuleTarget:
|
class LineRuleTarget:
|
||||||
""" Base class for LineRule targets. A LineRuleTarget specifies where a given rule will be applied
|
"""Base class for LineRule targets. A LineRuleTarget specifies where a given rule will be applied
|
||||||
(e.g. commit message title, commit message body).
|
(e.g. commit message title, commit message body).
|
||||||
Each LineRule MUST have a target specified. """
|
Each LineRule MUST have a target specified."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CommitMessageTitle(LineRuleTarget):
|
class CommitMessageTitle(LineRuleTarget):
|
||||||
""" Target class used for rules that apply to a commit message title """
|
"""Target class used for rules that apply to a commit message title"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CommitMessageBody(LineRuleTarget):
|
class CommitMessageBody(LineRuleTarget):
|
||||||
""" Target class used for rules that apply to a commit message body """
|
"""Target class used for rules that apply to a commit message body"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RuleViolation:
|
class RuleViolation:
|
||||||
""" Class representing a violation of a rule. I.e.: When a rule is broken, the rule will instantiate this class
|
"""Class representing a violation of a rule. I.e.: When a rule is broken, the rule will instantiate this class
|
||||||
to indicate how and where the rule was broken. """
|
to indicate how and where the rule was broken."""
|
||||||
|
|
||||||
def __init__(self, rule_id, message, content=None, line_nr=None):
|
def __init__(self, rule_id, message, content=None, line_nr=None):
|
||||||
self.rule_id = rule_id
|
self.rule_id = rule_id
|
||||||
|
@ -88,22 +101,23 @@ class RuleViolation:
|
||||||
return equal
|
return equal
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.line_nr}: {self.rule_id} {self.message}: \"{self.content}\""
|
return f'{self.line_nr}: {self.rule_id} {self.message}: "{self.content}"'
|
||||||
|
|
||||||
|
|
||||||
class UserRuleError(GitlintError):
|
class UserRuleError(GitlintError):
|
||||||
""" Error used to indicate that an error occurred while trying to load a user rule """
|
"""Error used to indicate that an error occurred while trying to load a user rule"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MaxLineLength(LineRule):
|
class MaxLineLength(LineRule):
|
||||||
name = "max-line-length"
|
name = "max-line-length"
|
||||||
id = "R1"
|
id = "R1"
|
||||||
options_spec = [IntOption('line-length', 80, "Max line length")]
|
options_spec = [IntOption("line-length", 80, "Max line length")]
|
||||||
violation_message = "Line exceeds max length ({0}>{1})"
|
violation_message = "Line exceeds max length ({0}>{1})"
|
||||||
|
|
||||||
def validate(self, line, _commit):
|
def validate(self, line, _commit):
|
||||||
max_length = self.options['line-length'].value
|
max_length = self.options["line-length"].value
|
||||||
if len(line) > max_length:
|
if len(line) > max_length:
|
||||||
return [RuleViolation(self.id, self.violation_message.format(len(line), max_length), line)]
|
return [RuleViolation(self.id, self.violation_message.format(len(line), max_length), line)]
|
||||||
|
|
||||||
|
@ -130,15 +144,16 @@ class HardTab(LineRule):
|
||||||
|
|
||||||
|
|
||||||
class LineMustNotContainWord(LineRule):
|
class LineMustNotContainWord(LineRule):
|
||||||
""" Violation if a line contains one of a list of words (NOTE: using a word in the list inside another word is not
|
"""Violation if a line contains one of a list of words (NOTE: using a word in the list inside another word is not
|
||||||
a violation, e.g: WIPING is not a violation if 'WIP' is a word that is not allowed.) """
|
a violation, e.g: WIPING is not a violation if 'WIP' is a word that is not allowed.)"""
|
||||||
|
|
||||||
name = "line-must-not-contain"
|
name = "line-must-not-contain"
|
||||||
id = "R5"
|
id = "R5"
|
||||||
options_spec = [ListOption('words', [], "Comma separated list of words that should not be found")]
|
options_spec = [ListOption("words", [], "Comma separated list of words that should not be found")]
|
||||||
violation_message = "Line contains {0}"
|
violation_message = "Line contains {0}"
|
||||||
|
|
||||||
def validate(self, line, _commit):
|
def validate(self, line, _commit):
|
||||||
strings = self.options['words'].value
|
strings = self.options["words"].value
|
||||||
violations = []
|
violations = []
|
||||||
for string in strings:
|
for string in strings:
|
||||||
regex = re.compile(rf"\b{string.lower()}\b", re.IGNORECASE | re.UNICODE)
|
regex = re.compile(rf"\b{string.lower()}\b", re.IGNORECASE | re.UNICODE)
|
||||||
|
@ -163,7 +178,7 @@ class TitleMaxLength(MaxLineLength):
|
||||||
name = "title-max-length"
|
name = "title-max-length"
|
||||||
id = "T1"
|
id = "T1"
|
||||||
target = CommitMessageTitle
|
target = CommitMessageTitle
|
||||||
options_spec = [IntOption('line-length', 72, "Max line length")]
|
options_spec = [IntOption("line-length", 72, "Max line length")]
|
||||||
violation_message = "Title exceeds max length ({0}>{1})"
|
violation_message = "Title exceeds max length ({0}>{1})"
|
||||||
|
|
||||||
|
|
||||||
|
@ -180,7 +195,7 @@ class TitleTrailingPunctuation(LineRule):
|
||||||
target = CommitMessageTitle
|
target = CommitMessageTitle
|
||||||
|
|
||||||
def validate(self, title, _commit):
|
def validate(self, title, _commit):
|
||||||
punctuation_marks = '?:!.,;'
|
punctuation_marks = "?:!.,;"
|
||||||
for punctuation_mark in punctuation_marks:
|
for punctuation_mark in punctuation_marks:
|
||||||
if title.endswith(punctuation_mark):
|
if title.endswith(punctuation_mark):
|
||||||
return [RuleViolation(self.id, f"Title has trailing punctuation ({punctuation_mark})", title)]
|
return [RuleViolation(self.id, f"Title has trailing punctuation ({punctuation_mark})", title)]
|
||||||
|
@ -197,7 +212,7 @@ class TitleMustNotContainWord(LineMustNotContainWord):
|
||||||
name = "title-must-not-contain-word"
|
name = "title-must-not-contain-word"
|
||||||
id = "T5"
|
id = "T5"
|
||||||
target = CommitMessageTitle
|
target = CommitMessageTitle
|
||||||
options_spec = [ListOption('words', ["WIP"], "Must not contain word")]
|
options_spec = [ListOption("words", ["WIP"], "Must not contain word")]
|
||||||
violation_message = "Title contains the word '{0}' (case-insensitive)"
|
violation_message = "Title contains the word '{0}' (case-insensitive)"
|
||||||
|
|
||||||
|
|
||||||
|
@ -212,14 +227,14 @@ class TitleRegexMatches(LineRule):
|
||||||
name = "title-match-regex"
|
name = "title-match-regex"
|
||||||
id = "T7"
|
id = "T7"
|
||||||
target = CommitMessageTitle
|
target = CommitMessageTitle
|
||||||
options_spec = [RegexOption('regex', None, "Regex the title should match")]
|
options_spec = [RegexOption("regex", None, "Regex the title should match")]
|
||||||
|
|
||||||
def validate(self, title, _commit):
|
def validate(self, title, _commit):
|
||||||
# If no regex is specified, immediately return
|
# If no regex is specified, immediately return
|
||||||
if not self.options['regex'].value:
|
if not self.options["regex"].value:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.options['regex'].value.search(title):
|
if not self.options["regex"].value.search(title):
|
||||||
violation_msg = f"Title does not match regex ({self.options['regex'].value.pattern})"
|
violation_msg = f"Title does not match regex ({self.options['regex'].value.pattern})"
|
||||||
return [RuleViolation(self.id, violation_msg, title)]
|
return [RuleViolation(self.id, violation_msg, title)]
|
||||||
|
|
||||||
|
@ -228,10 +243,10 @@ class TitleMinLength(LineRule):
|
||||||
name = "title-min-length"
|
name = "title-min-length"
|
||||||
id = "T8"
|
id = "T8"
|
||||||
target = CommitMessageTitle
|
target = CommitMessageTitle
|
||||||
options_spec = [IntOption('min-length', 5, "Minimum required title length")]
|
options_spec = [IntOption("min-length", 5, "Minimum required title length")]
|
||||||
|
|
||||||
def validate(self, title, _commit):
|
def validate(self, title, _commit):
|
||||||
min_length = self.options['min-length'].value
|
min_length = self.options["min-length"].value
|
||||||
actual_length = len(title)
|
actual_length = len(title)
|
||||||
if actual_length < min_length:
|
if actual_length < min_length:
|
||||||
violation_message = f"Title is too short ({actual_length}<{min_length})"
|
violation_message = f"Title is too short ({actual_length}<{min_length})"
|
||||||
|
@ -270,10 +285,10 @@ class BodyFirstLineEmpty(CommitRule):
|
||||||
class BodyMinLength(CommitRule):
|
class BodyMinLength(CommitRule):
|
||||||
name = "body-min-length"
|
name = "body-min-length"
|
||||||
id = "B5"
|
id = "B5"
|
||||||
options_spec = [IntOption('min-length', 20, "Minimum body length")]
|
options_spec = [IntOption("min-length", 20, "Minimum body length")]
|
||||||
|
|
||||||
def validate(self, commit):
|
def validate(self, commit):
|
||||||
min_length = self.options['min-length'].value
|
min_length = self.options["min-length"].value
|
||||||
body_message_no_newline = "".join([line for line in commit.message.body if line is not None])
|
body_message_no_newline = "".join([line for line in commit.message.body if line is not None])
|
||||||
actual_length = len(body_message_no_newline)
|
actual_length = len(body_message_no_newline)
|
||||||
if 0 < actual_length < min_length:
|
if 0 < actual_length < min_length:
|
||||||
|
@ -284,24 +299,24 @@ class BodyMinLength(CommitRule):
|
||||||
class BodyMissing(CommitRule):
|
class BodyMissing(CommitRule):
|
||||||
name = "body-is-missing"
|
name = "body-is-missing"
|
||||||
id = "B6"
|
id = "B6"
|
||||||
options_spec = [BoolOption('ignore-merge-commits', True, "Ignore merge commits")]
|
options_spec = [BoolOption("ignore-merge-commits", True, "Ignore merge commits")]
|
||||||
|
|
||||||
def validate(self, commit):
|
def validate(self, commit):
|
||||||
# ignore merges when option tells us to, which may have no body
|
# ignore merges when option tells us to, which may have no body
|
||||||
if self.options['ignore-merge-commits'].value and commit.is_merge_commit:
|
if self.options["ignore-merge-commits"].value and commit.is_merge_commit:
|
||||||
return
|
return
|
||||||
if len(commit.message.body) < 2 or not ''.join(commit.message.body).strip():
|
if len(commit.message.body) < 2 or not "".join(commit.message.body).strip():
|
||||||
return [RuleViolation(self.id, "Body message is missing", None, 3)]
|
return [RuleViolation(self.id, "Body message is missing", None, 3)]
|
||||||
|
|
||||||
|
|
||||||
class BodyChangedFileMention(CommitRule):
|
class BodyChangedFileMention(CommitRule):
|
||||||
name = "body-changed-file-mention"
|
name = "body-changed-file-mention"
|
||||||
id = "B7"
|
id = "B7"
|
||||||
options_spec = [ListOption('files', [], "Files that need to be mentioned")]
|
options_spec = [ListOption("files", [], "Files that need to be mentioned")]
|
||||||
|
|
||||||
def validate(self, commit):
|
def validate(self, commit):
|
||||||
violations = []
|
violations = []
|
||||||
for needs_mentioned_file in self.options['files'].value:
|
for needs_mentioned_file in self.options["files"].value:
|
||||||
# if a file that we need to look out for is actually changed, then check whether it occurs
|
# if a file that we need to look out for is actually changed, then check whether it occurs
|
||||||
# in the commit msg body
|
# in the commit msg body
|
||||||
if needs_mentioned_file in commit.changed_files:
|
if needs_mentioned_file in commit.changed_files:
|
||||||
|
@ -314,11 +329,11 @@ class BodyChangedFileMention(CommitRule):
|
||||||
class BodyRegexMatches(CommitRule):
|
class BodyRegexMatches(CommitRule):
|
||||||
name = "body-match-regex"
|
name = "body-match-regex"
|
||||||
id = "B8"
|
id = "B8"
|
||||||
options_spec = [RegexOption('regex', None, "Regex the body should match")]
|
options_spec = [RegexOption("regex", None, "Regex the body should match")]
|
||||||
|
|
||||||
def validate(self, commit):
|
def validate(self, commit):
|
||||||
# If no regex is specified, immediately return
|
# If no regex is specified, immediately return
|
||||||
if not self.options['regex'].value:
|
if not self.options["regex"].value:
|
||||||
return
|
return
|
||||||
|
|
||||||
# We intentionally ignore the first line in the body as that's the empty line after the title,
|
# We intentionally ignore the first line in the body as that's the empty line after the title,
|
||||||
|
@ -334,7 +349,7 @@ class BodyRegexMatches(CommitRule):
|
||||||
|
|
||||||
full_body = "\n".join(body_lines)
|
full_body = "\n".join(body_lines)
|
||||||
|
|
||||||
if not self.options['regex'].value.search(full_body):
|
if not self.options["regex"].value.search(full_body):
|
||||||
violation_msg = f"Body does not match regex ({self.options['regex'].value.pattern})"
|
violation_msg = f"Body does not match regex ({self.options['regex'].value.pattern})"
|
||||||
return [RuleViolation(self.id, violation_msg, None, len(commit.message.body) + 1)]
|
return [RuleViolation(self.id, violation_msg, None, len(commit.message.body) + 1)]
|
||||||
|
|
||||||
|
@ -342,33 +357,51 @@ class BodyRegexMatches(CommitRule):
|
||||||
class AuthorValidEmail(CommitRule):
|
class AuthorValidEmail(CommitRule):
|
||||||
name = "author-valid-email"
|
name = "author-valid-email"
|
||||||
id = "M1"
|
id = "M1"
|
||||||
options_spec = [RegexOption('regex', r"[^@ ]+@[^@ ]+\.[^@ ]+", "Regex that author email address should match")]
|
DEFAULT_AUTHOR_VALID_EMAIL_REGEX = r"^[^@ ]+@[^@ ]+\.[^@ ]+"
|
||||||
|
options_spec = [
|
||||||
|
RegexOption("regex", DEFAULT_AUTHOR_VALID_EMAIL_REGEX, "Regex that author email address should match")
|
||||||
|
]
|
||||||
|
|
||||||
def validate(self, commit):
|
def validate(self, commit):
|
||||||
# If no regex is specified, immediately return
|
# If no regex is specified, immediately return
|
||||||
if not self.options['regex'].value:
|
if not self.options["regex"].value:
|
||||||
return
|
return
|
||||||
|
|
||||||
if commit.author_email and not self.options['regex'].value.match(commit.author_email):
|
# We're replacing regex match with search semantics, see https://github.com/jorisroovers/gitlint/issues/254
|
||||||
|
# In case the user is using the default regex, we can silently change to using search
|
||||||
|
# If not, it depends on config (handled by Deprecation class)
|
||||||
|
if self.DEFAULT_AUTHOR_VALID_EMAIL_REGEX == self.options["regex"].value.pattern:
|
||||||
|
regex_method = self.options["regex"].value.search
|
||||||
|
else:
|
||||||
|
regex_method = Deprecation.get_regex_method(self, self.options["regex"])
|
||||||
|
|
||||||
|
if commit.author_email and not regex_method(commit.author_email):
|
||||||
return [RuleViolation(self.id, "Author email for commit is invalid", commit.author_email)]
|
return [RuleViolation(self.id, "Author email for commit is invalid", commit.author_email)]
|
||||||
|
|
||||||
|
|
||||||
class IgnoreByTitle(ConfigurationRule):
|
class IgnoreByTitle(ConfigurationRule):
|
||||||
name = "ignore-by-title"
|
name = "ignore-by-title"
|
||||||
id = "I1"
|
id = "I1"
|
||||||
options_spec = [RegexOption('regex', None, "Regex matching the titles of commits this rule should apply to"),
|
options_spec = [
|
||||||
StrOption('ignore', "all", "Comma-separated list of rules to ignore")]
|
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):
|
def apply(self, config, commit):
|
||||||
# If no regex is specified, immediately return
|
# If no regex is specified, immediately return
|
||||||
if not self.options['regex'].value:
|
if not self.options["regex"].value:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.options['regex'].value.match(commit.message.title):
|
# We're replacing regex match with search semantics, see https://github.com/jorisroovers/gitlint/issues/254
|
||||||
config.ignore = self.options['ignore'].value
|
regex_method = Deprecation.get_regex_method(self, self.options["regex"])
|
||||||
|
|
||||||
message = f"Commit title '{commit.message.title}' matches the regex " + \
|
if regex_method(commit.message.title):
|
||||||
f"'{self.options['regex'].value.pattern}', ignoring rules: {self.options['ignore'].value}"
|
config.ignore = self.options["ignore"].value
|
||||||
|
|
||||||
|
message = (
|
||||||
|
f"Commit title '{commit.message.title}' matches the regex "
|
||||||
|
f"'{self.options['regex'].value.pattern}', ignoring rules: {self.options['ignore'].value}"
|
||||||
|
)
|
||||||
|
|
||||||
self.log.debug("Ignoring commit because of rule '%s': %s", self.id, message)
|
self.log.debug("Ignoring commit because of rule '%s': %s", self.id, message)
|
||||||
|
|
||||||
|
@ -376,20 +409,27 @@ class IgnoreByTitle(ConfigurationRule):
|
||||||
class IgnoreByBody(ConfigurationRule):
|
class IgnoreByBody(ConfigurationRule):
|
||||||
name = "ignore-by-body"
|
name = "ignore-by-body"
|
||||||
id = "I2"
|
id = "I2"
|
||||||
options_spec = [RegexOption('regex', None, "Regex matching lines of the body of commits this rule should apply to"),
|
options_spec = [
|
||||||
StrOption('ignore', "all", "Comma-separated list of rules to ignore")]
|
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):
|
def apply(self, config, commit):
|
||||||
# If no regex is specified, immediately return
|
# If no regex is specified, immediately return
|
||||||
if not self.options['regex'].value:
|
if not self.options["regex"].value:
|
||||||
return
|
return
|
||||||
|
|
||||||
for line in commit.message.body:
|
# We're replacing regex match with search semantics, see https://github.com/jorisroovers/gitlint/issues/254
|
||||||
if self.options['regex'].value.match(line):
|
regex_method = Deprecation.get_regex_method(self, self.options["regex"])
|
||||||
config.ignore = self.options['ignore'].value
|
|
||||||
|
|
||||||
message = f"Commit message line '{line}' matches the regex '{self.options['regex'].value.pattern}'," + \
|
for line in commit.message.body:
|
||||||
f" ignoring rules: {self.options['ignore'].value}"
|
if regex_method(line):
|
||||||
|
config.ignore = self.options["ignore"].value
|
||||||
|
|
||||||
|
message = (
|
||||||
|
f"Commit message line '{line}' matches the regex '{self.options['regex'].value.pattern}',"
|
||||||
|
f" ignoring rules: {self.options['ignore'].value}"
|
||||||
|
)
|
||||||
|
|
||||||
self.log.debug("Ignoring commit because of rule '%s': %s", self.id, message)
|
self.log.debug("Ignoring commit because of rule '%s': %s", self.id, message)
|
||||||
# No need to check other lines if we found a match
|
# No need to check other lines if we found a match
|
||||||
|
@ -399,18 +439,21 @@ class IgnoreByBody(ConfigurationRule):
|
||||||
class IgnoreBodyLines(ConfigurationRule):
|
class IgnoreBodyLines(ConfigurationRule):
|
||||||
name = "ignore-body-lines"
|
name = "ignore-body-lines"
|
||||||
id = "I3"
|
id = "I3"
|
||||||
options_spec = [RegexOption('regex', None, "Regex matching lines of the body that should be ignored")]
|
options_spec = [RegexOption("regex", None, "Regex matching lines of the body that should be ignored")]
|
||||||
|
|
||||||
def apply(self, _, commit):
|
def apply(self, _, commit):
|
||||||
# If no regex is specified, immediately return
|
# If no regex is specified, immediately return
|
||||||
if not self.options['regex'].value:
|
if not self.options["regex"].value:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# We're replacing regex match with search semantics, see https://github.com/jorisroovers/gitlint/issues/254
|
||||||
|
regex_method = Deprecation.get_regex_method(self, self.options["regex"])
|
||||||
|
|
||||||
new_body = []
|
new_body = []
|
||||||
for line in commit.message.body:
|
for line in commit.message.body:
|
||||||
if self.options['regex'].value.match(line):
|
if regex_method(line):
|
||||||
debug_msg = "Ignoring line '%s' because it matches '%s'"
|
debug_msg = "Ignoring line '%s' because it matches '%s'"
|
||||||
self.log.debug(debug_msg, line, self.options['regex'].value.pattern)
|
self.log.debug(debug_msg, line, self.options["regex"].value.pattern)
|
||||||
else:
|
else:
|
||||||
new_body.append(line)
|
new_body.append(line)
|
||||||
|
|
||||||
|
@ -421,19 +464,25 @@ class IgnoreBodyLines(ConfigurationRule):
|
||||||
class IgnoreByAuthorName(ConfigurationRule):
|
class IgnoreByAuthorName(ConfigurationRule):
|
||||||
name = "ignore-by-author-name"
|
name = "ignore-by-author-name"
|
||||||
id = "I4"
|
id = "I4"
|
||||||
options_spec = [RegexOption('regex', None, "Regex matching the author name of commits this rule should apply to"),
|
options_spec = [
|
||||||
StrOption('ignore', "all", "Comma-separated list of rules to ignore")]
|
RegexOption("regex", None, "Regex matching the author name of commits this rule should apply to"),
|
||||||
|
StrOption("ignore", "all", "Comma-separated list of rules to ignore"),
|
||||||
|
]
|
||||||
|
|
||||||
def apply(self, config, commit):
|
def apply(self, config, commit):
|
||||||
# If no regex is specified, immediately return
|
# If no regex is specified, immediately return
|
||||||
if not self.options['regex'].value:
|
if not self.options["regex"].value:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.options['regex'].value.match(commit.author_name):
|
regex_method = Deprecation.get_regex_method(self, self.options["regex"])
|
||||||
config.ignore = self.options['ignore'].value
|
|
||||||
|
|
||||||
message = (f"Commit Author Name '{commit.author_name}' matches the regex "
|
if regex_method(commit.author_name):
|
||||||
f"'{self.options['regex'].value.pattern}', ignoring rules: {self.options['ignore'].value}")
|
config.ignore = self.options["ignore"].value
|
||||||
|
|
||||||
|
message = (
|
||||||
|
f"Commit Author Name '{commit.author_name}' matches the regex "
|
||||||
|
f"'{self.options['regex'].value.pattern}', ignoring rules: {self.options['ignore'].value}"
|
||||||
|
)
|
||||||
|
|
||||||
self.log.debug("Ignoring commit because of rule '%s': %s", self.id, message)
|
self.log.debug("Ignoring commit because of rule '%s': %s", self.id, message)
|
||||||
# No need to check other lines if we found a match
|
# No need to check other lines if we found a match
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
This module implements a shim for the 'sh' library, mainly for use on Windows (sh is not supported on Windows).
|
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 altogether in the future, but 'sh' does provide a few
|
We might consider removing the 'sh' dependency altogether in the future, but 'sh' does provide a few
|
||||||
|
@ -10,26 +9,28 @@ from gitlint.utils import USE_SH_LIB, DEFAULT_ENCODING
|
||||||
|
|
||||||
|
|
||||||
def shell(cmd):
|
def shell(cmd):
|
||||||
""" Convenience function that opens a given command in a shell. Does not use 'sh' library. """
|
"""Convenience function that opens a given command in a shell. Does not use 'sh' library."""
|
||||||
with subprocess.Popen(cmd, shell=True) as p:
|
with subprocess.Popen(cmd, shell=True) as p:
|
||||||
p.communicate()
|
p.communicate()
|
||||||
|
|
||||||
|
|
||||||
if USE_SH_LIB:
|
if USE_SH_LIB:
|
||||||
from sh import git # pylint: disable=unused-import,import-error
|
from sh import git # pylint: disable=unused-import,import-error
|
||||||
|
|
||||||
# import exceptions separately, this makes it a little easier to mock them out in the unit tests
|
# import exceptions separately, this makes it a little easier to mock them out in the unit tests
|
||||||
from sh import CommandNotFound, ErrorReturnCode # pylint: disable=import-error
|
from sh import CommandNotFound, ErrorReturnCode # pylint: disable=import-error
|
||||||
else:
|
else:
|
||||||
|
|
||||||
class CommandNotFound(Exception):
|
class CommandNotFound(Exception):
|
||||||
""" Exception indicating a command was not found during execution """
|
"""Exception indicating a command was not found during execution"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class ShResult:
|
class ShResult:
|
||||||
""" Result wrapper class. We use this to more easily migrate from using https://amoffat.github.io/sh/ to using
|
"""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):
|
def __init__(self, full_cmd, stdout, stderr="", exitcode=0):
|
||||||
self.full_cmd = full_cmd
|
self.full_cmd = full_cmd
|
||||||
self.stdout = stdout
|
self.stdout = stdout
|
||||||
self.stderr = stderr
|
self.stderr = stderr
|
||||||
|
@ -39,22 +40,23 @@ else:
|
||||||
return self.stdout
|
return self.stdout
|
||||||
|
|
||||||
class ErrorReturnCode(ShResult, Exception):
|
class ErrorReturnCode(ShResult, Exception):
|
||||||
""" ShResult subclass for unexpected results (acts as an exception). """
|
"""ShResult subclass for unexpected results (acts as an exception)."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def git(*command_parts, **kwargs):
|
def git(*command_parts, **kwargs):
|
||||||
""" Git shell wrapper.
|
"""Git shell wrapper.
|
||||||
Implemented as separate function here, so we can do a 'sh' style imports:
|
Implemented as separate function here, so we can do a 'sh' style imports:
|
||||||
`from shell import git`
|
`from shell import git`
|
||||||
"""
|
"""
|
||||||
args = ['git'] + list(command_parts)
|
args = ["git"] + list(command_parts)
|
||||||
return _exec(*args, **kwargs)
|
return _exec(*args, **kwargs)
|
||||||
|
|
||||||
def _exec(*args, **kwargs):
|
def _exec(*args, **kwargs):
|
||||||
pipe = subprocess.PIPE
|
pipe = subprocess.PIPE
|
||||||
popen_kwargs = {'stdout': pipe, 'stderr': pipe, 'shell': kwargs.get('_tty_out', False)}
|
popen_kwargs = {"stdout": pipe, "stderr": pipe, "shell": kwargs.get("_tty_out", False)}
|
||||||
if '_cwd' in kwargs:
|
if "_cwd" in kwargs:
|
||||||
popen_kwargs['cwd'] = kwargs['_cwd']
|
popen_kwargs["cwd"] = kwargs["_cwd"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with subprocess.Popen(args, **popen_kwargs) as p:
|
with subprocess.Popen(args, **popen_kwargs) as p:
|
||||||
|
@ -65,10 +67,10 @@ else:
|
||||||
exit_code = p.returncode
|
exit_code = p.returncode
|
||||||
stdout = result[0].decode(DEFAULT_ENCODING)
|
stdout = result[0].decode(DEFAULT_ENCODING)
|
||||||
stderr = result[1] # 'sh' does not decode the stderr bytes to unicode
|
stderr = result[1] # 'sh' does not decode the stderr bytes to unicode
|
||||||
full_cmd = '' if args is None else ' '.join(args)
|
full_cmd = "" if args is None else " ".join(args)
|
||||||
|
|
||||||
# If not _ok_code is specified, then only a 0 exit code is allowed
|
# If not _ok_code is specified, then only a 0 exit code is allowed
|
||||||
ok_exit_codes = kwargs.get('_ok_code', [0])
|
ok_exit_codes = kwargs.get("_ok_code", [0])
|
||||||
|
|
||||||
if exit_code in ok_exit_codes:
|
if exit_code in ok_exit_codes:
|
||||||
return ShResult(full_cmd, stdout, stderr, exit_code)
|
return ShResult(full_cmd, stdout, stderr, exit_code)
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import copy
|
import copy
|
||||||
import io
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -13,12 +10,22 @@ import unittest
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from gitlint.git import GitContext
|
from gitlint.config import LintConfig
|
||||||
|
from gitlint.deprecation import Deprecation, LOG as DEPRECATION_LOG
|
||||||
|
from gitlint.git import GitContext, GitChangedFileStats
|
||||||
from gitlint.utils import LOG_FORMAT, DEFAULT_ENCODING
|
from gitlint.utils import LOG_FORMAT, DEFAULT_ENCODING
|
||||||
|
|
||||||
|
EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING = (
|
||||||
|
"WARNING: gitlint.deprecated.regex_style_search {0} - {1}: gitlint will be switching from using "
|
||||||
|
"Python regex 'match' (match beginning) to 'search' (match anywhere) semantics. "
|
||||||
|
"Please review your {1}.regex option accordingly. "
|
||||||
|
"To remove this warning, set general.regex-style-search=True. More details: "
|
||||||
|
"https://jorisroovers.github.io/gitlint/configuration/#regex-style-search"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseTestCase(unittest.TestCase):
|
class BaseTestCase(unittest.TestCase):
|
||||||
""" Base class of which all gitlint unit test classes are derived. Provides a number of convenience methods. """
|
"""Base class of which all gitlint unit test classes are derived. Provides a number of convenience methods."""
|
||||||
|
|
||||||
# In case of assert failures, print the full error message
|
# In case of assert failures, print the full error message
|
||||||
maxDiff = None
|
maxDiff = None
|
||||||
|
@ -30,13 +37,24 @@ class BaseTestCase(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.logcapture = LogCapture()
|
self.logcapture = LogCapture()
|
||||||
self.logcapture.setFormatter(logging.Formatter(LOG_FORMAT))
|
self.logcapture.setFormatter(logging.Formatter(LOG_FORMAT))
|
||||||
logging.getLogger('gitlint').setLevel(logging.DEBUG)
|
logging.getLogger("gitlint").setLevel(logging.DEBUG)
|
||||||
logging.getLogger('gitlint').handlers = [self.logcapture]
|
logging.getLogger("gitlint").handlers = [self.logcapture]
|
||||||
|
DEPRECATION_LOG.handlers = [self.logcapture]
|
||||||
|
|
||||||
# Make sure we don't propagate anything to child loggers, we need to do this explicitly here
|
# Make sure we don't propagate anything to child loggers, we need to do this explicitly here
|
||||||
# because if you run a specific test file like test_lint.py, we won't be calling the setupLogging() method
|
# because if you run a specific test file like test_lint.py, we won't be calling the setupLogging() method
|
||||||
# in gitlint.cli that normally takes care of this
|
# in gitlint.cli that normally takes care of this
|
||||||
logging.getLogger('gitlint').propagate = False
|
# Example test where this matters (for DEPRECATION_LOG):
|
||||||
|
# gitlint-core/gitlint/tests/rules/test_configuration_rules.py::ConfigurationRuleTests::test_ignore_by_title
|
||||||
|
logging.getLogger("gitlint").propagate = False
|
||||||
|
DEPRECATION_LOG.propagate = False
|
||||||
|
|
||||||
|
# Make sure Deprecation has a clean config set at the start of each test.
|
||||||
|
# Tests that want to specifically test deprecation should override this.
|
||||||
|
Deprecation.config = LintConfig()
|
||||||
|
# Normally Deprecation only logs messages once per process.
|
||||||
|
# For tests we want to log every time, so we reset the warning_msgs set per test.
|
||||||
|
Deprecation.warning_msgs = set()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
|
@ -57,25 +75,25 @@ class BaseTestCase(unittest.TestCase):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_sample(filename=""):
|
def get_sample(filename=""):
|
||||||
""" Read and return the contents of a file in gitlint/tests/samples """
|
"""Read and return the contents of a file in gitlint/tests/samples"""
|
||||||
sample_path = BaseTestCase.get_sample_path(filename)
|
sample_path = BaseTestCase.get_sample_path(filename)
|
||||||
with io.open(sample_path, encoding=DEFAULT_ENCODING) as content:
|
with open(sample_path, encoding=DEFAULT_ENCODING) as content:
|
||||||
sample = content.read()
|
sample = content.read()
|
||||||
return sample
|
return sample
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def patch_input(side_effect):
|
def patch_input(side_effect):
|
||||||
""" Patches the built-in input() with a provided side-effect """
|
"""Patches the built-in input() with a provided side-effect"""
|
||||||
module_path = "builtins.input"
|
module_path = "builtins.input"
|
||||||
patched_module = patch(module_path, side_effect=side_effect)
|
patched_module = patch(module_path, side_effect=side_effect)
|
||||||
return patched_module
|
return patched_module
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_expected(filename="", variable_dict=None):
|
def get_expected(filename="", variable_dict=None):
|
||||||
""" Utility method to read an expected file from gitlint/tests/expected and return it as a string.
|
"""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. """
|
Optionally replace template variables specified by variable_dict."""
|
||||||
expected_path = os.path.join(BaseTestCase.EXPECTED_DIR, filename)
|
expected_path = os.path.join(BaseTestCase.EXPECTED_DIR, filename)
|
||||||
with io.open(expected_path, encoding=DEFAULT_ENCODING) as content:
|
with open(expected_path, encoding=DEFAULT_ENCODING) as content:
|
||||||
expected = content.read()
|
expected = content.read()
|
||||||
|
|
||||||
if variable_dict:
|
if variable_dict:
|
||||||
|
@ -87,20 +105,21 @@ class BaseTestCase(unittest.TestCase):
|
||||||
return os.path.join(BaseTestCase.SAMPLES_DIR, "user_rules")
|
return os.path.join(BaseTestCase.SAMPLES_DIR, "user_rules")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def gitcontext(commit_msg_str, changed_files=None, ):
|
def gitcontext(commit_msg_str, changed_files=None):
|
||||||
""" Utility method to easily create gitcontext objects based on a given commit msg string and an optional set of
|
"""Utility method to easily create gitcontext objects based on a given commit msg string and an optional set of
|
||||||
changed files"""
|
changed files"""
|
||||||
with patch("gitlint.git.git_commentchar") as comment_char:
|
with patch("gitlint.git.git_commentchar") as comment_char:
|
||||||
comment_char.return_value = "#"
|
comment_char.return_value = "#"
|
||||||
gitcontext = GitContext.from_commit_msg(commit_msg_str)
|
gitcontext = GitContext.from_commit_msg(commit_msg_str)
|
||||||
commit = gitcontext.commits[-1]
|
commit = gitcontext.commits[-1]
|
||||||
if changed_files:
|
if changed_files:
|
||||||
commit.changed_files = changed_files
|
changed_file_stats = {filename: GitChangedFileStats(filename, 8, 3) for filename in changed_files}
|
||||||
|
commit.changed_files_stats = changed_file_stats
|
||||||
return gitcontext
|
return gitcontext
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def gitcommit(commit_msg_str, changed_files=None, **kwargs):
|
def gitcommit(commit_msg_str, changed_files=None, **kwargs):
|
||||||
""" Utility method to easily create git commit given a commit msg string and an optional set of changed files"""
|
"""Utility method to easily create git commit given a commit msg string and an optional set of changed files"""
|
||||||
gitcontext = BaseTestCase.gitcontext(commit_msg_str, changed_files)
|
gitcontext = BaseTestCase.gitcontext(commit_msg_str, changed_files)
|
||||||
commit = gitcontext.commits[-1]
|
commit = gitcontext.commits[-1]
|
||||||
for attr, value in kwargs.items():
|
for attr, value in kwargs.items():
|
||||||
|
@ -108,31 +127,31 @@ class BaseTestCase(unittest.TestCase):
|
||||||
return commit
|
return commit
|
||||||
|
|
||||||
def assert_logged(self, expected):
|
def assert_logged(self, expected):
|
||||||
""" Asserts that the logs match an expected string or list.
|
"""Asserts that the logs match an expected string or list.
|
||||||
This method knows how to compare a passed list of log lines as well as a newline concatenated string
|
This method knows how to compare a passed list of log lines as well as a newline concatenated string
|
||||||
of all loglines. """
|
of all loglines."""
|
||||||
if isinstance(expected, list):
|
if isinstance(expected, list):
|
||||||
self.assertListEqual(self.logcapture.messages, expected)
|
self.assertListEqual(self.logcapture.messages, expected)
|
||||||
else:
|
else:
|
||||||
self.assertEqual("\n".join(self.logcapture.messages), expected)
|
self.assertEqual("\n".join(self.logcapture.messages), expected)
|
||||||
|
|
||||||
def assert_log_contains(self, line):
|
def assert_log_contains(self, line):
|
||||||
""" Asserts that a certain line is in the logs """
|
"""Asserts that a certain line is in the logs"""
|
||||||
self.assertIn(line, self.logcapture.messages)
|
self.assertIn(line, self.logcapture.messages)
|
||||||
|
|
||||||
def assertRaisesRegex(self, expected_exception, expected_regex, *args, **kwargs):
|
def assertRaisesRegex(self, expected_exception, expected_regex, *args, **kwargs):
|
||||||
""" Pass-through method to unittest.TestCase.assertRaisesRegex that applies re.escape() to the passed
|
"""Pass-through method to unittest.TestCase.assertRaisesRegex that applies re.escape() to the passed
|
||||||
`expected_regex`. This is useful to automatically escape all file paths that might be present in the regex.
|
`expected_regex`. This is useful to automatically escape all file paths that might be present in the regex.
|
||||||
"""
|
"""
|
||||||
return super().assertRaisesRegex(expected_exception, re.escape(expected_regex), *args, **kwargs)
|
return super().assertRaisesRegex(expected_exception, re.escape(expected_regex), *args, **kwargs)
|
||||||
|
|
||||||
def clearlog(self):
|
def clearlog(self):
|
||||||
""" Clears the log capture """
|
"""Clears the log capture"""
|
||||||
self.logcapture.clear()
|
self.logcapture.clear()
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def assertRaisesMessage(self, expected_exception, expected_msg): # pylint: disable=invalid-name
|
def assertRaisesMessage(self, expected_exception, expected_msg): # pylint: disable=invalid-name
|
||||||
""" Asserts an exception has occurred with a given error message """
|
"""Asserts an exception has occurred with a given error message"""
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
except expected_exception as exc:
|
except expected_exception as exc:
|
||||||
|
@ -149,10 +168,10 @@ class BaseTestCase(unittest.TestCase):
|
||||||
raise self.fail(f"Expected to raise {expected_exception.__name__}, didn't get an exception at all")
|
raise self.fail(f"Expected to raise {expected_exception.__name__}, didn't get an exception at all")
|
||||||
|
|
||||||
def object_equality_test(self, obj, attr_list, ctor_kwargs=None):
|
def object_equality_test(self, obj, attr_list, ctor_kwargs=None):
|
||||||
""" Helper function to easily implement object equality tests.
|
"""Helper function to easily implement object equality tests.
|
||||||
Creates an object clone for every passed attribute and checks for (in)equality
|
Creates an object clone for every passed attribute and checks for (in)equality
|
||||||
of the original object with the clone based on those attributes' values.
|
of the original object with the clone based on those attributes' values.
|
||||||
This function assumes all attributes in `attr_list` can be passed to the ctor of `obj.__class__`.
|
This function assumes all attributes in `attr_list` can be passed to the ctor of `obj.__class__`.
|
||||||
"""
|
"""
|
||||||
if not ctor_kwargs:
|
if not ctor_kwargs:
|
||||||
ctor_kwargs = {}
|
ctor_kwargs = {}
|
||||||
|
@ -178,7 +197,7 @@ class BaseTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
|
||||||
class LogCapture(logging.Handler):
|
class LogCapture(logging.Handler):
|
||||||
""" Mock logging handler used to capture any log messages during tests."""
|
"""Mock logging handler used to capture any log messages during tests."""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
logging.Handler.__init__(self, *args, **kwargs)
|
logging.Handler.__init__(self, *args, **kwargs)
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
@ -29,11 +26,11 @@ class CLITests(BaseTestCase):
|
||||||
GITLINT_SUCCESS_CODE = 0
|
GITLINT_SUCCESS_CODE = 0
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(CLITests, self).setUp()
|
super().setUp()
|
||||||
self.cli = CliRunner()
|
self.cli = CliRunner()
|
||||||
|
|
||||||
# Patch gitlint.cli.git_version() so that we don't have to patch it separately in every test
|
# Patch gitlint.cli.git_version() so that we don't have to patch it separately in every test
|
||||||
self.git_version_path = patch('gitlint.cli.git_version')
|
self.git_version_path = patch("gitlint.cli.git_version")
|
||||||
cli.git_version = self.git_version_path.start()
|
cli.git_version = self.git_version_path.start()
|
||||||
cli.git_version.return_value = "git version 1.2.3"
|
cli.git_version.return_value = "git version 1.2.3"
|
||||||
|
|
||||||
|
@ -42,39 +39,44 @@ class CLITests(BaseTestCase):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_system_info_dict():
|
def get_system_info_dict():
|
||||||
""" Returns a dict with items related to system values logged by `gitlint --debug` """
|
"""Returns a dict with items related to system values logged by `gitlint --debug`"""
|
||||||
return {'platform': platform.platform(), "python_version": sys.version, 'gitlint_version': __version__,
|
return {
|
||||||
'GITLINT_USE_SH_LIB': BaseTestCase.GITLINT_USE_SH_LIB, 'target': os.path.realpath(os.getcwd()),
|
"platform": platform.platform(),
|
||||||
'DEFAULT_ENCODING': DEFAULT_ENCODING}
|
"python_version": sys.version,
|
||||||
|
"gitlint_version": __version__,
|
||||||
|
"GITLINT_USE_SH_LIB": BaseTestCase.GITLINT_USE_SH_LIB,
|
||||||
|
"target": os.path.realpath(os.getcwd()),
|
||||||
|
"DEFAULT_ENCODING": DEFAULT_ENCODING,
|
||||||
|
}
|
||||||
|
|
||||||
def test_version(self):
|
def test_version(self):
|
||||||
""" Test for --version option """
|
"""Test for --version option"""
|
||||||
result = self.cli.invoke(cli.cli, ["--version"])
|
result = self.cli.invoke(cli.cli, ["--version"])
|
||||||
self.assertEqual(result.output.split("\n")[0], f"cli, version {__version__}")
|
self.assertEqual(result.output.split("\n")[0], f"cli, version {__version__}")
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_lint(self, sh, _):
|
def test_lint(self, sh, _):
|
||||||
""" Test for basic simple linting functionality """
|
"""Test for basic simple linting functionality"""
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
"6f29bf81a8322a04071bb794666e48c443a90360",
|
"6f29bf81a8322a04071bb794666e48c443a90360",
|
||||||
"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\ncommït-title\n\ncommït-body",
|
||||||
"commït-title\n\ncommït-body",
|
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
|
"1\t4\tfile1.txt\n3\t5\tpåth/to/file2.txt\n",
|
||||||
"commit-1-branch-1\ncommit-1-branch-2\n",
|
"commit-1-branch-1\ncommit-1-branch-2\n",
|
||||||
"file1.txt\npåth/to/file2.txt\n"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli)
|
result = self.cli.invoke(cli.cli)
|
||||||
self.assertEqual(stderr.getvalue(), u'3: B5 Body message is too short (11<20): "commït-body"\n')
|
self.assertEqual(stderr.getvalue(), '3: B5 Body message is too short (11<20): "commït-body"\n')
|
||||||
self.assertEqual(result.exit_code, 1)
|
self.assertEqual(result.exit_code, 1)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_lint_multiple_commits(self, sh, _):
|
def test_lint_multiple_commits(self, sh, _):
|
||||||
""" Test for --commits option """
|
"""Test for --commits option"""
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
"6f29bf81a8322a04071bb794666e48c443a90360\n" + # git rev-list <SHA>
|
"6f29bf81a8322a04071bb794666e48c443a90360\n" + # git rev-list <SHA>
|
||||||
"25053ccec5e28e1bb8f7551fdbb5ab213ada2401\n" +
|
"25053ccec5e28e1bb8f7551fdbb5ab213ada2401\n" +
|
||||||
|
@ -83,30 +85,32 @@ class CLITests(BaseTestCase):
|
||||||
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
||||||
"commït-title1\n\ncommït-body1",
|
"commït-title1\n\ncommït-body1",
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
|
"3\t5\tcommit-1/file-1\n1\t4\tcommit-1/file-2\n", # git diff-tree
|
||||||
"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
|
"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
|
||||||
"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
|
|
||||||
# git log --pretty <FORMAT> <SHA>
|
# git log --pretty <FORMAT> <SHA>
|
||||||
"test åuthor2\x00test-email3@föo.com\x002016-12-04 15:28:15 +0100\x00åbc\n"
|
"test åuthor2\x00test-email3@föo.com\x002016-12-04 15:28:15 +0100\x00åbc\n"
|
||||||
"commït-title2\n\ncommït-body2",
|
"commït-title2\n\ncommït-body2",
|
||||||
|
"8\t3\tcommit-2/file-1\n1\t5\tcommit-2/file-2\n", # git diff-tree
|
||||||
"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
|
"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
|
||||||
"commit-2/file-1\ncommit-2/file-2\n", # git diff-tree
|
|
||||||
# git log --pretty <FORMAT> <SHA>
|
# git log --pretty <FORMAT> <SHA>
|
||||||
"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00åbc\n"
|
"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00åbc\n"
|
||||||
"commït-title3\n\ncommït-body3",
|
"commït-title3\n\ncommït-body3",
|
||||||
|
"7\t2\tcommit-3/file-1\n1\t7\tcommit-3/file-2\n", # git diff-tree
|
||||||
"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
|
"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
|
||||||
"commit-3/file-1\ncommit-3/file-2\n", # git diff-tree
|
|
||||||
]
|
]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["--commits", "foo...bar"])
|
result = self.cli.invoke(cli.cli, ["--commits", "foo...bar"])
|
||||||
self.assertEqual(stderr.getvalue(), self.get_expected("cli/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)
|
self.assertEqual(result.exit_code, 3)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_lint_multiple_commits_config(self, sh, _):
|
def test_lint_multiple_commits_config(self, sh, _):
|
||||||
""" Test for --commits option where some of the commits have gitlint config in the commit message """
|
"""Test for --commits option where some of the commits have gitlint config in the commit message"""
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
# Note that the second commit title has a trailing period that is being ignored by gitlint-ignore: T3
|
# Note that the second commit title has a trailing period that is being ignored by gitlint-ignore: T3
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
"6f29bf81a8322a04071bb794666e48c443a90360\n" + # git rev-list <SHA>
|
"6f29bf81a8322a04071bb794666e48c443a90360\n" + # git rev-list <SHA>
|
||||||
|
@ -116,32 +120,33 @@ class CLITests(BaseTestCase):
|
||||||
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
||||||
"commït-title1\n\ncommït-body1",
|
"commït-title1\n\ncommït-body1",
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
|
"9\t4\tcommit-1/file-1\n0\t2\tcommit-1/file-2\n", # git diff-tree
|
||||||
"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
|
"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
|
||||||
"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
|
|
||||||
# git log --pretty <FORMAT> <SHA>
|
# git log --pretty <FORMAT> <SHA>
|
||||||
"test åuthor2\x00test-email2@föo.com\x002016-12-04 15:28:15 +0100\x00åbc\n"
|
"test åuthor2\x00test-email2@föo.com\x002016-12-04 15:28:15 +0100\x00åbc\n"
|
||||||
"commït-title2.\n\ncommït-body2\ngitlint-ignore: T3\n",
|
"commït-title2.\n\ncommït-body2\ngitlint-ignore: T3\n",
|
||||||
|
"3\t7\tcommit-2/file-1\n4\t6\tcommit-2/file-2\n", # git diff-tree
|
||||||
"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
|
"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
|
||||||
"commit-2/file-1\ncommit-2/file-2\n", # git diff-tree
|
|
||||||
# git log --pretty <FORMAT> <SHA>
|
# git log --pretty <FORMAT> <SHA>
|
||||||
"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00åbc\n"
|
"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00åbc\n"
|
||||||
"commït-title3.\n\ncommït-body3",
|
"commït-title3.\n\ncommït-body3",
|
||||||
|
"3\t8\tcommit-3/file-1\n1\t4\tcommit-3/file-2\n", # git diff-tree
|
||||||
"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
|
"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
|
||||||
"commit-3/file-1\ncommit-3/file-2\n", # git diff-tree
|
|
||||||
]
|
]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["--commits", "foo...bar"])
|
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
|
# 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("cli/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)
|
self.assertEqual(result.exit_code, 3)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_lint_multiple_commits_configuration_rules(self, sh, _):
|
def test_lint_multiple_commits_configuration_rules(self, sh, _):
|
||||||
""" Test for --commits option where where we have configured gitlint to ignore certain rules for certain commits
|
"""Test for --commits option where where we have configured gitlint to ignore certain rules for certain commits"""
|
||||||
"""
|
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
# Note that the second commit
|
# Note that the second commit
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
"6f29bf81a8322a04071bb794666e48c443a90360\n" + # git rev-list <SHA>
|
"6f29bf81a8322a04071bb794666e48c443a90360\n" + # git rev-list <SHA>
|
||||||
|
@ -151,62 +156,78 @@ class CLITests(BaseTestCase):
|
||||||
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
||||||
"commït-title1\n\ncommït-body1",
|
"commït-title1\n\ncommït-body1",
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
|
"5\t9\tcommit-1/file-1\n1\t4\tcommit-1/file-2\n", # git diff-tree
|
||||||
"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
|
"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
|
||||||
"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
|
|
||||||
# git log --pretty <FORMAT> <SHA>
|
# git log --pretty <FORMAT> <SHA>
|
||||||
"test åuthor2\x00test-email3@föo.com\x002016-12-04 15:28:15 +0100\x00åbc\n"
|
"test åuthor2\x00test-email3@föo.com\x002016-12-04 15:28:15 +0100\x00åbc\n"
|
||||||
# Normally T3 violation (trailing punctuation), but this commit is ignored because of
|
# Normally T3 violation (trailing punctuation), but this commit is ignored because of
|
||||||
# config below
|
# config below
|
||||||
"commït-title2.\n\ncommït-body2\n",
|
"commït-title2.\n\ncommït-body2\n",
|
||||||
|
"4\t7\tcommit-2/file-1\n1\t4\tcommit-2/file-2\n", # git diff-tree
|
||||||
"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
|
"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
|
||||||
"commit-2/file-1\ncommit-2/file-2\n", # git diff-tree
|
|
||||||
# git log --pretty <FORMAT> <SHA>
|
# git log --pretty <FORMAT> <SHA>
|
||||||
"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00åbc\n"
|
"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00åbc\n"
|
||||||
# Normally T1 and B5 violations, now only T1 because we're ignoring B5 in config below
|
# Normally T1 and B5 violations, now only T1 because we're ignoring B5 in config below
|
||||||
"commït-title3.\n\ncommït-body3 foo",
|
"commït-title3.\n\ncommït-body3 foo",
|
||||||
|
"1\t9\tcommit-3/file-1\n3\t7\tcommit-3/file-2\n", # git diff-tree
|
||||||
"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
|
"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
|
||||||
"commit-3/file-1\ncommit-3/file-2\n", # git diff-tree
|
|
||||||
]
|
]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["--commits", "foo...bar", "-c", "I1.regex=^commït-title2(.*)",
|
result = self.cli.invoke(
|
||||||
"-c", "I2.regex=^commït-body3(.*)", "-c", "I2.ignore=B5"])
|
cli.cli,
|
||||||
|
[
|
||||||
|
"--commits",
|
||||||
|
"foo...bar",
|
||||||
|
"-c",
|
||||||
|
"I1.regex=^commït-title2(.*)",
|
||||||
|
"-c",
|
||||||
|
"I2.regex=^commït-body3(.*)",
|
||||||
|
"-c",
|
||||||
|
"I2.ignore=B5",
|
||||||
|
],
|
||||||
|
)
|
||||||
# We expect that the second commit has no failures because of it matching against I1.regex
|
# We expect that the second commit has no failures because of it matching against I1.regex
|
||||||
# Because we do test for the 3th commit to return violations, this test also ensures that a unique
|
# Because we do test for the 3th commit to return violations, this test also ensures that a unique
|
||||||
# config object is passed to each commit lint call
|
# config object is passed to each commit lint call
|
||||||
expected = ("Commit 6f29bf81a8:\n"
|
expected = (
|
||||||
u'3: B5 Body message is too short (12<20): "commït-body1"\n\n'
|
"Commit 6f29bf81a8:\n"
|
||||||
"Commit 4da2656b0d:\n"
|
'3: B5 Body message is too short (12<20): "commït-body1"\n\n'
|
||||||
u'1: T3 Title has trailing punctuation (.): "commït-title3."\n')
|
"Commit 4da2656b0d:\n"
|
||||||
|
'1: T3 Title has trailing punctuation (.): "commït-title3."\n'
|
||||||
|
)
|
||||||
self.assertEqual(stderr.getvalue(), expected)
|
self.assertEqual(stderr.getvalue(), expected)
|
||||||
self.assertEqual(result.exit_code, 2)
|
self.assertEqual(result.exit_code, 2)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_lint_commit(self, sh, _):
|
def test_lint_commit(self, sh, _):
|
||||||
""" Test for --commit option """
|
"""Test for --commit option"""
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
"6f29bf81a8322a04071bb794666e48c443a90360\n", # git log -1 <SHA> --pretty=%H
|
"6f29bf81a8322a04071bb794666e48c443a90360\n", # git log -1 <SHA> --pretty=%H
|
||||||
# git log --pretty <FORMAT> <SHA>
|
# git log --pretty <FORMAT> <SHA>
|
||||||
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
||||||
"WIP: commït-title1\n\ncommït-body1",
|
"WIP: commït-title1\n\ncommït-body1",
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
|
"4\t5\tcommit-1/file-1\n1\t4\tcommit-1/file-2\n", # git diff-tree
|
||||||
"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
|
"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
|
||||||
"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
|
|
||||||
]
|
]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["--commit", "foo"])
|
result = self.cli.invoke(cli.cli, ["--commit", "foo"])
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
|
|
||||||
self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_commit_1"))
|
self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_commit_1"))
|
||||||
self.assertEqual(result.exit_code, 2)
|
self.assertEqual(result.exit_code, 2)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_lint_commit_negative(self, sh, _):
|
def test_lint_commit_negative(self, sh, _):
|
||||||
""" Negative test for --commit option """
|
"""Negative test for --commit option"""
|
||||||
|
|
||||||
# Try using --commit and --commits at the same time (not allowed)
|
# Try using --commit and --commits at the same time (not allowed)
|
||||||
result = self.cli.invoke(cli.cli, ["--commit", "foo", "--commits", "foo...bar"])
|
result = self.cli.invoke(cli.cli, ["--commit", "foo", "--commits", "foo...bar"])
|
||||||
|
@ -214,275 +235,309 @@ class CLITests(BaseTestCase):
|
||||||
self.assertEqual(result.output, expected_output)
|
self.assertEqual(result.output, expected_output)
|
||||||
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=u'WIP: tïtle \n')
|
@patch("gitlint.cli.get_stdin_data", return_value="WIP: tïtle \n")
|
||||||
def test_input_stream(self, _):
|
def test_input_stream(self, _):
|
||||||
""" Test for linting when a message is passed via stdin """
|
"""Test for linting when a message is passed via stdin"""
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli)
|
result = self.cli.invoke(cli.cli)
|
||||||
self.assertEqual(stderr.getvalue(), self.get_expected("cli/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.exit_code, 3)
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=u'WIP: tïtle \n')
|
@patch("gitlint.cli.get_stdin_data", return_value="WIP: tïtle \n")
|
||||||
def test_input_stream_debug(self, _):
|
def test_input_stream_debug(self, _):
|
||||||
""" Test for linting when a message is passed via stdin, and debug is enabled.
|
"""Test for linting when a message is passed via stdin, and debug is enabled.
|
||||||
This tests specifically that git commit meta is not fetched when not passing --staged """
|
This tests specifically that git commit meta is not fetched when not passing --staged"""
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["--debug"])
|
result = self.cli.invoke(cli.cli, ["--debug"])
|
||||||
self.assertEqual(stderr.getvalue(), self.get_expected("cli/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.exit_code, 3)
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
expected_kwargs = self.get_system_info_dict()
|
expected_kwargs = self.get_system_info_dict()
|
||||||
expected_logs = self.get_expected('cli/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)
|
self.assert_logged(expected_logs)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value="Should be ignored\n")
|
@patch("gitlint.cli.get_stdin_data", return_value="Should be ignored\n")
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_lint_ignore_stdin(self, sh, stdin_data):
|
def test_lint_ignore_stdin(self, sh, stdin_data):
|
||||||
""" Test for ignoring stdin when --ignore-stdin flag is enabled"""
|
"""Test for ignoring stdin when --ignore-stdin flag is enabled"""
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
"6f29bf81a8322a04071bb794666e48c443a90360",
|
"6f29bf81a8322a04071bb794666e48c443a90360",
|
||||||
"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\ncommït-title\n\ncommït-body",
|
||||||
"commït-title\n\ncommït-body",
|
"#", # git config --get core.commentchar
|
||||||
"#", # git config --get core.commentchar
|
"3\t12\tfile1.txt\n8\t5\tpåth/to/file2.txt\n", # git diff-tree
|
||||||
"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
|
"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
|
||||||
"file1.txt\npåth/to/file2.txt\n" # git diff-tree
|
|
||||||
]
|
]
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["--ignore-stdin"])
|
result = self.cli.invoke(cli.cli, ["--ignore-stdin"])
|
||||||
self.assertEqual(stderr.getvalue(), u'3: B5 Body message is too short (11<20): "commït-body"\n')
|
self.assertEqual(stderr.getvalue(), '3: B5 Body message is too short (11<20): "commït-body"\n')
|
||||||
self.assertEqual(result.exit_code, 1)
|
self.assertEqual(result.exit_code, 1)
|
||||||
|
|
||||||
# Assert that we didn't even try to get the stdin data
|
# Assert that we didn't even try to get the stdin data
|
||||||
self.assertEqual(stdin_data.call_count, 0)
|
self.assertEqual(stdin_data.call_count, 0)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=u'WIP: tïtle \n')
|
@patch("gitlint.cli.get_stdin_data", return_value="WIP: tïtle \n")
|
||||||
@patch('arrow.now', return_value=arrow.get("2020-02-19T12:18:46.675182+01:00"))
|
@patch("arrow.now", return_value=arrow.get("2020-02-19T12:18:46.675182+01:00"))
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_lint_staged_stdin(self, sh, _, __):
|
def test_lint_staged_stdin(self, sh, _, __):
|
||||||
""" Test for ignoring stdin when --ignore-stdin flag is enabled"""
|
"""Test for ignoring stdin when --ignore-stdin flag is enabled"""
|
||||||
|
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
"föo user\n", # git config --get user.name
|
"1\t5\tcommit-1/file-1\n8\t9\tcommit-1/file-2\n", # git diff-tree
|
||||||
"föo@bar.com\n", # git config --get user.email
|
"föo user\n", # git config --get user.name
|
||||||
"my-branch\n", # git rev-parse --abbrev-ref HEAD (=current branch)
|
"föo@bar.com\n", # git config --get user.email
|
||||||
"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
|
"my-branch\n", # git rev-parse --abbrev-ref HEAD (=current branch)
|
||||||
]
|
]
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["--debug", "--staged"])
|
result = self.cli.invoke(cli.cli, ["--debug", "--staged"])
|
||||||
self.assertEqual(stderr.getvalue(), self.get_expected("cli/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.exit_code, 3)
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
|
|
||||||
expected_kwargs = self.get_system_info_dict()
|
expected_kwargs = self.get_system_info_dict()
|
||||||
expected_logs = self.get_expected('cli/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)
|
self.assert_logged(expected_logs)
|
||||||
|
|
||||||
@patch('arrow.now', return_value=arrow.get("2020-02-19T12:18:46.675182+01:00"))
|
@patch("arrow.now", return_value=arrow.get("2020-02-19T12:18:46.675182+01:00"))
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_lint_staged_msg_filename(self, sh, _):
|
def test_lint_staged_msg_filename(self, sh, _):
|
||||||
""" Test for ignoring stdin when --ignore-stdin flag is enabled"""
|
"""Test for ignoring stdin when --ignore-stdin flag is enabled"""
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
|
"3\t4\tcommit-1/file-1\n4\t7\tcommit-1/file-2\n", # git diff-tree
|
||||||
"föo user\n", # git config --get user.name
|
"föo user\n", # git config --get user.name
|
||||||
"föo@bar.com\n", # git config --get user.email
|
"föo@bar.com\n", # git config --get user.email
|
||||||
"my-branch\n", # git rev-parse --abbrev-ref HEAD (=current branch)
|
"my-branch\n", # git rev-parse --abbrev-ref HEAD (=current branch)
|
||||||
"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
|
|
||||||
]
|
]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
with self.tempdir() as tmpdir:
|
with self.tempdir() as tmpdir:
|
||||||
msg_filename = os.path.join(tmpdir, "msg")
|
msg_filename = os.path.join(tmpdir, "msg")
|
||||||
with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
|
with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f:
|
||||||
f.write("WIP: msg-filename tïtle\n")
|
f.write("WIP: msg-filename tïtle\n")
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["--debug", "--staged", "--msg-filename", msg_filename])
|
result = self.cli.invoke(cli.cli, ["--debug", "--staged", "--msg-filename", msg_filename])
|
||||||
self.assertEqual(stderr.getvalue(), self.get_expected("cli/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.exit_code, 2)
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
|
|
||||||
expected_kwargs = self.get_system_info_dict()
|
expected_kwargs = self.get_system_info_dict()
|
||||||
expected_logs = self.get_expected('cli/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)
|
self.assert_logged(expected_logs)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||||
def test_lint_staged_negative(self, _):
|
def test_lint_staged_negative(self, _):
|
||||||
result = self.cli.invoke(cli.cli, ["--staged"])
|
result = self.cli.invoke(cli.cli, ["--staged"])
|
||||||
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
||||||
self.assertEqual(result.output, ("Error: The 'staged' option (--staged) can only be used when using "
|
self.assertEqual(
|
||||||
"'--msg-filename' or when piping data to gitlint via stdin.\n"))
|
result.output,
|
||||||
|
"Error: The 'staged' option (--staged) can only be used when using "
|
||||||
|
"'--msg-filename' or when piping data to gitlint via stdin.\n",
|
||||||
|
)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_fail_without_commits(self, sh, _):
|
def test_fail_without_commits(self, sh, _):
|
||||||
""" Test for --debug option """
|
"""Test for --debug option"""
|
||||||
|
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = ["", ""] # First invocation of git rev-list # Second invocation of git rev-list
|
||||||
"", # First invocation of git rev-list
|
|
||||||
"" # Second invocation of git rev-list
|
|
||||||
]
|
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
# By default, gitlint should silently exit with code GITLINT_SUCCESS when there are no commits
|
# By default, gitlint should silently exit with code GITLINT_SUCCESS when there are no commits
|
||||||
result = self.cli.invoke(cli.cli, ["--commits", "foo..bar"])
|
result = self.cli.invoke(cli.cli, ["--commits", "foo..bar"])
|
||||||
self.assertEqual(stderr.getvalue(), "")
|
self.assertEqual(stderr.getvalue(), "")
|
||||||
self.assertEqual(result.exit_code, cli.GITLINT_SUCCESS)
|
self.assertEqual(result.exit_code, cli.GITLINT_SUCCESS)
|
||||||
self.assert_log_contains("DEBUG: gitlint.cli No commits in range \"foo..bar\"")
|
self.assert_log_contains('DEBUG: gitlint.cli No commits in range "foo..bar"')
|
||||||
|
|
||||||
# When --fail-without-commits is set, gitlint should hard fail with code USAGE_ERROR_CODE
|
# When --fail-without-commits is set, gitlint should hard fail with code USAGE_ERROR_CODE
|
||||||
self.clearlog()
|
self.clearlog()
|
||||||
result = self.cli.invoke(cli.cli, ["--commits", "foo..bar", "--fail-without-commits"])
|
result = self.cli.invoke(cli.cli, ["--commits", "foo..bar", "--fail-without-commits"])
|
||||||
self.assertEqual(result.output, 'Error: No commits in range "foo..bar"\n')
|
self.assertEqual(result.output, 'Error: No commits in range "foo..bar"\n')
|
||||||
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
||||||
self.assert_log_contains("DEBUG: gitlint.cli No commits in range \"foo..bar\"")
|
self.assert_log_contains('DEBUG: gitlint.cli No commits in range "foo..bar"')
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||||
def test_msg_filename(self, _):
|
def test_msg_filename(self, _):
|
||||||
expected_output = "3: B6 Body message is missing\n"
|
expected_output = "3: B6 Body message is missing\n"
|
||||||
|
|
||||||
with self.tempdir() as tmpdir:
|
with self.tempdir() as tmpdir:
|
||||||
msg_filename = os.path.join(tmpdir, "msg")
|
msg_filename = os.path.join(tmpdir, "msg")
|
||||||
with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
|
with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f:
|
||||||
f.write("Commït title\n")
|
f.write("Commït title\n")
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename])
|
result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename])
|
||||||
self.assertEqual(stderr.getvalue(), expected_output)
|
self.assertEqual(stderr.getvalue(), expected_output)
|
||||||
self.assertEqual(result.exit_code, 1)
|
self.assertEqual(result.exit_code, 1)
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value="WIP: tïtle \n")
|
@patch("gitlint.cli.get_stdin_data", return_value="WIP: tïtle \n")
|
||||||
def test_silent_mode(self, _):
|
def test_silent_mode(self, _):
|
||||||
""" Test for --silent option """
|
"""Test for --silent option"""
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["--silent"])
|
result = self.cli.invoke(cli.cli, ["--silent"])
|
||||||
self.assertEqual(stderr.getvalue(), "")
|
self.assertEqual(stderr.getvalue(), "")
|
||||||
self.assertEqual(result.exit_code, 3)
|
self.assertEqual(result.exit_code, 3)
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value="WIP: tïtle \n")
|
@patch("gitlint.cli.get_stdin_data", return_value="WIP: tïtle \n")
|
||||||
def test_verbosity(self, _):
|
def test_verbosity(self, _):
|
||||||
""" Test for --verbosity option """
|
"""Test for --verbosity option"""
|
||||||
# We only test -v and -vv, more testing is really not required here
|
# We only test -v and -vv, more testing is really not required here
|
||||||
# -v
|
# -v
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["-v"])
|
result = self.cli.invoke(cli.cli, ["-v"])
|
||||||
self.assertEqual(stderr.getvalue(), "1: T2\n1: T5\n3: B6\n")
|
self.assertEqual(stderr.getvalue(), "1: T2\n1: T5\n3: B6\n")
|
||||||
self.assertEqual(result.exit_code, 3)
|
self.assertEqual(result.exit_code, 3)
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
|
|
||||||
# -vv
|
# -vv
|
||||||
expected_output = "1: T2 Title has trailing whitespace\n" + \
|
expected_output = (
|
||||||
"1: T5 Title contains the word 'WIP' (case-insensitive)\n" + \
|
"1: T2 Title has trailing whitespace\n"
|
||||||
"3: B6 Body message is missing\n"
|
+ "1: T5 Title contains the word 'WIP' (case-insensitive)\n"
|
||||||
|
+ "3: B6 Body message is missing\n"
|
||||||
|
)
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["-vv"], input="WIP: tïtle \n")
|
result = self.cli.invoke(cli.cli, ["-vv"], input="WIP: tïtle \n")
|
||||||
self.assertEqual(stderr.getvalue(), expected_output)
|
self.assertEqual(stderr.getvalue(), expected_output)
|
||||||
self.assertEqual(result.exit_code, 3)
|
self.assertEqual(result.exit_code, 3)
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
|
|
||||||
# -vvvv: not supported -> should print a config error
|
# -vvvv: not supported -> should print a config error
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["-vvvv"], input=u'WIP: tïtle \n')
|
result = self.cli.invoke(cli.cli, ["-vvvv"], input="WIP: tïtle \n")
|
||||||
self.assertEqual(stderr.getvalue(), "")
|
self.assertEqual(stderr.getvalue(), "")
|
||||||
self.assertEqual(result.exit_code, CLITests.CONFIG_ERROR_CODE)
|
self.assertEqual(result.exit_code, CLITests.CONFIG_ERROR_CODE)
|
||||||
self.assertEqual(result.output, "Config Error: Option 'verbosity' must be set between 0 and 3\n")
|
self.assertEqual(result.output, "Config Error: Option 'verbosity' must be set between 0 and 3\n")
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_debug(self, sh, _):
|
def test_debug(self, sh, _):
|
||||||
""" Test for --debug option """
|
"""Test for --debug option"""
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
"6f29bf81a8322a04071bb794666e48c443a90360\n" # git rev-list <SHA>
|
"6f29bf81a8322a04071bb794666e48c443a90360\n" # git rev-list <SHA>
|
||||||
"25053ccec5e28e1bb8f7551fdbb5ab213ada2401\n"
|
"25053ccec5e28e1bb8f7551fdbb5ab213ada2401\n"
|
||||||
"4da2656b0dadc76c7ee3fd0243a96cb64007f125\n",
|
"4da2656b0dadc76c7ee3fd0243a96cb64007f125\n",
|
||||||
# git log --pretty <FORMAT> <SHA>
|
# git log --pretty <FORMAT> <SHA>
|
||||||
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00abc\n"
|
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00a123\n"
|
||||||
"commït-title1\n\ncommït-body1",
|
"commït-title1\n\ncommït-body1",
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
|
"5\t8\tcommit-1/file-1\n2\t9\tcommit-1/file-2\n", # git diff-tree
|
||||||
"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
|
"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
|
||||||
"test åuthor2\x00test-email2@föo.com\x002016-12-04 15:28:15 +0100\x00abc\n"
|
"test åuthor2\x00test-email2@föo.com\x002016-12-04 15:28:15 +0100\x00b123\n"
|
||||||
"commït-title2.\n\ncommït-body2",
|
"commït-title2.\n\ncommït-body2",
|
||||||
"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
|
"5\t8\tcommit-2/file-1\n7\t9\tcommit-2/file-2\n", # git diff-tree
|
||||||
"commit-2/file-1\ncommit-2/file-2\n", # git diff-tree
|
"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
|
||||||
"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00abc\n"
|
"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00c123\n"
|
||||||
"föobar\nbar",
|
"föobar\nbar",
|
||||||
"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
|
"1\t4\tcommit-3/file-1\n3\t4\tcommit-3/file-2\n", # git diff-tree
|
||||||
"commit-3/file-1\ncommit-3/file-2\n", # git diff-tree
|
"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
|
||||||
]
|
]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
config_path = self.get_sample_path(os.path.join("config", "gitlintconfig"))
|
config_path = self.get_sample_path(os.path.join("config", "gitlintconfig"))
|
||||||
result = self.cli.invoke(cli.cli, ["--config", config_path, "--debug", "--commits",
|
result = self.cli.invoke(cli.cli, ["--config", config_path, "--debug", "--commits", "foo...bar"])
|
||||||
"foo...bar"])
|
|
||||||
|
|
||||||
expected = "Commit 6f29bf81a8:\n3: B5\n\n" + \
|
expected = (
|
||||||
"Commit 25053ccec5:\n1: T3\n3: B5\n\n" + \
|
"Commit 6f29bf81a8:\n3: B5\n\n"
|
||||||
"Commit 4da2656b0d:\n2: B4\n3: B5\n3: B6\n"
|
"Commit 25053ccec5:\n1: T3\n3: B5\n\n"
|
||||||
|
"Commit 4da2656b0d:\n2: B4\n3: B5\n3: B6\n"
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(stderr.getvalue(), expected)
|
self.assertEqual(stderr.getvalue(), expected)
|
||||||
self.assertEqual(result.exit_code, 6)
|
self.assertEqual(result.exit_code, 6)
|
||||||
|
|
||||||
expected_kwargs = self.get_system_info_dict()
|
expected_kwargs = self.get_system_info_dict()
|
||||||
expected_kwargs.update({'config_path': config_path})
|
expected_kwargs.update({"config_path": config_path})
|
||||||
expected_logs = self.get_expected('cli/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)
|
self.assert_logged(expected_logs)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value="Test tïtle\n")
|
@patch("gitlint.cli.get_stdin_data", return_value="Test tïtle\n")
|
||||||
def test_extra_path(self, _):
|
def test_extra_path(self, _):
|
||||||
""" Test for --extra-path flag """
|
"""Test for --extra-path flag"""
|
||||||
# Test extra-path pointing to a directory
|
# Test extra-path pointing to a directory
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
extra_path = self.get_sample_path("user_rules")
|
extra_path = self.get_sample_path("user_rules")
|
||||||
result = self.cli.invoke(cli.cli, ["--extra-path", extra_path])
|
result = self.cli.invoke(cli.cli, ["--extra-path", extra_path])
|
||||||
expected_output = "1: UC1 Commit violåtion 1: \"Contënt 1\"\n" + \
|
expected_output = '1: UC1 Commit violåtion 1: "Contënt 1"\n' + "3: B6 Body message is missing\n"
|
||||||
"3: B6 Body message is missing\n"
|
|
||||||
self.assertEqual(stderr.getvalue(), expected_output)
|
self.assertEqual(stderr.getvalue(), expected_output)
|
||||||
self.assertEqual(result.exit_code, 2)
|
self.assertEqual(result.exit_code, 2)
|
||||||
|
|
||||||
# Test extra-path pointing to a file
|
# Test extra-path pointing to a file
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
extra_path = self.get_sample_path(os.path.join("user_rules", "my_commit_rules.py"))
|
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])
|
result = self.cli.invoke(cli.cli, ["--extra-path", extra_path])
|
||||||
expected_output = "1: UC1 Commit violåtion 1: \"Contënt 1\"\n" + \
|
expected_output = '1: UC1 Commit violåtion 1: "Contënt 1"\n' + "3: B6 Body message is missing\n"
|
||||||
"3: B6 Body message is missing\n"
|
|
||||||
self.assertEqual(stderr.getvalue(), expected_output)
|
self.assertEqual(stderr.getvalue(), expected_output)
|
||||||
self.assertEqual(result.exit_code, 2)
|
self.assertEqual(result.exit_code, 2)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value="Test tïtle\n\nMy body that is long enough")
|
@patch("gitlint.cli.get_stdin_data", return_value="Test tïtle\n")
|
||||||
|
def test_extra_path_environment(self, _):
|
||||||
|
"""Test for GITLINT_EXTRA_PATH environment variable"""
|
||||||
|
# Test setting extra-path to a directory from an environment variable
|
||||||
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
|
extra_path = self.get_sample_path("user_rules")
|
||||||
|
result = self.cli.invoke(cli.cli, env={"GITLINT_EXTRA_PATH": extra_path})
|
||||||
|
|
||||||
|
expected_output = '1: UC1 Commit violåtion 1: "Contënt 1"\n' + "3: B6 Body message is missing\n"
|
||||||
|
self.assertEqual(stderr.getvalue(), expected_output)
|
||||||
|
self.assertEqual(result.exit_code, 2)
|
||||||
|
|
||||||
|
# Test extra-path pointing to a file from an environment variable
|
||||||
|
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, env={"GITLINT_EXTRA_PATH": extra_path})
|
||||||
|
expected_output = '1: UC1 Commit violåtion 1: "Contënt 1"\n' + "3: B6 Body message is missing\n"
|
||||||
|
self.assertEqual(stderr.getvalue(), expected_output)
|
||||||
|
self.assertEqual(result.exit_code, 2)
|
||||||
|
|
||||||
|
@patch("gitlint.cli.get_stdin_data", return_value="Test tïtle\n\nMy body that is long enough")
|
||||||
def test_contrib(self, _):
|
def test_contrib(self, _):
|
||||||
# Test enabled contrib rules
|
# Test enabled contrib rules
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["--contrib", "contrib-title-conventional-commits,CC1"])
|
result = self.cli.invoke(cli.cli, ["--contrib", "contrib-title-conventional-commits,CC1"])
|
||||||
expected_output = self.get_expected('cli/test_cli/test_contrib_1')
|
expected_output = self.get_expected("cli/test_cli/test_contrib_1")
|
||||||
self.assertEqual(stderr.getvalue(), expected_output)
|
self.assertEqual(stderr.getvalue(), expected_output)
|
||||||
self.assertEqual(result.exit_code, 2)
|
self.assertEqual(result.exit_code, 2)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value="Test tïtle\n")
|
@patch("gitlint.cli.get_stdin_data", return_value="Test tïtle\n")
|
||||||
def test_contrib_negative(self, _):
|
def test_contrib_negative(self, _):
|
||||||
result = self.cli.invoke(cli.cli, ["--contrib", "föobar,CC1"])
|
result = self.cli.invoke(cli.cli, ["--contrib", "föobar,CC1"])
|
||||||
self.assertEqual(result.output, "Config Error: No contrib rule with id or name 'föobar' found.\n")
|
self.assertEqual(result.output, "Config Error: No contrib rule with id or name 'föobar' found.\n")
|
||||||
self.assertEqual(result.exit_code, self.CONFIG_ERROR_CODE)
|
self.assertEqual(result.exit_code, self.CONFIG_ERROR_CODE)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value="WIP: tëst")
|
@patch("gitlint.cli.get_stdin_data", return_value="WIP: tëst")
|
||||||
def test_config_file(self, _):
|
def test_config_file(self, _):
|
||||||
""" Test for --config option """
|
"""Test for --config option"""
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
config_path = self.get_sample_path(os.path.join("config", "gitlintconfig"))
|
config_path = self.get_sample_path(os.path.join("config", "gitlintconfig"))
|
||||||
result = self.cli.invoke(cli.cli, ["--config", config_path])
|
result = self.cli.invoke(cli.cli, ["--config", config_path])
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
self.assertEqual(stderr.getvalue(), "1: T5\n3: B6\n")
|
self.assertEqual(stderr.getvalue(), "1: T5\n3: B6\n")
|
||||||
self.assertEqual(result.exit_code, 2)
|
self.assertEqual(result.exit_code, 2)
|
||||||
|
|
||||||
|
@patch("gitlint.cli.get_stdin_data", return_value="WIP: tëst")
|
||||||
|
def test_config_file_environment(self, _):
|
||||||
|
"""Test for GITLINT_CONFIG environment variable"""
|
||||||
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
|
config_path = self.get_sample_path(os.path.join("config", "gitlintconfig"))
|
||||||
|
result = self.cli.invoke(cli.cli, env={"GITLINT_CONFIG": config_path})
|
||||||
|
self.assertEqual(result.output, "")
|
||||||
|
self.assertEqual(stderr.getvalue(), "1: T5\n3: B6\n")
|
||||||
|
self.assertEqual(result.exit_code, 2)
|
||||||
|
|
||||||
def test_config_file_negative(self):
|
def test_config_file_negative(self):
|
||||||
""" Negative test for --config option """
|
"""Negative test for --config option"""
|
||||||
# Directory as config file
|
# Directory as config file
|
||||||
config_path = self.get_sample_path("config")
|
config_path = self.get_sample_path("config")
|
||||||
result = self.cli.invoke(cli.cli, ["--config", config_path])
|
result = self.cli.invoke(cli.cli, ["--config", config_path])
|
||||||
|
@ -502,9 +557,30 @@ class CLITests(BaseTestCase):
|
||||||
result = self.cli.invoke(cli.cli, ["--config", config_path])
|
result = self.cli.invoke(cli.cli, ["--config", config_path])
|
||||||
self.assertEqual(result.exit_code, self.CONFIG_ERROR_CODE)
|
self.assertEqual(result.exit_code, self.CONFIG_ERROR_CODE)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
def test_config_file_negative_environment(self):
|
||||||
|
"""Negative test for GITLINT_CONFIG environment variable"""
|
||||||
|
# Directory as config file
|
||||||
|
config_path = self.get_sample_path("config")
|
||||||
|
result = self.cli.invoke(cli.cli, env={"GITLINT_CONFIG": config_path})
|
||||||
|
expected_string = f"Error: Invalid value for '-C' / '--config': File '{config_path}' is a directory."
|
||||||
|
self.assertEqual(result.output.split("\n")[3], expected_string)
|
||||||
|
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
||||||
|
|
||||||
|
# Non existing file
|
||||||
|
config_path = self.get_sample_path("föo")
|
||||||
|
result = self.cli.invoke(cli.cli, env={"GITLINT_CONFIG": config_path})
|
||||||
|
expected_string = f"Error: Invalid value for '-C' / '--config': File '{config_path}' does not exist."
|
||||||
|
self.assertEqual(result.output.split("\n")[3], expected_string)
|
||||||
|
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
||||||
|
|
||||||
|
# Invalid config file
|
||||||
|
config_path = self.get_sample_path(os.path.join("config", "invalid-option-value"))
|
||||||
|
result = self.cli.invoke(cli.cli, env={"GITLINT_CONFIG": config_path})
|
||||||
|
self.assertEqual(result.exit_code, self.CONFIG_ERROR_CODE)
|
||||||
|
|
||||||
|
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||||
def test_target(self, _):
|
def test_target(self, _):
|
||||||
""" Test for the --target option """
|
"""Test for the --target option"""
|
||||||
with self.tempdir() as tmpdir:
|
with self.tempdir() as tmpdir:
|
||||||
tmpdir_path = os.path.realpath(tmpdir)
|
tmpdir_path = os.path.realpath(tmpdir)
|
||||||
os.environ["LANGUAGE"] = "C" # Force language to english so we can check for error message
|
os.environ["LANGUAGE"] = "C" # Force language to english so we can check for error message
|
||||||
|
@ -515,7 +591,7 @@ class CLITests(BaseTestCase):
|
||||||
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
|
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
|
||||||
|
|
||||||
def test_target_negative(self):
|
def test_target_negative(self):
|
||||||
""" Negative test for the --target option """
|
"""Negative test for the --target option"""
|
||||||
# try setting a non-existing target
|
# try setting a non-existing target
|
||||||
result = self.cli.invoke(cli.cli, ["--target", "/föo/bar"])
|
result = self.cli.invoke(cli.cli, ["--target", "/föo/bar"])
|
||||||
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
||||||
|
@ -529,57 +605,63 @@ class CLITests(BaseTestCase):
|
||||||
expected_msg = f"Error: Invalid value for '--target': Directory '{target_path}' is a file."
|
expected_msg = f"Error: Invalid value for '--target': Directory '{target_path}' is a file."
|
||||||
self.assertEqual(result.output.split("\n")[3], expected_msg)
|
self.assertEqual(result.output.split("\n")[3], expected_msg)
|
||||||
|
|
||||||
@patch('gitlint.config.LintConfigGenerator.generate_config')
|
@patch("gitlint.config.LintConfigGenerator.generate_config")
|
||||||
def test_generate_config(self, generate_config):
|
def test_generate_config(self, generate_config):
|
||||||
""" Test for the generate-config subcommand """
|
"""Test for the generate-config subcommand"""
|
||||||
result = self.cli.invoke(cli.cli, ["generate-config"], input="tëstfile\n")
|
result = self.cli.invoke(cli.cli, ["generate-config"], input="tëstfile\n")
|
||||||
self.assertEqual(result.exit_code, self.GITLINT_SUCCESS_CODE)
|
self.assertEqual(result.exit_code, self.GITLINT_SUCCESS_CODE)
|
||||||
expected_msg = "Please specify a location for the sample gitlint config file [.gitlint]: tëstfile\n" + \
|
expected_msg = (
|
||||||
f"Successfully generated {os.path.realpath('tëstfile')}\n"
|
"Please specify a location for the sample gitlint config file [.gitlint]: tëstfile\n"
|
||||||
|
+ f"Successfully generated {os.path.realpath('tëstfile')}\n"
|
||||||
|
)
|
||||||
self.assertEqual(result.output, expected_msg)
|
self.assertEqual(result.output, expected_msg)
|
||||||
generate_config.assert_called_once_with(os.path.realpath("tëstfile"))
|
generate_config.assert_called_once_with(os.path.realpath("tëstfile"))
|
||||||
|
|
||||||
def test_generate_config_negative(self):
|
def test_generate_config_negative(self):
|
||||||
""" Negative test for the generate-config subcommand """
|
"""Negative test for the generate-config subcommand"""
|
||||||
# Non-existing directory
|
# Non-existing directory
|
||||||
fake_dir = os.path.abspath("/föo")
|
fake_dir = os.path.abspath("/föo")
|
||||||
fake_path = os.path.join(fake_dir, "bar")
|
fake_path = os.path.join(fake_dir, "bar")
|
||||||
result = self.cli.invoke(cli.cli, ["generate-config"], input=fake_path)
|
result = self.cli.invoke(cli.cli, ["generate-config"], input=fake_path)
|
||||||
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
||||||
expected_msg = f"Please specify a location for the sample gitlint config file [.gitlint]: {fake_path}\n" + \
|
expected_msg = (
|
||||||
f"Error: Directory '{fake_dir}' does not exist.\n"
|
f"Please specify a location for the sample gitlint config file [.gitlint]: {fake_path}\n"
|
||||||
|
+ f"Error: Directory '{fake_dir}' does not exist.\n"
|
||||||
|
)
|
||||||
self.assertEqual(result.output, expected_msg)
|
self.assertEqual(result.output, expected_msg)
|
||||||
|
|
||||||
# Existing file
|
# Existing file
|
||||||
sample_path = self.get_sample_path(os.path.join("config", "gitlintconfig"))
|
sample_path = self.get_sample_path(os.path.join("config", "gitlintconfig"))
|
||||||
result = self.cli.invoke(cli.cli, ["generate-config"], input=sample_path)
|
result = self.cli.invoke(cli.cli, ["generate-config"], input=sample_path)
|
||||||
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
||||||
expected_msg = "Please specify a location for the sample gitlint " + \
|
expected_msg = (
|
||||||
f"config file [.gitlint]: {sample_path}\n" + \
|
"Please specify a location for the sample gitlint "
|
||||||
f"Error: File \"{sample_path}\" already exists.\n"
|
f"config file [.gitlint]: {sample_path}\n"
|
||||||
|
f'Error: File "{sample_path}" already exists.\n'
|
||||||
|
)
|
||||||
self.assertEqual(result.output, expected_msg)
|
self.assertEqual(result.output, expected_msg)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_git_error(self, sh, _):
|
def test_git_error(self, sh, _):
|
||||||
""" Tests that the cli handles git errors properly """
|
"""Tests that the cli handles git errors properly"""
|
||||||
sh.git.side_effect = CommandNotFound("git")
|
sh.git.side_effect = CommandNotFound("git")
|
||||||
result = self.cli.invoke(cli.cli)
|
result = self.cli.invoke(cli.cli)
|
||||||
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
|
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_no_commits_in_range(self, sh, _):
|
def test_no_commits_in_range(self, sh, _):
|
||||||
""" Test for --commits with the specified range being empty. """
|
"""Test for --commits with the specified range being empty."""
|
||||||
sh.git.side_effect = lambda *_args, **_kwargs: ""
|
sh.git.side_effect = lambda *_args, **_kwargs: ""
|
||||||
result = self.cli.invoke(cli.cli, ["--commits", "master...HEAD"])
|
result = self.cli.invoke(cli.cli, ["--commits", "main...HEAD"])
|
||||||
|
|
||||||
self.assert_log_contains("DEBUG: gitlint.cli No commits in range \"master...HEAD\"")
|
self.assert_log_contains('DEBUG: gitlint.cli No commits in range "main...HEAD"')
|
||||||
self.assertEqual(result.exit_code, self.GITLINT_SUCCESS_CODE)
|
self.assertEqual(result.exit_code, self.GITLINT_SUCCESS_CODE)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value="WIP: tëst tïtle")
|
@patch("gitlint.cli.get_stdin_data", return_value="WIP: tëst tïtle")
|
||||||
def test_named_rules(self, _):
|
def test_named_rules(self, _):
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
config_path = self.get_sample_path(os.path.join("config", "named-rules"))
|
config_path = self.get_sample_path(os.path.join("config", "named-rules"))
|
||||||
result = self.cli.invoke(cli.cli, ["--config", config_path, "--debug"])
|
result = self.cli.invoke(cli.cli, ["--config", config_path, "--debug"])
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
|
@ -588,6 +670,6 @@ class CLITests(BaseTestCase):
|
||||||
|
|
||||||
# Assert debug logs are correct
|
# Assert debug logs are correct
|
||||||
expected_kwargs = self.get_system_info_dict()
|
expected_kwargs = self.get_system_info_dict()
|
||||||
expected_kwargs.update({'config_path': config_path})
|
expected_kwargs.update({"config_path": config_path})
|
||||||
expected_logs = self.get_expected('cli/test_cli/test_named_rules_2', expected_kwargs)
|
expected_logs = self.get_expected("cli/test_cli/test_named_rules_2", expected_kwargs)
|
||||||
self.assert_logged(expected_logs)
|
self.assert_logged(expected_logs)
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import io
|
import io
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
import os
|
import os
|
||||||
|
@ -23,21 +21,21 @@ class CLIHookTests(BaseTestCase):
|
||||||
CONFIG_ERROR_CODE = 255
|
CONFIG_ERROR_CODE = 255
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(CLIHookTests, self).setUp()
|
super().setUp()
|
||||||
self.cli = CliRunner()
|
self.cli = CliRunner()
|
||||||
|
|
||||||
# Patch gitlint.cli.git_version() so that we don't have to patch it separately in every test
|
# Patch gitlint.cli.git_version() so that we don't have to patch it separately in every test
|
||||||
self.git_version_path = patch('gitlint.cli.git_version')
|
self.git_version_path = patch("gitlint.cli.git_version")
|
||||||
cli.git_version = self.git_version_path.start()
|
cli.git_version = self.git_version_path.start()
|
||||||
cli.git_version.return_value = "git version 1.2.3"
|
cli.git_version.return_value = "git version 1.2.3"
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.git_version_path.stop()
|
self.git_version_path.stop()
|
||||||
|
|
||||||
@patch('gitlint.hooks.GitHookInstaller.install_commit_msg_hook')
|
@patch("gitlint.hooks.GitHookInstaller.install_commit_msg_hook")
|
||||||
@patch('gitlint.hooks.git_hooks_dir', return_value=os.path.join("/hür", "dur"))
|
@patch("gitlint.hooks.git_hooks_dir", return_value=os.path.join("/hür", "dur"))
|
||||||
def test_install_hook(self, _, install_hook):
|
def test_install_hook(self, _, install_hook):
|
||||||
""" Test for install-hook subcommand """
|
"""Test for install-hook subcommand"""
|
||||||
result = self.cli.invoke(cli.cli, ["install-hook"])
|
result = self.cli.invoke(cli.cli, ["install-hook"])
|
||||||
expected_path = os.path.join("/hür", "dur", hooks.COMMIT_MSG_HOOK_DST_PATH)
|
expected_path = os.path.join("/hür", "dur", hooks.COMMIT_MSG_HOOK_DST_PATH)
|
||||||
expected = f"Successfully installed gitlint commit-msg hook in {expected_path}\n"
|
expected = f"Successfully installed gitlint commit-msg hook in {expected_path}\n"
|
||||||
|
@ -47,10 +45,10 @@ class CLIHookTests(BaseTestCase):
|
||||||
expected_config.target = os.path.realpath(os.getcwd())
|
expected_config.target = os.path.realpath(os.getcwd())
|
||||||
install_hook.assert_called_once_with(expected_config)
|
install_hook.assert_called_once_with(expected_config)
|
||||||
|
|
||||||
@patch('gitlint.hooks.GitHookInstaller.install_commit_msg_hook')
|
@patch("gitlint.hooks.GitHookInstaller.install_commit_msg_hook")
|
||||||
@patch('gitlint.hooks.git_hooks_dir', return_value=os.path.join("/hür", "dur"))
|
@patch("gitlint.hooks.git_hooks_dir", return_value=os.path.join("/hür", "dur"))
|
||||||
def test_install_hook_target(self, _, install_hook):
|
def test_install_hook_target(self, _, install_hook):
|
||||||
""" Test for install-hook subcommand with a specific --target option specified """
|
"""Test for install-hook subcommand with a specific --target option specified"""
|
||||||
# Specified target
|
# Specified target
|
||||||
result = self.cli.invoke(cli.cli, ["--target", self.SAMPLES_DIR, "install-hook"])
|
result = self.cli.invoke(cli.cli, ["--target", self.SAMPLES_DIR, "install-hook"])
|
||||||
expected_path = os.path.join("/hür", "dur", hooks.COMMIT_MSG_HOOK_DST_PATH)
|
expected_path = os.path.join("/hür", "dur", hooks.COMMIT_MSG_HOOK_DST_PATH)
|
||||||
|
@ -62,9 +60,9 @@ class CLIHookTests(BaseTestCase):
|
||||||
expected_config.target = self.SAMPLES_DIR
|
expected_config.target = self.SAMPLES_DIR
|
||||||
install_hook.assert_called_once_with(expected_config)
|
install_hook.assert_called_once_with(expected_config)
|
||||||
|
|
||||||
@patch('gitlint.hooks.GitHookInstaller.install_commit_msg_hook', side_effect=hooks.GitHookInstallerError("tëst"))
|
@patch("gitlint.hooks.GitHookInstaller.install_commit_msg_hook", side_effect=hooks.GitHookInstallerError("tëst"))
|
||||||
def test_install_hook_negative(self, install_hook):
|
def test_install_hook_negative(self, install_hook):
|
||||||
""" Negative test for install-hook subcommand """
|
"""Negative test for install-hook subcommand"""
|
||||||
result = self.cli.invoke(cli.cli, ["install-hook"])
|
result = self.cli.invoke(cli.cli, ["install-hook"])
|
||||||
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
|
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
|
||||||
self.assertEqual(result.output, "tëst\n")
|
self.assertEqual(result.output, "tëst\n")
|
||||||
|
@ -72,10 +70,10 @@ class CLIHookTests(BaseTestCase):
|
||||||
expected_config.target = os.path.realpath(os.getcwd())
|
expected_config.target = os.path.realpath(os.getcwd())
|
||||||
install_hook.assert_called_once_with(expected_config)
|
install_hook.assert_called_once_with(expected_config)
|
||||||
|
|
||||||
@patch('gitlint.hooks.GitHookInstaller.uninstall_commit_msg_hook')
|
@patch("gitlint.hooks.GitHookInstaller.uninstall_commit_msg_hook")
|
||||||
@patch('gitlint.hooks.git_hooks_dir', return_value=os.path.join("/hür", "dur"))
|
@patch("gitlint.hooks.git_hooks_dir", return_value=os.path.join("/hür", "dur"))
|
||||||
def test_uninstall_hook(self, _, uninstall_hook):
|
def test_uninstall_hook(self, _, uninstall_hook):
|
||||||
""" Test for uninstall-hook subcommand """
|
"""Test for uninstall-hook subcommand"""
|
||||||
result = self.cli.invoke(cli.cli, ["uninstall-hook"])
|
result = self.cli.invoke(cli.cli, ["uninstall-hook"])
|
||||||
expected_path = os.path.join("/hür", "dur", hooks.COMMIT_MSG_HOOK_DST_PATH)
|
expected_path = os.path.join("/hür", "dur", hooks.COMMIT_MSG_HOOK_DST_PATH)
|
||||||
expected = f"Successfully uninstalled gitlint commit-msg hook from {expected_path}\n"
|
expected = f"Successfully uninstalled gitlint commit-msg hook from {expected_path}\n"
|
||||||
|
@ -85,9 +83,9 @@ class CLIHookTests(BaseTestCase):
|
||||||
expected_config.target = os.path.realpath(os.getcwd())
|
expected_config.target = os.path.realpath(os.getcwd())
|
||||||
uninstall_hook.assert_called_once_with(expected_config)
|
uninstall_hook.assert_called_once_with(expected_config)
|
||||||
|
|
||||||
@patch('gitlint.hooks.GitHookInstaller.uninstall_commit_msg_hook', side_effect=hooks.GitHookInstallerError("tëst"))
|
@patch("gitlint.hooks.GitHookInstaller.uninstall_commit_msg_hook", side_effect=hooks.GitHookInstallerError("tëst"))
|
||||||
def test_uninstall_hook_negative(self, uninstall_hook):
|
def test_uninstall_hook_negative(self, uninstall_hook):
|
||||||
""" Negative test for uninstall-hook subcommand """
|
"""Negative test for uninstall-hook subcommand"""
|
||||||
result = self.cli.invoke(cli.cli, ["uninstall-hook"])
|
result = self.cli.invoke(cli.cli, ["uninstall-hook"])
|
||||||
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
|
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
|
||||||
self.assertEqual(result.output, "tëst\n")
|
self.assertEqual(result.output, "tëst\n")
|
||||||
|
@ -96,8 +94,8 @@ class CLIHookTests(BaseTestCase):
|
||||||
uninstall_hook.assert_called_once_with(expected_config)
|
uninstall_hook.assert_called_once_with(expected_config)
|
||||||
|
|
||||||
def test_run_hook_no_tty(self):
|
def test_run_hook_no_tty(self):
|
||||||
""" Test for run-hook subcommand.
|
"""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.
|
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.
|
# No need to patch git as we're passing a msg-filename to run-hook, so no git calls are made.
|
||||||
|
@ -110,20 +108,20 @@ class CLIHookTests(BaseTestCase):
|
||||||
|
|
||||||
with self.tempdir() as tmpdir:
|
with self.tempdir() as tmpdir:
|
||||||
msg_filename = os.path.join(tmpdir, "hür")
|
msg_filename = os.path.join(tmpdir, "hür")
|
||||||
with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
|
with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f:
|
||||||
f.write("WIP: tïtle\n")
|
f.write("WIP: tïtle\n")
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"])
|
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(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"))
|
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)
|
# exit code is 1 because aborted (no stdin available)
|
||||||
self.assertEqual(result.exit_code, 1)
|
self.assertEqual(result.exit_code, 1)
|
||||||
|
|
||||||
@patch('gitlint.cli.shell')
|
@patch("gitlint.cli.shell")
|
||||||
def test_run_hook_edit(self, shell):
|
def test_run_hook_edit(self, shell):
|
||||||
""" Test for run-hook subcommand, answering 'e(dit)' after commit-hook """
|
"""Test for run-hook subcommand, answering 'e(dit)' after commit-hook"""
|
||||||
|
|
||||||
set_editors = [None, "myeditor"]
|
set_editors = [None, "myeditor"]
|
||||||
expected_editors = ["vim -n", "myeditor"]
|
expected_editors = ["vim -n", "myeditor"]
|
||||||
|
@ -131,20 +129,28 @@ class CLIHookTests(BaseTestCase):
|
||||||
|
|
||||||
for i in range(0, len(set_editors)):
|
for i in range(0, len(set_editors)):
|
||||||
if set_editors[i]:
|
if set_editors[i]:
|
||||||
os.environ['EDITOR'] = set_editors[i]
|
os.environ["EDITOR"] = set_editors[i]
|
||||||
|
else:
|
||||||
|
# When set_editors[i] == None, ensure we don't fallback to EDITOR set in shell invocating the tests
|
||||||
|
os.environ.pop("EDITOR", None)
|
||||||
|
|
||||||
with self.patch_input(['e', 'e', 'n']):
|
with self.patch_input(["e", "e", "n"]):
|
||||||
with self.tempdir() as tmpdir:
|
with self.tempdir() as tmpdir:
|
||||||
msg_filename = os.path.realpath(os.path.join(tmpdir, "hür"))
|
msg_filename = os.path.realpath(os.path.join(tmpdir, "hür"))
|
||||||
with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
|
with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f:
|
||||||
f.write(commit_messages[i] + "\n")
|
f.write(commit_messages[i] + "\n")
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"])
|
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',
|
self.assertEqual(
|
||||||
{"commit_msg": commit_messages[i]}))
|
result.output,
|
||||||
expected = self.get_expected("cli/test_cli_hooks/test_hook_edit_1_stderr",
|
self.get_expected(
|
||||||
{"commit_msg": commit_messages[i]})
|
"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)
|
self.assertEqual(stderr.getvalue(), expected)
|
||||||
|
|
||||||
# exit code = number of violations
|
# exit code = number of violations
|
||||||
|
@ -155,17 +161,17 @@ class CLIHookTests(BaseTestCase):
|
||||||
self.assert_log_contains(f"DEBUG: gitlint.cli run-hook: {expected_editors[i]} {msg_filename}")
|
self.assert_log_contains(f"DEBUG: gitlint.cli run-hook: {expected_editors[i]} {msg_filename}")
|
||||||
|
|
||||||
def test_run_hook_no(self):
|
def test_run_hook_no(self):
|
||||||
""" Test for run-hook subcommand, answering 'n(o)' after commit-hook """
|
"""Test for run-hook subcommand, answering 'n(o)' after commit-hook"""
|
||||||
|
|
||||||
with self.patch_input(['n']):
|
with self.patch_input(["n"]):
|
||||||
with self.tempdir() as tmpdir:
|
with self.tempdir() as tmpdir:
|
||||||
msg_filename = os.path.join(tmpdir, "hür")
|
msg_filename = os.path.join(tmpdir, "hür")
|
||||||
with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
|
with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f:
|
||||||
f.write("WIP: höok no\n")
|
f.write("WIP: höok no\n")
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"])
|
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(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"))
|
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)
|
# We decided not to keep the commit message: hook returns number of violations (>0)
|
||||||
|
@ -174,16 +180,16 @@ class CLIHookTests(BaseTestCase):
|
||||||
self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message declined")
|
self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message declined")
|
||||||
|
|
||||||
def test_run_hook_yes(self):
|
def test_run_hook_yes(self):
|
||||||
""" Test for run-hook subcommand, answering 'y(es)' after commit-hook """
|
"""Test for run-hook subcommand, answering 'y(es)' after commit-hook"""
|
||||||
with self.patch_input(['y']):
|
with self.patch_input(["y"]):
|
||||||
with self.tempdir() as tmpdir:
|
with self.tempdir() as tmpdir:
|
||||||
msg_filename = os.path.join(tmpdir, "hür")
|
msg_filename = os.path.join(tmpdir, "hür")
|
||||||
with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
|
with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f:
|
||||||
f.write("WIP: höok yes\n")
|
f.write("WIP: höok yes\n")
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"])
|
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(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"))
|
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
|
# Exit code is 0 because we decide to keep the commit message
|
||||||
|
@ -191,23 +197,23 @@ class CLIHookTests(BaseTestCase):
|
||||||
self.assertEqual(result.exit_code, 0)
|
self.assertEqual(result.exit_code, 0)
|
||||||
self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message accepted")
|
self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message accepted")
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_run_hook_negative(self, sh, _):
|
def test_run_hook_negative(self, sh, _):
|
||||||
""" Negative test for the run-hook subcommand: testing whether exceptions are correctly handled when
|
"""Negative test for the run-hook subcommand: testing whether exceptions are correctly handled when
|
||||||
running `gitlint run-hook`.
|
running `gitlint run-hook`.
|
||||||
"""
|
"""
|
||||||
# GIT_CONTEXT_ERROR_CODE: git error
|
# GIT_CONTEXT_ERROR_CODE: git error
|
||||||
error_msg = b"fatal: not a git repository (or any of the parent directories): .git"
|
error_msg = b"fatal: not a git repository (or any of the parent directories): .git"
|
||||||
sh.git.side_effect = ErrorReturnCode("full command", b"stdout", error_msg)
|
sh.git.side_effect = ErrorReturnCode("full command", b"stdout", error_msg)
|
||||||
result = self.cli.invoke(cli.cli, ["run-hook"])
|
result = self.cli.invoke(cli.cli, ["run-hook"])
|
||||||
expected = self.get_expected('cli/test_cli_hooks/test_run_hook_negative_1', {'git_repo': os.getcwd()})
|
expected = self.get_expected("cli/test_cli_hooks/test_run_hook_negative_1", {"git_repo": os.getcwd()})
|
||||||
self.assertEqual(result.output, expected)
|
self.assertEqual(result.output, expected)
|
||||||
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
|
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
|
||||||
|
|
||||||
# USAGE_ERROR_CODE: incorrect use of gitlint
|
# USAGE_ERROR_CODE: incorrect use of gitlint
|
||||||
result = self.cli.invoke(cli.cli, ["--staged", "run-hook"])
|
result = self.cli.invoke(cli.cli, ["--staged", "run-hook"])
|
||||||
self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_run_hook_negative_2'))
|
self.assertEqual(result.output, self.get_expected("cli/test_cli_hooks/test_run_hook_negative_2"))
|
||||||
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
||||||
|
|
||||||
# CONFIG_ERROR_CODE: incorrect config. Note that this is handled before the hook even runs
|
# CONFIG_ERROR_CODE: incorrect config. Note that this is handled before the hook even runs
|
||||||
|
@ -215,67 +221,66 @@ class CLIHookTests(BaseTestCase):
|
||||||
self.assertEqual(result.output, "Config Error: No such rule 'föo'\n")
|
self.assertEqual(result.output, "Config Error: No such rule 'föo'\n")
|
||||||
self.assertEqual(result.exit_code, self.CONFIG_ERROR_CODE)
|
self.assertEqual(result.exit_code, self.CONFIG_ERROR_CODE)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value="WIP: Test hook stdin tïtle\n")
|
@patch("gitlint.cli.get_stdin_data", return_value="WIP: Test hook stdin tïtle\n")
|
||||||
def test_run_hook_stdin_violations(self, _):
|
def test_run_hook_stdin_violations(self, _):
|
||||||
""" Test for passing stdin data to run-hook, expecting some violations. Equivalent of:
|
"""Test for passing stdin data to run-hook, expecting some violations. Equivalent of:
|
||||||
$ echo "WIP: Test hook stdin tïtle" | gitlint run-hook
|
$ echo "WIP: Test hook stdin tïtle" | gitlint run-hook
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["run-hook"])
|
result = self.cli.invoke(cli.cli, ["run-hook"])
|
||||||
expected_stderr = self.get_expected('cli/test_cli_hooks/test_hook_stdin_violations_1_stderr')
|
expected_stderr = self.get_expected("cli/test_cli_hooks/test_hook_stdin_violations_1_stderr")
|
||||||
self.assertEqual(stderr.getvalue(), expected_stderr)
|
self.assertEqual(stderr.getvalue(), expected_stderr)
|
||||||
self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_stdin_violations_1_stdout'))
|
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
|
# Hook will auto-abort because we're using stdin. Abort = exit code 1
|
||||||
self.assertEqual(result.exit_code, 1)
|
self.assertEqual(result.exit_code, 1)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value="Test tïtle\n\nTest bödy that is long enough")
|
@patch("gitlint.cli.get_stdin_data", return_value="Test tïtle\n\nTest bödy that is long enough")
|
||||||
def test_run_hook_stdin_no_violations(self, _):
|
def test_run_hook_stdin_no_violations(self, _):
|
||||||
""" Test for passing stdin data to run-hook, expecting *NO* violations, Equivalent of:
|
"""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
|
$ 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:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["run-hook"])
|
result = self.cli.invoke(cli.cli, ["run-hook"])
|
||||||
self.assertEqual(stderr.getvalue(), "") # no errors = no stderr output
|
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')
|
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.output, expected_stdout)
|
||||||
self.assertEqual(result.exit_code, 0)
|
self.assertEqual(result.exit_code, 0)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value="WIP: Test hook config tïtle\n")
|
@patch("gitlint.cli.get_stdin_data", return_value="WIP: Test hook config tïtle\n")
|
||||||
def test_run_hook_config(self, _):
|
def test_run_hook_config(self, _):
|
||||||
""" Test that gitlint still respects config when running run-hook, equivalent of:
|
"""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
|
$ 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:
|
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"])
|
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(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'))
|
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
|
# Hook will auto-abort because we're using stdin. Abort = exit code 1
|
||||||
self.assertEqual(result.exit_code, 1)
|
self.assertEqual(result.exit_code, 1)
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_run_hook_local_commit(self, sh, _):
|
def test_run_hook_local_commit(self, sh, _):
|
||||||
""" Test running the hook on the last commit-msg from the local repo, equivalent of:
|
"""Test running the hook on the last commit-msg from the local repo, equivalent of:
|
||||||
$ gitlint run-hook
|
$ gitlint run-hook
|
||||||
and then choosing 'e'
|
and then choosing 'e'
|
||||||
"""
|
"""
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
"6f29bf81a8322a04071bb794666e48c443a90360",
|
"6f29bf81a8322a04071bb794666e48c443a90360",
|
||||||
"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\nWIP: commït-title\n\ncommït-body",
|
||||||
"WIP: commït-title\n\ncommït-body",
|
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
|
"1\t5\tfile1.txt\n3\t4\tpåth/to/file2.txt\n",
|
||||||
"commit-1-branch-1\ncommit-1-branch-2\n",
|
"commit-1-branch-1\ncommit-1-branch-2\n",
|
||||||
"file1.txt\npåth/to/file2.txt\n"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
with self.patch_input(['e']):
|
with self.patch_input(["e"]):
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["run-hook"])
|
result = self.cli.invoke(cli.cli, ["run-hook"])
|
||||||
expected = self.get_expected('cli/test_cli_hooks/test_hook_local_commit_1_stderr')
|
expected = self.get_expected("cli/test_cli_hooks/test_hook_local_commit_1_stderr")
|
||||||
self.assertEqual(stderr.getvalue(), expected)
|
self.assertEqual(stderr.getvalue(), expected)
|
||||||
self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_local_commit_1_stdout'))
|
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
|
# If we can't edit the message, run-hook follows regular gitlint behavior and exit code = # violations
|
||||||
self.assertEqual(result.exit_code, 2)
|
self.assertEqual(result.exit_code, 2)
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from gitlint import rules
|
from gitlint import rules
|
||||||
|
@ -9,16 +7,15 @@ from gitlint.tests.base import BaseTestCase
|
||||||
|
|
||||||
|
|
||||||
class LintConfigTests(BaseTestCase):
|
class LintConfigTests(BaseTestCase):
|
||||||
|
|
||||||
def test_set_rule_option(self):
|
def test_set_rule_option(self):
|
||||||
config = LintConfig()
|
config = LintConfig()
|
||||||
|
|
||||||
# assert default title line-length
|
# assert default title line-length
|
||||||
self.assertEqual(config.get_rule_option('title-max-length', 'line-length'), 72)
|
self.assertEqual(config.get_rule_option("title-max-length", "line-length"), 72)
|
||||||
|
|
||||||
# change line length and assert it is set
|
# change line length and assert it is set
|
||||||
config.set_rule_option('title-max-length', 'line-length', 60)
|
config.set_rule_option("title-max-length", "line-length", 60)
|
||||||
self.assertEqual(config.get_rule_option('title-max-length', 'line-length'), 60)
|
self.assertEqual(config.get_rule_option("title-max-length", "line-length"), 60)
|
||||||
|
|
||||||
def test_set_rule_option_negative(self):
|
def test_set_rule_option_negative(self):
|
||||||
config = LintConfig()
|
config = LintConfig()
|
||||||
|
@ -26,18 +23,20 @@ class LintConfigTests(BaseTestCase):
|
||||||
# non-existing rule
|
# non-existing rule
|
||||||
expected_error_msg = "No such rule 'föobar'"
|
expected_error_msg = "No such rule 'föobar'"
|
||||||
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
|
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
|
||||||
config.set_rule_option(u'föobar', u'lïne-length', 60)
|
config.set_rule_option("föobar", "lïne-length", 60)
|
||||||
|
|
||||||
# non-existing option
|
# non-existing option
|
||||||
expected_error_msg = "Rule 'title-max-length' has no option 'föobar'"
|
expected_error_msg = "Rule 'title-max-length' has no option 'föobar'"
|
||||||
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
|
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
|
||||||
config.set_rule_option('title-max-length', u'föobar', 60)
|
config.set_rule_option("title-max-length", "föobar", 60)
|
||||||
|
|
||||||
# invalid option value
|
# invalid option value
|
||||||
expected_error_msg = "'föo' is not a valid value for option 'title-max-length.line-length'. " + \
|
expected_error_msg = (
|
||||||
"Option 'line-length' must be a positive integer (current value: 'föo')."
|
"'föo' is not a valid value for option 'title-max-length.line-length'. "
|
||||||
|
"Option 'line-length' must be a positive integer (current value: 'föo')."
|
||||||
|
)
|
||||||
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
|
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
|
||||||
config.set_rule_option('title-max-length', 'line-length', "föo")
|
config.set_rule_option("title-max-length", "line-length", "föo")
|
||||||
|
|
||||||
def test_set_general_option(self):
|
def test_set_general_option(self):
|
||||||
config = LintConfig()
|
config = LintConfig()
|
||||||
|
@ -45,12 +44,14 @@ class LintConfigTests(BaseTestCase):
|
||||||
# Check that default general options are correct
|
# Check that default general options are correct
|
||||||
self.assertTrue(config.ignore_merge_commits)
|
self.assertTrue(config.ignore_merge_commits)
|
||||||
self.assertTrue(config.ignore_fixup_commits)
|
self.assertTrue(config.ignore_fixup_commits)
|
||||||
|
self.assertTrue(config.ignore_fixup_amend_commits)
|
||||||
self.assertTrue(config.ignore_squash_commits)
|
self.assertTrue(config.ignore_squash_commits)
|
||||||
self.assertTrue(config.ignore_revert_commits)
|
self.assertTrue(config.ignore_revert_commits)
|
||||||
|
|
||||||
self.assertFalse(config.ignore_stdin)
|
self.assertFalse(config.ignore_stdin)
|
||||||
self.assertFalse(config.staged)
|
self.assertFalse(config.staged)
|
||||||
self.assertFalse(config.fail_without_commits)
|
self.assertFalse(config.fail_without_commits)
|
||||||
|
self.assertFalse(config.regex_style_search)
|
||||||
self.assertFalse(config.debug)
|
self.assertFalse(config.debug)
|
||||||
self.assertEqual(config.verbosity, 3)
|
self.assertEqual(config.verbosity, 3)
|
||||||
active_rule_classes = tuple(type(rule) for rule in config.rules)
|
active_rule_classes = tuple(type(rule) for rule in config.rules)
|
||||||
|
@ -76,6 +77,10 @@ class LintConfigTests(BaseTestCase):
|
||||||
config.set_general_option("ignore-fixup-commits", "false")
|
config.set_general_option("ignore-fixup-commits", "false")
|
||||||
self.assertFalse(config.ignore_fixup_commits)
|
self.assertFalse(config.ignore_fixup_commits)
|
||||||
|
|
||||||
|
# ignore_fixup_amend_commit
|
||||||
|
config.set_general_option("ignore-fixup-amend-commits", "false")
|
||||||
|
self.assertFalse(config.ignore_fixup_amend_commits)
|
||||||
|
|
||||||
# ignore_squash_commit
|
# ignore_squash_commit
|
||||||
config.set_general_option("ignore-squash-commits", "false")
|
config.set_general_option("ignore-squash-commits", "false")
|
||||||
self.assertFalse(config.ignore_squash_commits)
|
self.assertFalse(config.ignore_squash_commits)
|
||||||
|
@ -100,6 +105,10 @@ class LintConfigTests(BaseTestCase):
|
||||||
config.set_general_option("fail-without-commits", "true")
|
config.set_general_option("fail-without-commits", "true")
|
||||||
self.assertTrue(config.fail_without_commits)
|
self.assertTrue(config.fail_without_commits)
|
||||||
|
|
||||||
|
# regex-style-search
|
||||||
|
config.set_general_option("regex-style-search", "true")
|
||||||
|
self.assertTrue(config.regex_style_search)
|
||||||
|
|
||||||
# target
|
# target
|
||||||
config.set_general_option("target", self.SAMPLES_DIR)
|
config.set_general_option("target", self.SAMPLES_DIR)
|
||||||
self.assertEqual(config.target, self.SAMPLES_DIR)
|
self.assertEqual(config.target, self.SAMPLES_DIR)
|
||||||
|
@ -118,8 +127,8 @@ class LintConfigTests(BaseTestCase):
|
||||||
self.assertTrue(actual_rule.is_contrib)
|
self.assertTrue(actual_rule.is_contrib)
|
||||||
|
|
||||||
self.assertEqual(str(type(actual_rule)), "<class 'conventional_commit.ConventionalCommit'>")
|
self.assertEqual(str(type(actual_rule)), "<class 'conventional_commit.ConventionalCommit'>")
|
||||||
self.assertEqual(actual_rule.id, 'CT1')
|
self.assertEqual(actual_rule.id, "CT1")
|
||||||
self.assertEqual(actual_rule.name, u'contrib-title-conventional-commits')
|
self.assertEqual(actual_rule.name, "contrib-title-conventional-commits")
|
||||||
self.assertEqual(actual_rule.target, rules.CommitMessageTitle)
|
self.assertEqual(actual_rule.target, rules.CommitMessageTitle)
|
||||||
|
|
||||||
expected_rule_option = options.ListOption(
|
expected_rule_option = options.ListOption(
|
||||||
|
@ -129,15 +138,15 @@ class LintConfigTests(BaseTestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertListEqual(actual_rule.options_spec, [expected_rule_option])
|
self.assertListEqual(actual_rule.options_spec, [expected_rule_option])
|
||||||
self.assertDictEqual(actual_rule.options, {'types': expected_rule_option})
|
self.assertDictEqual(actual_rule.options, {"types": expected_rule_option})
|
||||||
|
|
||||||
# Check contrib-body-requires-signed-off-by contrib rule
|
# Check contrib-body-requires-signed-off-by contrib rule
|
||||||
actual_rule = config.rules.find_rule("contrib-body-requires-signed-off-by")
|
actual_rule = config.rules.find_rule("contrib-body-requires-signed-off-by")
|
||||||
self.assertTrue(actual_rule.is_contrib)
|
self.assertTrue(actual_rule.is_contrib)
|
||||||
|
|
||||||
self.assertEqual(str(type(actual_rule)), "<class 'signedoff_by.SignedOffBy'>")
|
self.assertEqual(str(type(actual_rule)), "<class 'signedoff_by.SignedOffBy'>")
|
||||||
self.assertEqual(actual_rule.id, 'CC1')
|
self.assertEqual(actual_rule.id, "CC1")
|
||||||
self.assertEqual(actual_rule.name, u'contrib-body-requires-signed-off-by')
|
self.assertEqual(actual_rule.name, "contrib-body-requires-signed-off-by")
|
||||||
|
|
||||||
# reset value (this is a different code path)
|
# reset value (this is a different code path)
|
||||||
config.set_general_option("contrib", "contrib-body-requires-signed-off-by")
|
config.set_general_option("contrib", "contrib-body-requires-signed-off-by")
|
||||||
|
@ -157,7 +166,7 @@ class LintConfigTests(BaseTestCase):
|
||||||
# UserRuleError, RuleOptionError should be re-raised as LintConfigErrors
|
# UserRuleError, RuleOptionError should be re-raised as LintConfigErrors
|
||||||
side_effects = [rules.UserRuleError("üser-rule"), options.RuleOptionError("rüle-option")]
|
side_effects = [rules.UserRuleError("üser-rule"), options.RuleOptionError("rüle-option")]
|
||||||
for side_effect in side_effects:
|
for side_effect in side_effects:
|
||||||
with patch('gitlint.config.rule_finder.find_rule_classes', side_effect=side_effect):
|
with patch("gitlint.config.rule_finder.find_rule_classes", side_effect=side_effect):
|
||||||
with self.assertRaisesMessage(LintConfigError, str(side_effect)):
|
with self.assertRaisesMessage(LintConfigError, str(side_effect)):
|
||||||
config.contrib = "contrib-title-conventional-commits"
|
config.contrib = "contrib-title-conventional-commits"
|
||||||
|
|
||||||
|
@ -166,15 +175,15 @@ class LintConfigTests(BaseTestCase):
|
||||||
|
|
||||||
config.set_general_option("extra-path", self.get_user_rules_path())
|
config.set_general_option("extra-path", self.get_user_rules_path())
|
||||||
self.assertEqual(config.extra_path, self.get_user_rules_path())
|
self.assertEqual(config.extra_path, self.get_user_rules_path())
|
||||||
actual_rule = config.rules.find_rule('UC1')
|
actual_rule = config.rules.find_rule("UC1")
|
||||||
self.assertTrue(actual_rule.is_user_defined)
|
self.assertTrue(actual_rule.is_user_defined)
|
||||||
self.assertEqual(str(type(actual_rule)), "<class 'my_commit_rules.MyUserCommitRule'>")
|
self.assertEqual(str(type(actual_rule)), "<class 'my_commit_rules.MyUserCommitRule'>")
|
||||||
self.assertEqual(actual_rule.id, 'UC1')
|
self.assertEqual(actual_rule.id, "UC1")
|
||||||
self.assertEqual(actual_rule.name, u'my-üser-commit-rule')
|
self.assertEqual(actual_rule.name, "my-üser-commit-rule")
|
||||||
self.assertEqual(actual_rule.target, None)
|
self.assertEqual(actual_rule.target, None)
|
||||||
expected_rule_option = options.IntOption('violation-count', 1, "Number of violåtions to return")
|
expected_rule_option = options.IntOption("violation-count", 1, "Number of violåtions to return")
|
||||||
self.assertListEqual(actual_rule.options_spec, [expected_rule_option])
|
self.assertListEqual(actual_rule.options_spec, [expected_rule_option])
|
||||||
self.assertDictEqual(actual_rule.options, {'violation-count': expected_rule_option})
|
self.assertDictEqual(actual_rule.options, {"violation-count": expected_rule_option})
|
||||||
|
|
||||||
# reset value (this is a different code path)
|
# reset value (this is a different code path)
|
||||||
config.set_general_option("extra-path", self.SAMPLES_DIR)
|
config.set_general_option("extra-path", self.SAMPLES_DIR)
|
||||||
|
@ -189,8 +198,9 @@ class LintConfigTests(BaseTestCase):
|
||||||
config.extra_path = "föo/bar"
|
config.extra_path = "föo/bar"
|
||||||
|
|
||||||
# extra path contains classes with errors
|
# extra path contains classes with errors
|
||||||
with self.assertRaisesMessage(LintConfigError,
|
with self.assertRaisesMessage(
|
||||||
"User-defined rule class 'MyUserLineRule' must have a 'validate' method"):
|
LintConfigError, "User-defined rule class 'MyUserLineRule' must have a 'validate' method"
|
||||||
|
):
|
||||||
config.extra_path = self.get_sample_path("user_rules/incorrect_linerule")
|
config.extra_path = self.get_sample_path("user_rules/incorrect_linerule")
|
||||||
|
|
||||||
def test_set_general_option_negative(self):
|
def test_set_general_option_negative(self):
|
||||||
|
@ -218,31 +228,37 @@ class LintConfigTests(BaseTestCase):
|
||||||
config.verbosity = value
|
config.verbosity = value
|
||||||
|
|
||||||
# invalid ignore_xxx_commits
|
# invalid ignore_xxx_commits
|
||||||
ignore_attributes = ["ignore_merge_commits", "ignore_fixup_commits", "ignore_squash_commits",
|
ignore_attributes = [
|
||||||
"ignore_revert_commits"]
|
"ignore_merge_commits",
|
||||||
|
"ignore_fixup_commits",
|
||||||
|
"ignore_fixup_amend_commits",
|
||||||
|
"ignore_squash_commits",
|
||||||
|
"ignore_revert_commits",
|
||||||
|
]
|
||||||
incorrect_values = [-1, 4, "föo"]
|
incorrect_values = [-1, 4, "föo"]
|
||||||
for attribute in ignore_attributes:
|
for attribute in ignore_attributes:
|
||||||
for value in incorrect_values:
|
for value in incorrect_values:
|
||||||
option_name = attribute.replace("_", "-")
|
option_name = attribute.replace("_", "-")
|
||||||
with self.assertRaisesMessage(LintConfigError,
|
with self.assertRaisesMessage(
|
||||||
f"Option '{option_name}' must be either 'true' or 'false'"):
|
LintConfigError, f"Option '{option_name}' must be either 'true' or 'false'"
|
||||||
|
):
|
||||||
setattr(config, attribute, value)
|
setattr(config, attribute, value)
|
||||||
|
|
||||||
# invalid ignore -> not here because ignore is a ListOption which converts everything to a string before
|
# invalid ignore -> not here because ignore is a ListOption which converts everything to a string before
|
||||||
# splitting which means it it will accept just about everything
|
# splitting which means it it will accept just about everything
|
||||||
|
|
||||||
# invalid boolean options
|
# invalid boolean options
|
||||||
for attribute in ['debug', 'staged', 'ignore_stdin', 'fail_without_commits']:
|
for attribute in ["debug", "staged", "ignore_stdin", "fail_without_commits", "regex_style_search"]:
|
||||||
option_name = attribute.replace("_", "-")
|
option_name = attribute.replace("_", "-")
|
||||||
with self.assertRaisesMessage(LintConfigError,
|
with self.assertRaisesMessage(LintConfigError, f"Option '{option_name}' must be either 'true' or 'false'"):
|
||||||
f"Option '{option_name}' must be either 'true' or 'false'"):
|
|
||||||
setattr(config, attribute, "föobar")
|
setattr(config, attribute, "föobar")
|
||||||
|
|
||||||
# extra-path has its own negative test
|
# extra-path has its own negative test
|
||||||
|
|
||||||
# invalid target
|
# invalid target
|
||||||
with self.assertRaisesMessage(LintConfigError,
|
with self.assertRaisesMessage(
|
||||||
"Option target must be an existing directory (current value: 'föo/bar')"):
|
LintConfigError, "Option target must be an existing directory (current value: 'föo/bar')"
|
||||||
|
):
|
||||||
config.target = "föo/bar"
|
config.target = "föo/bar"
|
||||||
|
|
||||||
def test_ignore_independent_from_rules(self):
|
def test_ignore_independent_from_rules(self):
|
||||||
|
@ -259,12 +275,25 @@ class LintConfigTests(BaseTestCase):
|
||||||
self.assertNotEqual(LintConfig(), LintConfigGenerator())
|
self.assertNotEqual(LintConfig(), LintConfigGenerator())
|
||||||
|
|
||||||
# Ensure LintConfig are not equal if they differ on their attributes
|
# Ensure LintConfig are not equal if they differ on their attributes
|
||||||
attrs = [("verbosity", 1), ("rules", []), ("ignore_stdin", True), ("debug", True),
|
attrs = [
|
||||||
("ignore", ["T1"]), ("staged", True), ("_config_path", self.get_sample_path()),
|
("verbosity", 1),
|
||||||
("ignore_merge_commits", False), ("ignore_fixup_commits", False),
|
("rules", []),
|
||||||
("ignore_squash_commits", False), ("ignore_revert_commits", False),
|
("ignore_stdin", True),
|
||||||
("extra_path", self.get_sample_path("user_rules")), ("target", self.get_sample_path()),
|
("fail_without_commits", True),
|
||||||
("contrib", ["CC1"])]
|
("regex_style_search", True),
|
||||||
|
("debug", True),
|
||||||
|
("ignore", ["T1"]),
|
||||||
|
("staged", True),
|
||||||
|
("_config_path", self.get_sample_path()),
|
||||||
|
("ignore_merge_commits", False),
|
||||||
|
("ignore_fixup_commits", False),
|
||||||
|
("ignore_fixup_amend_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:
|
for attr, val in attrs:
|
||||||
config = LintConfig()
|
config = LintConfig()
|
||||||
setattr(config, attr, val)
|
setattr(config, attr, val)
|
||||||
|
@ -281,7 +310,7 @@ class LintConfigTests(BaseTestCase):
|
||||||
|
|
||||||
class LintConfigGeneratorTests(BaseTestCase):
|
class LintConfigGeneratorTests(BaseTestCase):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@patch('gitlint.config.shutil.copyfile')
|
@patch("gitlint.config.shutil.copyfile")
|
||||||
def test_install_commit_msg_hook_negative(copy):
|
def test_install_commit_msg_hook_negative(copy):
|
||||||
LintConfigGenerator.generate_config("föo/bar/test")
|
LintConfigGenerator.generate_config("föo/bar/test")
|
||||||
copy.assert_called_with(GITLINT_CONFIG_TEMPLATE_SRC_PATH, "föo/bar/test")
|
copy.assert_called_with(GITLINT_CONFIG_TEMPLATE_SRC_PATH, "föo/bar/test")
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from gitlint.tests.base import BaseTestCase
|
from gitlint.tests.base import BaseTestCase
|
||||||
|
@ -14,24 +13,27 @@ class LintConfigBuilderTests(BaseTestCase):
|
||||||
config = config_builder.build()
|
config = config_builder.build()
|
||||||
|
|
||||||
# assert some defaults
|
# assert some defaults
|
||||||
self.assertEqual(config.get_rule_option('title-max-length', 'line-length'), 72)
|
self.assertEqual(config.get_rule_option("title-max-length", "line-length"), 72)
|
||||||
self.assertEqual(config.get_rule_option('body-max-line-length', 'line-length'), 80)
|
self.assertEqual(config.get_rule_option("body-max-line-length", "line-length"), 80)
|
||||||
self.assertListEqual(config.get_rule_option('title-must-not-contain-word', 'words'), ["WIP"])
|
self.assertListEqual(config.get_rule_option("title-must-not-contain-word", "words"), ["WIP"])
|
||||||
self.assertEqual(config.verbosity, 3)
|
self.assertEqual(config.verbosity, 3)
|
||||||
|
|
||||||
# Make some changes and check blueprint
|
# Make some changes and check blueprint
|
||||||
config_builder.set_option('title-max-length', 'line-length', 100)
|
config_builder.set_option("title-max-length", "line-length", 100)
|
||||||
config_builder.set_option('general', 'verbosity', 2)
|
config_builder.set_option("general", "verbosity", 2)
|
||||||
config_builder.set_option('title-must-not-contain-word', 'words', ["foo", "bar"])
|
config_builder.set_option("title-must-not-contain-word", "words", ["foo", "bar"])
|
||||||
expected_blueprint = {'title-must-not-contain-word': {'words': ['foo', 'bar']},
|
expected_blueprint = {
|
||||||
'title-max-length': {'line-length': 100}, 'general': {'verbosity': 2}}
|
"title-must-not-contain-word": {"words": ["foo", "bar"]},
|
||||||
|
"title-max-length": {"line-length": 100},
|
||||||
|
"general": {"verbosity": 2},
|
||||||
|
}
|
||||||
self.assertDictEqual(config_builder._config_blueprint, expected_blueprint)
|
self.assertDictEqual(config_builder._config_blueprint, expected_blueprint)
|
||||||
|
|
||||||
# Build config and verify that the changes have occurred and no other changes
|
# Build config and verify that the changes have occurred and no other changes
|
||||||
config = config_builder.build()
|
config = config_builder.build()
|
||||||
self.assertEqual(config.get_rule_option('title-max-length', 'line-length'), 100)
|
self.assertEqual(config.get_rule_option("title-max-length", "line-length"), 100)
|
||||||
self.assertEqual(config.get_rule_option('body-max-line-length', 'line-length'), 80) # should be unchanged
|
self.assertEqual(config.get_rule_option("body-max-line-length", "line-length"), 80) # should be unchanged
|
||||||
self.assertListEqual(config.get_rule_option('title-must-not-contain-word', 'words'), ["foo", "bar"])
|
self.assertListEqual(config.get_rule_option("title-must-not-contain-word", "words"), ["foo", "bar"])
|
||||||
self.assertEqual(config.verbosity, 2)
|
self.assertEqual(config.verbosity, 2)
|
||||||
|
|
||||||
def test_set_from_commit_ignore_all(self):
|
def test_set_from_commit_ignore_all(self):
|
||||||
|
@ -82,8 +84,8 @@ class LintConfigBuilderTests(BaseTestCase):
|
||||||
self.assertIsNone(config.extra_path)
|
self.assertIsNone(config.extra_path)
|
||||||
self.assertEqual(config.ignore, ["title-trailing-whitespace", "B2"])
|
self.assertEqual(config.ignore, ["title-trailing-whitespace", "B2"])
|
||||||
|
|
||||||
self.assertEqual(config.get_rule_option('title-max-length', 'line-length'), 20)
|
self.assertEqual(config.get_rule_option("title-max-length", "line-length"), 20)
|
||||||
self.assertEqual(config.get_rule_option('body-max-line-length', 'line-length'), 30)
|
self.assertEqual(config.get_rule_option("body-max-line-length", "line-length"), 30)
|
||||||
|
|
||||||
def test_set_from_config_file_negative(self):
|
def test_set_from_config_file_negative(self):
|
||||||
config_builder = LintConfigBuilder()
|
config_builder = LintConfigBuilder()
|
||||||
|
@ -129,8 +131,10 @@ class LintConfigBuilderTests(BaseTestCase):
|
||||||
path = self.get_sample_path("config/invalid-option-value")
|
path = self.get_sample_path("config/invalid-option-value")
|
||||||
config_builder = LintConfigBuilder()
|
config_builder = LintConfigBuilder()
|
||||||
config_builder.set_from_config_file(path)
|
config_builder.set_from_config_file(path)
|
||||||
expected_error_msg = "'föo' is not a valid value for option 'title-max-length.line-length'. " + \
|
expected_error_msg = (
|
||||||
"Option 'line-length' must be a positive integer (current value: 'föo')."
|
"'föo' is not a valid value for option 'title-max-length.line-length'. "
|
||||||
|
"Option 'line-length' must be a positive integer (current value: 'föo')."
|
||||||
|
)
|
||||||
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
|
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
|
||||||
config_builder.build()
|
config_builder.build()
|
||||||
|
|
||||||
|
@ -139,14 +143,19 @@ class LintConfigBuilderTests(BaseTestCase):
|
||||||
|
|
||||||
# change and assert changes
|
# change and assert changes
|
||||||
config_builder = LintConfigBuilder()
|
config_builder = LintConfigBuilder()
|
||||||
config_builder.set_config_from_string_list(['general.verbosity=1', 'title-max-length.line-length=60',
|
config_builder.set_config_from_string_list(
|
||||||
'body-max-line-length.line-length=120',
|
[
|
||||||
"title-must-not-contain-word.words=håha"])
|
"general.verbosity=1",
|
||||||
|
"title-max-length.line-length=60",
|
||||||
|
"body-max-line-length.line-length=120",
|
||||||
|
"title-must-not-contain-word.words=håha",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
config = config_builder.build()
|
config = config_builder.build()
|
||||||
self.assertEqual(config.get_rule_option('title-max-length', 'line-length'), 60)
|
self.assertEqual(config.get_rule_option("title-max-length", "line-length"), 60)
|
||||||
self.assertEqual(config.get_rule_option('body-max-line-length', 'line-length'), 120)
|
self.assertEqual(config.get_rule_option("body-max-line-length", "line-length"), 120)
|
||||||
self.assertListEqual(config.get_rule_option('title-must-not-contain-word', 'words'), ["håha"])
|
self.assertListEqual(config.get_rule_option("title-must-not-contain-word", "words"), ["håha"])
|
||||||
self.assertEqual(config.verbosity, 1)
|
self.assertEqual(config.verbosity, 1)
|
||||||
|
|
||||||
def test_set_config_from_string_list_negative(self):
|
def test_set_config_from_string_list_negative(self):
|
||||||
|
@ -175,12 +184,12 @@ class LintConfigBuilderTests(BaseTestCase):
|
||||||
# no period between rule and option names
|
# no period between rule and option names
|
||||||
expected_msg = "'föobar=1' is an invalid configuration option. Use '<rule>.<option>=<value>'"
|
expected_msg = "'föobar=1' is an invalid configuration option. Use '<rule>.<option>=<value>'"
|
||||||
with self.assertRaisesMessage(LintConfigError, expected_msg):
|
with self.assertRaisesMessage(LintConfigError, expected_msg):
|
||||||
config_builder.set_config_from_string_list([u'föobar=1'])
|
config_builder.set_config_from_string_list(["föobar=1"])
|
||||||
|
|
||||||
def test_rebuild_config(self):
|
def test_rebuild_config(self):
|
||||||
# normal config build
|
# normal config build
|
||||||
config_builder = LintConfigBuilder()
|
config_builder = LintConfigBuilder()
|
||||||
config_builder.set_option('general', 'verbosity', 3)
|
config_builder.set_option("general", "verbosity", 3)
|
||||||
lint_config = config_builder.build()
|
lint_config = config_builder.build()
|
||||||
self.assertEqual(lint_config.verbosity, 3)
|
self.assertEqual(lint_config.verbosity, 3)
|
||||||
|
|
||||||
|
@ -193,9 +202,9 @@ class LintConfigBuilderTests(BaseTestCase):
|
||||||
|
|
||||||
def test_clone(self):
|
def test_clone(self):
|
||||||
config_builder = LintConfigBuilder()
|
config_builder = LintConfigBuilder()
|
||||||
config_builder.set_option('general', 'verbosity', 2)
|
config_builder.set_option("general", "verbosity", 2)
|
||||||
config_builder.set_option('title-max-length', 'line-length', 100)
|
config_builder.set_option("title-max-length", "line-length", 100)
|
||||||
expected = {'title-max-length': {'line-length': 100}, 'general': {'verbosity': 2}}
|
expected = {"title-max-length": {"line-length": 100}, "general": {"verbosity": 2}}
|
||||||
self.assertDictEqual(config_builder._config_blueprint, expected)
|
self.assertDictEqual(config_builder._config_blueprint, expected)
|
||||||
|
|
||||||
# Clone and verify that the blueprint is the same as the original
|
# Clone and verify that the blueprint is the same as the original
|
||||||
|
@ -203,7 +212,7 @@ class LintConfigBuilderTests(BaseTestCase):
|
||||||
self.assertDictEqual(cloned_builder._config_blueprint, expected)
|
self.assertDictEqual(cloned_builder._config_blueprint, expected)
|
||||||
|
|
||||||
# Modify the original and make sure we're not modifying the clone (i.e. check that the copy is a deep copy)
|
# 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)
|
config_builder.set_option("title-max-length", "line-length", 120)
|
||||||
self.assertDictEqual(cloned_builder._config_blueprint, expected)
|
self.assertDictEqual(cloned_builder._config_blueprint, expected)
|
||||||
|
|
||||||
def test_named_rules(self):
|
def test_named_rules(self):
|
||||||
|
@ -215,17 +224,22 @@ class LintConfigBuilderTests(BaseTestCase):
|
||||||
|
|
||||||
# Add a named rule by setting an option in the config builder that follows the named rule pattern
|
# 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
|
# 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',
|
rule_qualifiers = [
|
||||||
u'T7:\t\n \tmy-extra-rüle\t\n\n', "title-match-regex:my-extra-rüle"]
|
"T7:my-extra-rüle",
|
||||||
|
" T7 : my-extra-rüle ",
|
||||||
|
"\tT7:\tmy-extra-rüle\t",
|
||||||
|
"T7:\t\n \tmy-extra-rüle\t\n\n",
|
||||||
|
"title-match-regex:my-extra-rüle",
|
||||||
|
]
|
||||||
for rule_qualifier in rule_qualifiers:
|
for rule_qualifier in rule_qualifiers:
|
||||||
config_builder = LintConfigBuilder()
|
config_builder = LintConfigBuilder()
|
||||||
config_builder.set_option(rule_qualifier, 'regex', "föo")
|
config_builder.set_option(rule_qualifier, "regex", "föo")
|
||||||
|
|
||||||
expected_rules = copy.deepcopy(default_rules)
|
expected_rules = copy.deepcopy(default_rules)
|
||||||
my_rule = rules.TitleRegexMatches({'regex': "föo"})
|
my_rule = rules.TitleRegexMatches({"regex": "föo"})
|
||||||
my_rule.id = rules.TitleRegexMatches.id + ":my-extra-rüle"
|
my_rule.id = rules.TitleRegexMatches.id + ":my-extra-rüle"
|
||||||
my_rule.name = rules.TitleRegexMatches.name + ":my-extra-rüle"
|
my_rule.name = rules.TitleRegexMatches.name + ":my-extra-rüle"
|
||||||
expected_rules._rules[u'T7:my-extra-rüle'] = my_rule
|
expected_rules._rules["T7:my-extra-rüle"] = my_rule
|
||||||
self.assertEqual(config_builder.build().rules, expected_rules)
|
self.assertEqual(config_builder.build().rules, expected_rules)
|
||||||
|
|
||||||
# assert that changing an option on the newly added rule is passed correctly to the RuleCollection
|
# assert that changing an option on the newly added rule is passed correctly to the RuleCollection
|
||||||
|
@ -233,20 +247,20 @@ class LintConfigBuilderTests(BaseTestCase):
|
||||||
# to the same rule
|
# to the same rule
|
||||||
for other_rule_qualifier in rule_qualifiers:
|
for other_rule_qualifier in rule_qualifiers:
|
||||||
cb = config_builder.clone()
|
cb = config_builder.clone()
|
||||||
cb.set_option(other_rule_qualifier, 'regex', other_rule_qualifier + "bōr")
|
cb.set_option(other_rule_qualifier, "regex", other_rule_qualifier + "bōr")
|
||||||
# before setting the expected rule option value correctly, the RuleCollection should be different
|
# before setting the expected rule option value correctly, the RuleCollection should be different
|
||||||
self.assertNotEqual(cb.build().rules, expected_rules)
|
self.assertNotEqual(cb.build().rules, expected_rules)
|
||||||
# after setting the option on the expected rule, it should be equal
|
# after setting the option on the expected rule, it should be equal
|
||||||
my_rule.options['regex'].set(other_rule_qualifier + "bōr")
|
my_rule.options["regex"].set(other_rule_qualifier + "bōr")
|
||||||
self.assertEqual(cb.build().rules, expected_rules)
|
self.assertEqual(cb.build().rules, expected_rules)
|
||||||
my_rule.options['regex'].set("wrong")
|
my_rule.options["regex"].set("wrong")
|
||||||
|
|
||||||
def test_named_rules_negative(self):
|
def test_named_rules_negative(self):
|
||||||
# T7 = title-match-regex
|
# T7 = title-match-regex
|
||||||
# Invalid rule name
|
# Invalid rule name
|
||||||
for invalid_name in ["", " ", " ", "\t", "\n", "å b", "å:b", "åb:", ":åb"]:
|
for invalid_name in ["", " ", " ", "\t", "\n", "å b", "å:b", "åb:", ":åb"]:
|
||||||
config_builder = LintConfigBuilder()
|
config_builder = LintConfigBuilder()
|
||||||
config_builder.set_option(f"T7:{invalid_name}", 'regex', "tëst")
|
config_builder.set_option(f"T7:{invalid_name}", "regex", "tëst")
|
||||||
expected_msg = f"The rule-name part in 'T7:{invalid_name}' cannot contain whitespace, colons or be empty"
|
expected_msg = f"The rule-name part in 'T7:{invalid_name}' cannot contain whitespace, colons or be empty"
|
||||||
with self.assertRaisesMessage(LintConfigError, expected_msg):
|
with self.assertRaisesMessage(LintConfigError, expected_msg):
|
||||||
config_builder.build()
|
config_builder.build()
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
@ -13,9 +11,10 @@ from gitlint.config import LintConfigBuilder
|
||||||
|
|
||||||
class LintConfigPrecedenceTests(BaseTestCase):
|
class LintConfigPrecedenceTests(BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
self.cli = CliRunner()
|
self.cli = CliRunner()
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value="WIP:fö\n\nThis is å test message\n")
|
@patch("gitlint.cli.get_stdin_data", return_value="WIP:fö\n\nThis is å test message\n")
|
||||||
def test_config_precedence(self, _):
|
def test_config_precedence(self, _):
|
||||||
# TODO(jroovers): this test really only test verbosity, we need to do some refactoring to gitlint.cli
|
# TODO(jroovers): this test really only test verbosity, we need to do some refactoring to gitlint.cli
|
||||||
# to more easily test everything
|
# to more easily test everything
|
||||||
|
@ -28,60 +27,63 @@ class LintConfigPrecedenceTests(BaseTestCase):
|
||||||
config_path = self.get_sample_path("config/gitlintconfig")
|
config_path = self.get_sample_path("config/gitlintconfig")
|
||||||
|
|
||||||
# 1. commandline convenience flags
|
# 1. commandline convenience flags
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["-vvv", "-c", "general.verbosity=2", "--config", config_path])
|
result = self.cli.invoke(cli.cli, ["-vvv", "-c", "general.verbosity=2", "--config", config_path])
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n")
|
self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n")
|
||||||
|
|
||||||
# 2. environment variables
|
# 2. environment variables
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["-c", "general.verbosity=2", "--config", config_path],
|
result = self.cli.invoke(
|
||||||
env={"GITLINT_VERBOSITY": "3"})
|
cli.cli, ["-c", "general.verbosity=2", "--config", config_path], env={"GITLINT_VERBOSITY": "3"}
|
||||||
|
)
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n")
|
self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n")
|
||||||
|
|
||||||
# 3. commandline -c flags
|
# 3. commandline -c flags
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["-c", "general.verbosity=2", "--config", config_path])
|
result = self.cli.invoke(cli.cli, ["-c", "general.verbosity=2", "--config", config_path])
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive)\n")
|
self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive)\n")
|
||||||
|
|
||||||
# 4. config file
|
# 4. config file
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli, ["--config", config_path])
|
result = self.cli.invoke(cli.cli, ["--config", config_path])
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
self.assertEqual(stderr.getvalue(), "1: T5\n")
|
self.assertEqual(stderr.getvalue(), "1: T5\n")
|
||||||
|
|
||||||
# 5. default config
|
# 5. default config
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
result = self.cli.invoke(cli.cli)
|
result = self.cli.invoke(cli.cli)
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n")
|
self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n")
|
||||||
|
|
||||||
@patch('gitlint.cli.get_stdin_data', return_value="WIP: This is å test")
|
@patch("gitlint.cli.get_stdin_data", return_value="WIP: This is å test")
|
||||||
def test_ignore_precedence(self, get_stdin_data):
|
def test_ignore_precedence(self, get_stdin_data):
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
# --ignore takes precedence over -c general.ignore
|
# --ignore takes precedence over -c general.ignore
|
||||||
result = self.cli.invoke(cli.cli, ["-c", "general.ignore=T5", "--ignore", "B6"])
|
result = self.cli.invoke(cli.cli, ["-c", "general.ignore=T5", "--ignore", "B6"])
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
self.assertEqual(result.exit_code, 1)
|
self.assertEqual(result.exit_code, 1)
|
||||||
# We still expect the T5 violation, but no B6 violation as --ignore overwrites -c general.ignore
|
# We still expect the T5 violation, but no B6 violation as --ignore overwrites -c general.ignore
|
||||||
self.assertEqual(stderr.getvalue(),
|
self.assertEqual(
|
||||||
"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: This is å test\"\n")
|
stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: This is å test\"\n"
|
||||||
|
)
|
||||||
|
|
||||||
# test that we can also still configure a rule that is first ignored but then not
|
# test that we can also still configure a rule that is first ignored but then not
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
get_stdin_data.return_value = "This is å test"
|
get_stdin_data.return_value = "This is å test"
|
||||||
# --ignore takes precedence over -c general.ignore
|
# --ignore takes precedence over -c general.ignore
|
||||||
result = self.cli.invoke(cli.cli, ["-c", "general.ignore=title-max-length",
|
result = self.cli.invoke(
|
||||||
"-c", "title-max-length.line-length=5",
|
cli.cli,
|
||||||
"--ignore", "B6"])
|
["-c", "general.ignore=title-max-length", "-c", "title-max-length.line-length=5", "--ignore", "B6"],
|
||||||
|
)
|
||||||
self.assertEqual(result.output, "")
|
self.assertEqual(result.output, "")
|
||||||
self.assertEqual(result.exit_code, 1)
|
self.assertEqual(result.exit_code, 1)
|
||||||
|
|
||||||
# We still expect the T1 violation with custom config,
|
# We still expect the T1 violation with custom config,
|
||||||
# but no B6 violation as --ignore overwrites -c general.ignore
|
# but no B6 violation as --ignore overwrites -c general.ignore
|
||||||
self.assertEqual(stderr.getvalue(), "1: T1 Title exceeds max length (14>5): \"This is å test\"\n")
|
self.assertEqual(stderr.getvalue(), '1: T1 Title exceeds max length (14>5): "This is å test"\n')
|
||||||
|
|
||||||
def test_general_option_after_rule_option(self):
|
def test_general_option_after_rule_option(self):
|
||||||
# We used to have a bug where we didn't process general options before setting specific options, this would
|
# We used to have a bug where we didn't process general options before setting specific options, this would
|
||||||
|
@ -89,10 +91,10 @@ class LintConfigPrecedenceTests(BaseTestCase):
|
||||||
# This test is here to test for regressions against this.
|
# This test is here to test for regressions against this.
|
||||||
|
|
||||||
config_builder = LintConfigBuilder()
|
config_builder = LintConfigBuilder()
|
||||||
config_builder.set_option(u'my-üser-commit-rule', 'violation-count', 3)
|
config_builder.set_option("my-üser-commit-rule", "violation-count", 3)
|
||||||
user_rules_path = self.get_sample_path("user_rules")
|
user_rules_path = self.get_sample_path("user_rules")
|
||||||
config_builder.set_option('general', 'extra-path', user_rules_path)
|
config_builder.set_option("general", "extra-path", user_rules_path)
|
||||||
config = config_builder.build()
|
config = config_builder.build()
|
||||||
|
|
||||||
self.assertEqual(config.extra_path, user_rules_path)
|
self.assertEqual(config.extra_path, user_rules_path)
|
||||||
self.assertEqual(config.get_rule_option(u'my-üser-commit-rule', 'violation-count'), 3)
|
self.assertEqual(config.get_rule_option("my-üser-commit-rule", "violation-count"), 3)
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from gitlint import rules
|
from gitlint import rules
|
||||||
from gitlint.config import RuleCollection
|
from gitlint.config import RuleCollection
|
||||||
|
@ -7,7 +5,6 @@ from gitlint.tests.base import BaseTestCase
|
||||||
|
|
||||||
|
|
||||||
class RuleCollectionTests(BaseTestCase):
|
class RuleCollectionTests(BaseTestCase):
|
||||||
|
|
||||||
def test_add_rule(self):
|
def test_add_rule(self):
|
||||||
collection = RuleCollection()
|
collection = RuleCollection()
|
||||||
collection.add_rule(rules.TitleMaxLength, "my-rüle", {"my_attr": "föo", "my_attr2": 123})
|
collection.add_rule(rules.TitleMaxLength, "my-rüle", {"my_attr": "föo", "my_attr2": 123})
|
||||||
|
@ -29,18 +26,18 @@ class RuleCollectionTests(BaseTestCase):
|
||||||
|
|
||||||
# find by id
|
# find by id
|
||||||
expected = rules.TitleMaxLength()
|
expected = rules.TitleMaxLength()
|
||||||
rule = collection.find_rule('T1')
|
rule = collection.find_rule("T1")
|
||||||
self.assertEqual(rule, expected)
|
self.assertEqual(rule, expected)
|
||||||
self.assertEqual(rule.my_attr, "föo")
|
self.assertEqual(rule.my_attr, "föo")
|
||||||
|
|
||||||
# find by name
|
# find by name
|
||||||
expected2 = rules.TitleTrailingWhitespace()
|
expected2 = rules.TitleTrailingWhitespace()
|
||||||
rule = collection.find_rule('title-trailing-whitespace')
|
rule = collection.find_rule("title-trailing-whitespace")
|
||||||
self.assertEqual(rule, expected2)
|
self.assertEqual(rule, expected2)
|
||||||
self.assertEqual(rule.my_attr, "föo")
|
self.assertEqual(rule.my_attr, "föo")
|
||||||
|
|
||||||
# find non-existing
|
# find non-existing
|
||||||
rule = collection.find_rule(u'föo')
|
rule = collection.find_rule("föo")
|
||||||
self.assertIsNone(rule)
|
self.assertIsNone(rule)
|
||||||
|
|
||||||
def test_delete_rules_by_attr(self):
|
def test_delete_rules_by_attr(self):
|
||||||
|
|
106
gitlint-core/gitlint/tests/contrib/rules/test_authors_commit.py
Normal file
106
gitlint-core/gitlint/tests/contrib/rules/test_authors_commit.py
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
from collections import namedtuple
|
||||||
|
from unittest.mock import patch
|
||||||
|
from gitlint.tests.base import BaseTestCase
|
||||||
|
from gitlint.rules import RuleViolation
|
||||||
|
from gitlint.config import LintConfig
|
||||||
|
|
||||||
|
from gitlint.contrib.rules.authors_commit import AllowedAuthors
|
||||||
|
|
||||||
|
|
||||||
|
class ContribAuthorsCommitTests(BaseTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
author = namedtuple("Author", "name, email")
|
||||||
|
self.author_1 = author("John Doe", "john.doe@mail.com")
|
||||||
|
self.author_2 = author("Bob Smith", "bob.smith@mail.com")
|
||||||
|
self.rule = AllowedAuthors()
|
||||||
|
self.gitcontext = self.get_gitcontext()
|
||||||
|
|
||||||
|
def get_gitcontext(self):
|
||||||
|
gitcontext = self.gitcontext(self.get_sample("commit_message/sample1"))
|
||||||
|
gitcontext.repository_path = self.get_sample_path("config")
|
||||||
|
return gitcontext
|
||||||
|
|
||||||
|
def get_commit(self, name, email):
|
||||||
|
commit = self.gitcommit("commit_message/sample1", author_name=name, author_email=email)
|
||||||
|
commit.message.context = self.gitcontext
|
||||||
|
return commit
|
||||||
|
|
||||||
|
def test_enable(self):
|
||||||
|
for rule_ref in ["CC3", "contrib-allowed-authors"]:
|
||||||
|
config = LintConfig()
|
||||||
|
config.contrib = [rule_ref]
|
||||||
|
self.assertIn(AllowedAuthors(), config.rules)
|
||||||
|
|
||||||
|
def test_authors_succeeds(self):
|
||||||
|
for author in [self.author_1, self.author_2]:
|
||||||
|
commit = self.get_commit(author.name, author.email)
|
||||||
|
violations = self.rule.validate(commit)
|
||||||
|
self.assertListEqual([], violations)
|
||||||
|
|
||||||
|
def test_authors_email_is_case_insensitive(self):
|
||||||
|
for email in [
|
||||||
|
self.author_2.email.capitalize(),
|
||||||
|
self.author_2.email.lower(),
|
||||||
|
self.author_2.email.upper(),
|
||||||
|
]:
|
||||||
|
commit = self.get_commit(self.author_2.name, email)
|
||||||
|
violations = self.rule.validate(commit)
|
||||||
|
self.assertListEqual([], violations)
|
||||||
|
|
||||||
|
def test_authors_name_is_case_sensitive(self):
|
||||||
|
for name in [self.author_2.name.lower(), self.author_2.name.upper()]:
|
||||||
|
commit = self.get_commit(name, self.author_2.email)
|
||||||
|
violations = self.rule.validate(commit)
|
||||||
|
expected_violation = RuleViolation(
|
||||||
|
"CC3",
|
||||||
|
f"Author not in 'AUTHORS' file: " f'"{name} <{self.author_2.email}>"',
|
||||||
|
)
|
||||||
|
self.assertListEqual([expected_violation], violations)
|
||||||
|
|
||||||
|
def test_authors_bad_name_fails(self):
|
||||||
|
for name in ["", "root"]:
|
||||||
|
commit = self.get_commit(name, self.author_2.email)
|
||||||
|
violations = self.rule.validate(commit)
|
||||||
|
expected_violation = RuleViolation(
|
||||||
|
"CC3",
|
||||||
|
f"Author not in 'AUTHORS' file: " f'"{name} <{self.author_2.email}>"',
|
||||||
|
)
|
||||||
|
self.assertListEqual([expected_violation], violations)
|
||||||
|
|
||||||
|
def test_authors_bad_email_fails(self):
|
||||||
|
for email in ["", "root@example.com"]:
|
||||||
|
commit = self.get_commit(self.author_2.name, email)
|
||||||
|
violations = self.rule.validate(commit)
|
||||||
|
expected_violation = RuleViolation(
|
||||||
|
"CC3",
|
||||||
|
f"Author not in 'AUTHORS' file: " f'"{self.author_2.name} <{email}>"',
|
||||||
|
)
|
||||||
|
self.assertListEqual([expected_violation], violations)
|
||||||
|
|
||||||
|
def test_authors_invalid_combination_fails(self):
|
||||||
|
commit = self.get_commit(self.author_1.name, self.author_2.email)
|
||||||
|
violations = self.rule.validate(commit)
|
||||||
|
expected_violation = RuleViolation(
|
||||||
|
"CC3",
|
||||||
|
f"Author not in 'AUTHORS' file: " f'"{self.author_1.name} <{self.author_2.email}>"',
|
||||||
|
)
|
||||||
|
self.assertListEqual([expected_violation], violations)
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"gitlint.contrib.rules.authors_commit.Path.read_text",
|
||||||
|
return_value="John Doe <john.doe@mail.com>",
|
||||||
|
)
|
||||||
|
def test_read_authors_file(self, _mock_read_text):
|
||||||
|
authors, authors_file_name = AllowedAuthors._read_authors_from_file(self.gitcontext)
|
||||||
|
self.assertEqual(authors_file_name, "AUTHORS")
|
||||||
|
self.assertEqual(len(authors), 1)
|
||||||
|
self.assertEqual(authors, {self.author_1})
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"gitlint.contrib.rules.authors_commit.Path.exists",
|
||||||
|
return_value=False,
|
||||||
|
)
|
||||||
|
def test_read_authors_file_missing_file(self, _mock_iterdir):
|
||||||
|
with self.assertRaises(FileNotFoundError) as err:
|
||||||
|
AllowedAuthors._read_authors_from_file(self.gitcontext)
|
||||||
|
self.assertEqual(err.exception.args[0], "AUTHORS file not found")
|
|
@ -1,5 +1,3 @@
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from gitlint.tests.base import BaseTestCase
|
from gitlint.tests.base import BaseTestCase
|
||||||
from gitlint.rules import RuleViolation
|
from gitlint.rules import RuleViolation
|
||||||
from gitlint.contrib.rules.conventional_commit import ConventionalCommit
|
from gitlint.contrib.rules.conventional_commit import ConventionalCommit
|
||||||
|
@ -7,10 +5,9 @@ from gitlint.config import LintConfig
|
||||||
|
|
||||||
|
|
||||||
class ContribConventionalCommitTests(BaseTestCase):
|
class ContribConventionalCommitTests(BaseTestCase):
|
||||||
|
|
||||||
def test_enable(self):
|
def test_enable(self):
|
||||||
# Test that rule can be enabled in config
|
# Test that rule can be enabled in config
|
||||||
for rule_ref in ['CT1', 'contrib-title-conventional-commits']:
|
for rule_ref in ["CT1", "contrib-title-conventional-commits"]:
|
||||||
config = LintConfig()
|
config = LintConfig()
|
||||||
config.contrib = [rule_ref]
|
config.contrib = [rule_ref]
|
||||||
self.assertIn(ConventionalCommit(), config.rules)
|
self.assertIn(ConventionalCommit(), config.rules)
|
||||||
|
@ -24,28 +21,38 @@ class ContribConventionalCommitTests(BaseTestCase):
|
||||||
self.assertListEqual([], violations)
|
self.assertListEqual([], violations)
|
||||||
|
|
||||||
# assert violation on wrong type
|
# assert violation on wrong type
|
||||||
expected_violation = RuleViolation("CT1", "Title does not start with one of fix, feat, chore, docs,"
|
expected_violation = RuleViolation(
|
||||||
" style, refactor, perf, test, revert, ci, build", "bår: foo")
|
"CT1",
|
||||||
|
"Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert, ci, build",
|
||||||
|
"bår: foo",
|
||||||
|
)
|
||||||
violations = rule.validate("bår: foo", None)
|
violations = rule.validate("bår: foo", None)
|
||||||
self.assertListEqual([expected_violation], violations)
|
self.assertListEqual([expected_violation], violations)
|
||||||
|
|
||||||
# assert violation when use strange chars after correct type
|
# assert violation when use strange chars after correct type
|
||||||
expected_violation = RuleViolation("CT1", "Title does not start with one of fix, feat, chore, docs,"
|
expected_violation = RuleViolation(
|
||||||
" style, refactor, perf, test, revert, ci, build",
|
"CT1",
|
||||||
"feat_wrong_chars: föo")
|
"Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert, ci, build",
|
||||||
|
"feat_wrong_chars: föo",
|
||||||
|
)
|
||||||
violations = rule.validate("feat_wrong_chars: föo", None)
|
violations = rule.validate("feat_wrong_chars: föo", None)
|
||||||
self.assertListEqual([expected_violation], violations)
|
self.assertListEqual([expected_violation], violations)
|
||||||
|
|
||||||
# assert violation when use strange chars after correct type
|
# assert violation when use strange chars after correct type
|
||||||
expected_violation = RuleViolation("CT1", "Title does not start with one of fix, feat, chore, docs,"
|
expected_violation = RuleViolation(
|
||||||
" style, refactor, perf, test, revert, ci, build",
|
"CT1",
|
||||||
"feat_wrong_chars(scope): föo")
|
"Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert, ci, build",
|
||||||
|
"feat_wrong_chars(scope): föo",
|
||||||
|
)
|
||||||
violations = rule.validate("feat_wrong_chars(scope): föo", None)
|
violations = rule.validate("feat_wrong_chars(scope): föo", None)
|
||||||
self.assertListEqual([expected_violation], violations)
|
self.assertListEqual([expected_violation], violations)
|
||||||
|
|
||||||
# assert violation on wrong format
|
# assert violation on wrong format
|
||||||
expected_violation = RuleViolation("CT1", "Title does not follow ConventionalCommits.org format "
|
expected_violation = RuleViolation(
|
||||||
"'type(optional-scope): description'", "fix föo")
|
"CT1",
|
||||||
|
"Title does not follow ConventionalCommits.org format 'type(optional-scope): description'",
|
||||||
|
"fix föo",
|
||||||
|
)
|
||||||
violations = rule.validate("fix föo", None)
|
violations = rule.validate("fix föo", None)
|
||||||
self.assertListEqual([expected_violation], violations)
|
self.assertListEqual([expected_violation], violations)
|
||||||
|
|
||||||
|
@ -58,7 +65,7 @@ class ContribConventionalCommitTests(BaseTestCase):
|
||||||
self.assertListEqual([], violations)
|
self.assertListEqual([], violations)
|
||||||
|
|
||||||
# assert no violation when adding new type
|
# assert no violation when adding new type
|
||||||
rule = ConventionalCommit({'types': ["föo", "bär"]})
|
rule = ConventionalCommit({"types": ["föo", "bär"]})
|
||||||
for typ in ["föo", "bär"]:
|
for typ in ["föo", "bär"]:
|
||||||
violations = rule.validate(typ + ": hür dur", None)
|
violations = rule.validate(typ + ": hür dur", None)
|
||||||
self.assertListEqual([], violations)
|
self.assertListEqual([], violations)
|
||||||
|
@ -69,7 +76,7 @@ class ContribConventionalCommitTests(BaseTestCase):
|
||||||
self.assertListEqual([expected_violation], violations)
|
self.assertListEqual([expected_violation], violations)
|
||||||
|
|
||||||
# assert no violation when adding new type named with numbers
|
# assert no violation when adding new type named with numbers
|
||||||
rule = ConventionalCommit({'types': ["föo123", "123bär"]})
|
rule = ConventionalCommit({"types": ["föo123", "123bär"]})
|
||||||
for typ in ["föo123", "123bär"]:
|
for typ in ["föo123", "123bär"]:
|
||||||
violations = rule.validate(typ + ": hür dur", None)
|
violations = rule.validate(typ + ": hür dur", None)
|
||||||
self.assertListEqual([], violations)
|
self.assertListEqual([], violations)
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
from gitlint.tests.base import BaseTestCase
|
||||||
|
from gitlint.rules import RuleViolation
|
||||||
|
from gitlint.contrib.rules.disallow_cleanup_commits import DisallowCleanupCommits
|
||||||
|
|
||||||
|
from gitlint.config import LintConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ContribDisallowCleanupCommitsTest(BaseTestCase):
|
||||||
|
def test_enable(self):
|
||||||
|
# Test that rule can be enabled in config
|
||||||
|
for rule_ref in ["CC2", "contrib-disallow-cleanup-commits"]:
|
||||||
|
config = LintConfig()
|
||||||
|
config.contrib = [rule_ref]
|
||||||
|
self.assertIn(DisallowCleanupCommits(), config.rules)
|
||||||
|
|
||||||
|
def test_disallow_fixup_squash_commit(self):
|
||||||
|
# No violations when no 'fixup!' line and no 'squash!' line is present
|
||||||
|
rule = DisallowCleanupCommits()
|
||||||
|
violations = rule.validate(self.gitcommit("Föobar\n\nMy Body"))
|
||||||
|
self.assertListEqual(violations, [])
|
||||||
|
|
||||||
|
# Assert violation when 'fixup!' in title
|
||||||
|
violations = rule.validate(self.gitcommit("fixup! Föobar\n\nMy Body"))
|
||||||
|
expected_violation = RuleViolation("CC2", "Fixup commits are not allowed", line_nr=1)
|
||||||
|
self.assertListEqual(violations, [expected_violation])
|
||||||
|
|
||||||
|
# Assert violation when 'squash!' in title
|
||||||
|
violations = rule.validate(self.gitcommit("squash! Föobar\n\nMy Body"))
|
||||||
|
expected_violation = RuleViolation("CC2", "Squash commits are not allowed", line_nr=1)
|
||||||
|
self.assertListEqual(violations, [expected_violation])
|
||||||
|
|
||||||
|
# Assert violation when 'amend!' in title
|
||||||
|
violations = rule.validate(self.gitcommit("amend! Föobar\n\nMy Body"))
|
||||||
|
expected_violation = RuleViolation("CC2", "Amend commits are not allowed", line_nr=1)
|
||||||
|
self.assertListEqual(violations, [expected_violation])
|
|
@ -1,5 +1,3 @@
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from gitlint.tests.base import BaseTestCase
|
from gitlint.tests.base import BaseTestCase
|
||||||
from gitlint.rules import RuleViolation
|
from gitlint.rules import RuleViolation
|
||||||
from gitlint.contrib.rules.signedoff_by import SignedOffBy
|
from gitlint.contrib.rules.signedoff_by import SignedOffBy
|
||||||
|
@ -8,10 +6,9 @@ from gitlint.config import LintConfig
|
||||||
|
|
||||||
|
|
||||||
class ContribSignedOffByTests(BaseTestCase):
|
class ContribSignedOffByTests(BaseTestCase):
|
||||||
|
|
||||||
def test_enable(self):
|
def test_enable(self):
|
||||||
# Test that rule can be enabled in config
|
# Test that rule can be enabled in config
|
||||||
for rule_ref in ['CC1', 'contrib-body-requires-signed-off-by']:
|
for rule_ref in ["CC1", "contrib-body-requires-signed-off-by"]:
|
||||||
config = LintConfig()
|
config = LintConfig()
|
||||||
config.contrib = [rule_ref]
|
config.contrib = [rule_ref]
|
||||||
self.assertIn(SignedOffBy(), config.rules)
|
self.assertIn(SignedOffBy(), config.rules)
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from gitlint.tests.base import BaseTestCase
|
from gitlint.tests.base import BaseTestCase
|
||||||
|
@ -8,13 +7,12 @@ from gitlint import rule_finder, rules
|
||||||
|
|
||||||
|
|
||||||
class ContribRuleTests(BaseTestCase):
|
class ContribRuleTests(BaseTestCase):
|
||||||
|
|
||||||
CONTRIB_DIR = os.path.dirname(os.path.realpath(contrib_rules.__file__))
|
CONTRIB_DIR = os.path.dirname(os.path.realpath(contrib_rules.__file__))
|
||||||
|
|
||||||
def test_contrib_tests_exist(self):
|
def test_contrib_tests_exist(self):
|
||||||
""" Tests that every contrib rule file has an associated test file.
|
"""Tests that every contrib rule file has an associated test file.
|
||||||
While this doesn't guarantee that every contrib rule has associated tests (as we don't check the content
|
While this doesn't guarantee that every contrib rule has associated tests (as we don't check the content
|
||||||
of the tests file), it's a good leading indicator. """
|
of the tests file), it's a good leading indicator."""
|
||||||
|
|
||||||
contrib_tests_dir = os.path.dirname(os.path.realpath(contrib_tests.__file__))
|
contrib_tests_dir = os.path.dirname(os.path.realpath(contrib_tests.__file__))
|
||||||
contrib_test_files = os.listdir(contrib_tests_dir)
|
contrib_test_files = os.listdir(contrib_tests_dir)
|
||||||
|
@ -22,16 +20,18 @@ class ContribRuleTests(BaseTestCase):
|
||||||
# Find all python files in the contrib dir and assert there's a corresponding test file
|
# Find all python files in the contrib dir and assert there's a corresponding test file
|
||||||
for filename in os.listdir(self.CONTRIB_DIR):
|
for filename in os.listdir(self.CONTRIB_DIR):
|
||||||
if filename.endswith(".py") and filename not in ["__init__.py"]:
|
if filename.endswith(".py") and filename not in ["__init__.py"]:
|
||||||
expected_test_file = "test_" + filename
|
expected_test_file = f"test_{filename}"
|
||||||
error_msg = "Every Contrib Rule must have associated tests. " + \
|
error_msg = (
|
||||||
f"Expected test file {os.path.join(contrib_tests_dir, expected_test_file)} not found."
|
"Every Contrib Rule must have associated tests. "
|
||||||
|
f"Expected test file {os.path.join(contrib_tests_dir, expected_test_file)} not found."
|
||||||
|
)
|
||||||
self.assertIn(expected_test_file, contrib_test_files, error_msg)
|
self.assertIn(expected_test_file, contrib_test_files, error_msg)
|
||||||
|
|
||||||
def test_contrib_rule_naming_conventions(self):
|
def test_contrib_rule_naming_conventions(self):
|
||||||
""" Tests that contrib rules follow certain naming conventions.
|
"""Tests that contrib rules follow certain naming conventions.
|
||||||
We can test for this at test time (and not during runtime like rule_finder.assert_valid_rule_class does)
|
We can test for this at test time (and not during runtime like rule_finder.assert_valid_rule_class does)
|
||||||
because these are contrib rules: once they're part of gitlint they can't change unless they pass this test
|
because these are contrib rules: once they're part of gitlint they can't change unless they pass this test
|
||||||
again.
|
again.
|
||||||
"""
|
"""
|
||||||
rule_classes = rule_finder.find_rule_classes(self.CONTRIB_DIR)
|
rule_classes = rule_finder.find_rule_classes(self.CONTRIB_DIR)
|
||||||
|
|
||||||
|
@ -47,10 +47,10 @@ class ContribRuleTests(BaseTestCase):
|
||||||
self.assertTrue(clazz.id.startswith("CB"))
|
self.assertTrue(clazz.id.startswith("CB"))
|
||||||
|
|
||||||
def test_contrib_rule_uniqueness(self):
|
def test_contrib_rule_uniqueness(self):
|
||||||
""" Tests that all contrib rules have unique identifiers.
|
"""Tests that all contrib rules have unique identifiers.
|
||||||
We can test for this at test time (and not during runtime like rule_finder.assert_valid_rule_class does)
|
We can test for this at test time (and not during runtime like rule_finder.assert_valid_rule_class does)
|
||||||
because these are contrib rules: once they're part of gitlint they can't change unless they pass this test
|
because these are contrib rules: once they're part of gitlint they can't change unless they pass this test
|
||||||
again.
|
again.
|
||||||
"""
|
"""
|
||||||
rule_classes = rule_finder.find_rule_classes(self.CONTRIB_DIR)
|
rule_classes = rule_finder.find_rule_classes(self.CONTRIB_DIR)
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ class ContribRuleTests(BaseTestCase):
|
||||||
self.assertEqual(len(set(class_ids)), len(class_ids))
|
self.assertEqual(len(set(class_ids)), len(class_ids))
|
||||||
|
|
||||||
def test_contrib_rule_instantiated(self):
|
def test_contrib_rule_instantiated(self):
|
||||||
""" Tests that all contrib rules can be instantiated without errors. """
|
"""Tests that all contrib rules can be instantiated without errors."""
|
||||||
rule_classes = rule_finder.find_rule_classes(self.CONTRIB_DIR)
|
rule_classes = rule_finder.find_rule_classes(self.CONTRIB_DIR)
|
||||||
|
|
||||||
# No exceptions = what we want :-)
|
# No exceptions = what we want :-)
|
||||||
|
|
|
@ -13,11 +13,13 @@ contrib: []
|
||||||
ignore: title-trailing-whitespace,B2
|
ignore: title-trailing-whitespace,B2
|
||||||
ignore-merge-commits: False
|
ignore-merge-commits: False
|
||||||
ignore-fixup-commits: True
|
ignore-fixup-commits: True
|
||||||
|
ignore-fixup-amend-commits: True
|
||||||
ignore-squash-commits: True
|
ignore-squash-commits: True
|
||||||
ignore-revert-commits: True
|
ignore-revert-commits: True
|
||||||
ignore-stdin: False
|
ignore-stdin: False
|
||||||
staged: False
|
staged: False
|
||||||
fail-without-commits: False
|
fail-without-commits: False
|
||||||
|
regex-style-search: False
|
||||||
verbosity: 1
|
verbosity: 1
|
||||||
debug: True
|
debug: True
|
||||||
target: {target}
|
target: {target}
|
||||||
|
@ -59,7 +61,7 @@ target: {target}
|
||||||
B8: body-match-regex
|
B8: body-match-regex
|
||||||
regex=None
|
regex=None
|
||||||
M1: author-valid-email
|
M1: author-valid-email
|
||||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||||
|
|
||||||
DEBUG: gitlint.cli No --msg-filename flag, no or empty data passed to stdin. Using the local repo.
|
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.git ('rev-list', 'foo...bar')
|
||||||
|
@ -67,8 +69,8 @@ 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 ('log', '6f29bf81a8322a04071bb794666e48c443a90360', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B')
|
||||||
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
||||||
DEBUG: gitlint.lint Linting commit 6f29bf81a8322a04071bb794666e48c443a90360
|
DEBUG: gitlint.lint Linting commit 6f29bf81a8322a04071bb794666e48c443a90360
|
||||||
|
DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--numstat', '-r', '--root', '6f29bf81a8322a04071bb794666e48c443a90360')
|
||||||
DEBUG: gitlint.git ('branch', '--contains', '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
|
DEBUG: gitlint.lint Commit Object
|
||||||
--- Commit Message ----
|
--- Commit Message ----
|
||||||
commït-title1
|
commït-title1
|
||||||
|
@ -79,15 +81,20 @@ Author: test åuthor1 <test-email1@föo.com>
|
||||||
Date: 2016-12-03 15:28:15 +0100
|
Date: 2016-12-03 15:28:15 +0100
|
||||||
is-merge-commit: False
|
is-merge-commit: False
|
||||||
is-fixup-commit: False
|
is-fixup-commit: False
|
||||||
|
is-fixup-amend-commit: False
|
||||||
is-squash-commit: False
|
is-squash-commit: False
|
||||||
is-revert-commit: False
|
is-revert-commit: False
|
||||||
|
Parents: ['a123']
|
||||||
Branches: ['commit-1-branch-1', 'commit-1-branch-2']
|
Branches: ['commit-1-branch-1', 'commit-1-branch-2']
|
||||||
Changed Files: ['commit-1/file-1', 'commit-1/file-2']
|
Changed Files: ['commit-1/file-1', 'commit-1/file-2']
|
||||||
|
Changed Files Stats:
|
||||||
|
commit-1/file-1: 5 additions, 8 deletions
|
||||||
|
commit-1/file-2: 2 additions, 9 deletions
|
||||||
-----------------------
|
-----------------------
|
||||||
DEBUG: gitlint.git ('log', '25053ccec5e28e1bb8f7551fdbb5ab213ada2401', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B')
|
DEBUG: gitlint.git ('log', '25053ccec5e28e1bb8f7551fdbb5ab213ada2401', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B')
|
||||||
DEBUG: gitlint.lint Linting commit 25053ccec5e28e1bb8f7551fdbb5ab213ada2401
|
DEBUG: gitlint.lint Linting commit 25053ccec5e28e1bb8f7551fdbb5ab213ada2401
|
||||||
|
DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--numstat', '-r', '--root', '25053ccec5e28e1bb8f7551fdbb5ab213ada2401')
|
||||||
DEBUG: gitlint.git ('branch', '--contains', '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
|
DEBUG: gitlint.lint Commit Object
|
||||||
--- Commit Message ----
|
--- Commit Message ----
|
||||||
commït-title2.
|
commït-title2.
|
||||||
|
@ -98,15 +105,20 @@ Author: test åuthor2 <test-email2@föo.com>
|
||||||
Date: 2016-12-04 15:28:15 +0100
|
Date: 2016-12-04 15:28:15 +0100
|
||||||
is-merge-commit: False
|
is-merge-commit: False
|
||||||
is-fixup-commit: False
|
is-fixup-commit: False
|
||||||
|
is-fixup-amend-commit: False
|
||||||
is-squash-commit: False
|
is-squash-commit: False
|
||||||
is-revert-commit: False
|
is-revert-commit: False
|
||||||
|
Parents: ['b123']
|
||||||
Branches: ['commit-2-branch-1', 'commit-2-branch-2']
|
Branches: ['commit-2-branch-1', 'commit-2-branch-2']
|
||||||
Changed Files: ['commit-2/file-1', 'commit-2/file-2']
|
Changed Files: ['commit-2/file-1', 'commit-2/file-2']
|
||||||
|
Changed Files Stats:
|
||||||
|
commit-2/file-1: 5 additions, 8 deletions
|
||||||
|
commit-2/file-2: 7 additions, 9 deletions
|
||||||
-----------------------
|
-----------------------
|
||||||
DEBUG: gitlint.git ('log', '4da2656b0dadc76c7ee3fd0243a96cb64007f125', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B')
|
DEBUG: gitlint.git ('log', '4da2656b0dadc76c7ee3fd0243a96cb64007f125', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B')
|
||||||
DEBUG: gitlint.lint Linting commit 4da2656b0dadc76c7ee3fd0243a96cb64007f125
|
DEBUG: gitlint.lint Linting commit 4da2656b0dadc76c7ee3fd0243a96cb64007f125
|
||||||
|
DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--numstat', '-r', '--root', '4da2656b0dadc76c7ee3fd0243a96cb64007f125')
|
||||||
DEBUG: gitlint.git ('branch', '--contains', '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
|
DEBUG: gitlint.lint Commit Object
|
||||||
--- Commit Message ----
|
--- Commit Message ----
|
||||||
föobar
|
föobar
|
||||||
|
@ -116,9 +128,14 @@ Author: test åuthor3 <test-email3@föo.com>
|
||||||
Date: 2016-12-05 15:28:15 +0100
|
Date: 2016-12-05 15:28:15 +0100
|
||||||
is-merge-commit: False
|
is-merge-commit: False
|
||||||
is-fixup-commit: False
|
is-fixup-commit: False
|
||||||
|
is-fixup-amend-commit: False
|
||||||
is-squash-commit: False
|
is-squash-commit: False
|
||||||
is-revert-commit: False
|
is-revert-commit: False
|
||||||
|
Parents: ['c123']
|
||||||
Branches: ['commit-3-branch-1', 'commit-3-branch-2']
|
Branches: ['commit-3-branch-1', 'commit-3-branch-2']
|
||||||
Changed Files: ['commit-3/file-1', 'commit-3/file-2']
|
Changed Files: ['commit-3/file-1', 'commit-3/file-2']
|
||||||
|
Changed Files Stats:
|
||||||
|
commit-3/file-1: 1 additions, 4 deletions
|
||||||
|
commit-3/file-2: 3 additions, 4 deletions
|
||||||
-----------------------
|
-----------------------
|
||||||
DEBUG: gitlint.cli Exit Code = 6
|
DEBUG: gitlint.cli Exit Code = 6
|
|
@ -13,11 +13,13 @@ contrib: []
|
||||||
ignore:
|
ignore:
|
||||||
ignore-merge-commits: True
|
ignore-merge-commits: True
|
||||||
ignore-fixup-commits: True
|
ignore-fixup-commits: True
|
||||||
|
ignore-fixup-amend-commits: True
|
||||||
ignore-squash-commits: True
|
ignore-squash-commits: True
|
||||||
ignore-revert-commits: True
|
ignore-revert-commits: True
|
||||||
ignore-stdin: False
|
ignore-stdin: False
|
||||||
staged: False
|
staged: False
|
||||||
fail-without-commits: False
|
fail-without-commits: False
|
||||||
|
regex-style-search: False
|
||||||
verbosity: 3
|
verbosity: 3
|
||||||
debug: True
|
debug: True
|
||||||
target: {target}
|
target: {target}
|
||||||
|
@ -59,7 +61,7 @@ target: {target}
|
||||||
B8: body-match-regex
|
B8: body-match-regex
|
||||||
regex=None
|
regex=None
|
||||||
M1: author-valid-email
|
M1: author-valid-email
|
||||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||||
|
|
||||||
DEBUG: gitlint.cli Stdin data: 'WIP: tïtle
|
DEBUG: gitlint.cli Stdin data: 'WIP: tïtle
|
||||||
'
|
'
|
||||||
|
@ -75,9 +77,12 @@ Author: None <None>
|
||||||
Date: None
|
Date: None
|
||||||
is-merge-commit: False
|
is-merge-commit: False
|
||||||
is-fixup-commit: False
|
is-fixup-commit: False
|
||||||
|
is-fixup-amend-commit: False
|
||||||
is-squash-commit: False
|
is-squash-commit: False
|
||||||
is-revert-commit: False
|
is-revert-commit: False
|
||||||
|
Parents: []
|
||||||
Branches: []
|
Branches: []
|
||||||
Changed Files: []
|
Changed Files: []
|
||||||
|
Changed Files Stats: {{}}
|
||||||
-----------------------
|
-----------------------
|
||||||
DEBUG: gitlint.cli Exit Code = 3
|
DEBUG: gitlint.cli Exit Code = 3
|
|
@ -13,11 +13,13 @@ contrib: []
|
||||||
ignore:
|
ignore:
|
||||||
ignore-merge-commits: True
|
ignore-merge-commits: True
|
||||||
ignore-fixup-commits: True
|
ignore-fixup-commits: True
|
||||||
|
ignore-fixup-amend-commits: True
|
||||||
ignore-squash-commits: True
|
ignore-squash-commits: True
|
||||||
ignore-revert-commits: True
|
ignore-revert-commits: True
|
||||||
ignore-stdin: False
|
ignore-stdin: False
|
||||||
staged: True
|
staged: True
|
||||||
fail-without-commits: False
|
fail-without-commits: False
|
||||||
|
regex-style-search: False
|
||||||
verbosity: 3
|
verbosity: 3
|
||||||
debug: True
|
debug: True
|
||||||
target: {target}
|
target: {target}
|
||||||
|
@ -59,17 +61,17 @@ target: {target}
|
||||||
B8: body-match-regex
|
B8: body-match-regex
|
||||||
regex=None
|
regex=None
|
||||||
M1: author-valid-email
|
M1: author-valid-email
|
||||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||||
|
|
||||||
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
|
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
|
||||||
DEBUG: gitlint.cli Using --msg-filename.
|
DEBUG: gitlint.cli Using --msg-filename.
|
||||||
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
||||||
DEBUG: gitlint.cli Linting 1 commit(s)
|
DEBUG: gitlint.cli Linting 1 commit(s)
|
||||||
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
|
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
|
||||||
|
DEBUG: gitlint.git ('diff', '--staged', '--numstat', '-r')
|
||||||
DEBUG: gitlint.git ('config', '--get', 'user.name')
|
DEBUG: gitlint.git ('config', '--get', 'user.name')
|
||||||
DEBUG: gitlint.git ('config', '--get', 'user.email')
|
DEBUG: gitlint.git ('config', '--get', 'user.email')
|
||||||
DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
|
DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
|
||||||
DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r')
|
|
||||||
DEBUG: gitlint.lint Commit Object
|
DEBUG: gitlint.lint Commit Object
|
||||||
--- Commit Message ----
|
--- Commit Message ----
|
||||||
WIP: msg-filename tïtle
|
WIP: msg-filename tïtle
|
||||||
|
@ -78,9 +80,14 @@ Author: föo user <föo@bar.com>
|
||||||
Date: 2020-02-19 12:18:46 +0100
|
Date: 2020-02-19 12:18:46 +0100
|
||||||
is-merge-commit: False
|
is-merge-commit: False
|
||||||
is-fixup-commit: False
|
is-fixup-commit: False
|
||||||
|
is-fixup-amend-commit: False
|
||||||
is-squash-commit: False
|
is-squash-commit: False
|
||||||
is-revert-commit: False
|
is-revert-commit: False
|
||||||
|
Parents: []
|
||||||
Branches: ['my-branch']
|
Branches: ['my-branch']
|
||||||
Changed Files: ['commit-1/file-1', 'commit-1/file-2']
|
Changed Files: ['commit-1/file-1', 'commit-1/file-2']
|
||||||
|
Changed Files Stats:
|
||||||
|
commit-1/file-1: 3 additions, 4 deletions
|
||||||
|
commit-1/file-2: 4 additions, 7 deletions
|
||||||
-----------------------
|
-----------------------
|
||||||
DEBUG: gitlint.cli Exit Code = 2
|
DEBUG: gitlint.cli Exit Code = 2
|
|
@ -13,11 +13,13 @@ contrib: []
|
||||||
ignore:
|
ignore:
|
||||||
ignore-merge-commits: True
|
ignore-merge-commits: True
|
||||||
ignore-fixup-commits: True
|
ignore-fixup-commits: True
|
||||||
|
ignore-fixup-amend-commits: True
|
||||||
ignore-squash-commits: True
|
ignore-squash-commits: True
|
||||||
ignore-revert-commits: True
|
ignore-revert-commits: True
|
||||||
ignore-stdin: False
|
ignore-stdin: False
|
||||||
staged: True
|
staged: True
|
||||||
fail-without-commits: False
|
fail-without-commits: False
|
||||||
|
regex-style-search: False
|
||||||
verbosity: 3
|
verbosity: 3
|
||||||
debug: True
|
debug: True
|
||||||
target: {target}
|
target: {target}
|
||||||
|
@ -59,7 +61,7 @@ target: {target}
|
||||||
B8: body-match-regex
|
B8: body-match-regex
|
||||||
regex=None
|
regex=None
|
||||||
M1: author-valid-email
|
M1: author-valid-email
|
||||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||||
|
|
||||||
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
|
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
|
||||||
DEBUG: gitlint.cli Stdin data: 'WIP: tïtle
|
DEBUG: gitlint.cli Stdin data: 'WIP: tïtle
|
||||||
|
@ -68,10 +70,10 @@ DEBUG: gitlint.cli Stdin detected and not ignored. Using as input.
|
||||||
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
||||||
DEBUG: gitlint.cli Linting 1 commit(s)
|
DEBUG: gitlint.cli Linting 1 commit(s)
|
||||||
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
|
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
|
||||||
|
DEBUG: gitlint.git ('diff', '--staged', '--numstat', '-r')
|
||||||
DEBUG: gitlint.git ('config', '--get', 'user.name')
|
DEBUG: gitlint.git ('config', '--get', 'user.name')
|
||||||
DEBUG: gitlint.git ('config', '--get', 'user.email')
|
DEBUG: gitlint.git ('config', '--get', 'user.email')
|
||||||
DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
|
DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
|
||||||
DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r')
|
|
||||||
DEBUG: gitlint.lint Commit Object
|
DEBUG: gitlint.lint Commit Object
|
||||||
--- Commit Message ----
|
--- Commit Message ----
|
||||||
WIP: tïtle
|
WIP: tïtle
|
||||||
|
@ -80,9 +82,14 @@ Author: föo user <föo@bar.com>
|
||||||
Date: 2020-02-19 12:18:46 +0100
|
Date: 2020-02-19 12:18:46 +0100
|
||||||
is-merge-commit: False
|
is-merge-commit: False
|
||||||
is-fixup-commit: False
|
is-fixup-commit: False
|
||||||
|
is-fixup-amend-commit: False
|
||||||
is-squash-commit: False
|
is-squash-commit: False
|
||||||
is-revert-commit: False
|
is-revert-commit: False
|
||||||
|
Parents: []
|
||||||
Branches: ['my-branch']
|
Branches: ['my-branch']
|
||||||
Changed Files: ['commit-1/file-1', 'commit-1/file-2']
|
Changed Files: ['commit-1/file-1', 'commit-1/file-2']
|
||||||
|
Changed Files Stats:
|
||||||
|
commit-1/file-1: 1 additions, 5 deletions
|
||||||
|
commit-1/file-2: 8 additions, 9 deletions
|
||||||
-----------------------
|
-----------------------
|
||||||
DEBUG: gitlint.cli Exit Code = 3
|
DEBUG: gitlint.cli Exit Code = 3
|
|
@ -13,11 +13,13 @@ contrib: []
|
||||||
ignore:
|
ignore:
|
||||||
ignore-merge-commits: True
|
ignore-merge-commits: True
|
||||||
ignore-fixup-commits: True
|
ignore-fixup-commits: True
|
||||||
|
ignore-fixup-amend-commits: True
|
||||||
ignore-squash-commits: True
|
ignore-squash-commits: True
|
||||||
ignore-revert-commits: True
|
ignore-revert-commits: True
|
||||||
ignore-stdin: False
|
ignore-stdin: False
|
||||||
staged: False
|
staged: False
|
||||||
fail-without-commits: False
|
fail-without-commits: False
|
||||||
|
regex-style-search: False
|
||||||
verbosity: 3
|
verbosity: 3
|
||||||
debug: True
|
debug: True
|
||||||
target: {target}
|
target: {target}
|
||||||
|
@ -59,7 +61,7 @@ target: {target}
|
||||||
B8: body-match-regex
|
B8: body-match-regex
|
||||||
regex=None
|
regex=None
|
||||||
M1: author-valid-email
|
M1: author-valid-email
|
||||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||||
T5:extra-wörds: title-must-not-contain-word:extra-wörds
|
T5:extra-wörds: title-must-not-contain-word:extra-wörds
|
||||||
words=hür,tëst
|
words=hür,tëst
|
||||||
T5:even-more-wörds: title-must-not-contain-word:even-more-wörds
|
T5:even-more-wörds: title-must-not-contain-word:even-more-wörds
|
||||||
|
@ -78,9 +80,12 @@ Author: None <None>
|
||||||
Date: None
|
Date: None
|
||||||
is-merge-commit: False
|
is-merge-commit: False
|
||||||
is-fixup-commit: False
|
is-fixup-commit: False
|
||||||
|
is-fixup-amend-commit: False
|
||||||
is-squash-commit: False
|
is-squash-commit: False
|
||||||
is-revert-commit: False
|
is-revert-commit: False
|
||||||
|
Parents: []
|
||||||
Branches: []
|
Branches: []
|
||||||
Changed Files: []
|
Changed Files: []
|
||||||
|
Changed Files Stats: {{}}
|
||||||
-----------------------
|
-----------------------
|
||||||
DEBUG: gitlint.cli Exit Code = 4
|
DEBUG: gitlint.cli Exit Code = 4
|
|
@ -1,7 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch, call
|
||||||
|
|
||||||
from gitlint.shell import ErrorReturnCode, CommandNotFound
|
from gitlint.shell import ErrorReturnCode, CommandNotFound
|
||||||
|
|
||||||
|
@ -10,25 +9,23 @@ from gitlint.git import GitContext, GitContextError, GitNotInstalledError, git_c
|
||||||
|
|
||||||
|
|
||||||
class GitTests(BaseTestCase):
|
class GitTests(BaseTestCase):
|
||||||
|
|
||||||
# Expected special_args passed to 'sh'
|
# Expected special_args passed to 'sh'
|
||||||
expected_sh_special_args = {
|
expected_sh_special_args = {"_tty_out": False, "_cwd": "fåke/path"}
|
||||||
'_tty_out': False,
|
|
||||||
'_cwd': "fåke/path"
|
|
||||||
}
|
|
||||||
|
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_get_latest_commit_command_not_found(self, sh):
|
def test_get_latest_commit_command_not_found(self, sh):
|
||||||
sh.git.side_effect = CommandNotFound("git")
|
sh.git.side_effect = CommandNotFound("git")
|
||||||
expected_msg = "'git' command not found. You need to install git to use gitlint on a local repository. " + \
|
expected_msg = (
|
||||||
"See https://git-scm.com/book/en/v2/Getting-Started-Installing-Git on how to install git."
|
"'git' command not found. You need to install git to use gitlint on a local repository. "
|
||||||
|
+ "See https://git-scm.com/book/en/v2/Getting-Started-Installing-Git on how to install git."
|
||||||
|
)
|
||||||
with self.assertRaisesMessage(GitNotInstalledError, expected_msg):
|
with self.assertRaisesMessage(GitNotInstalledError, expected_msg):
|
||||||
GitContext.from_local_repository("fåke/path")
|
GitContext.from_local_repository("fåke/path")
|
||||||
|
|
||||||
# assert that commit message was read using git command
|
# assert that commit message was read using git command
|
||||||
sh.git.assert_called_once_with("log", "-1", "--pretty=%H", **self.expected_sh_special_args)
|
sh.git.assert_called_once_with("log", "-1", "--pretty=%H", **self.expected_sh_special_args)
|
||||||
|
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_get_latest_commit_git_error(self, sh):
|
def test_get_latest_commit_git_error(self, sh):
|
||||||
# Current directory not a git repo
|
# Current directory not a git repo
|
||||||
err = b"fatal: Not a git repository (or any of the parent directories): .git"
|
err = b"fatal: Not a git repository (or any of the parent directories): .git"
|
||||||
|
@ -51,10 +48,10 @@ class GitTests(BaseTestCase):
|
||||||
# assert that commit message was read using git command
|
# assert that commit message was read using git command
|
||||||
sh.git.assert_called_once_with("log", "-1", "--pretty=%H", **self.expected_sh_special_args)
|
sh.git.assert_called_once_with("log", "-1", "--pretty=%H", **self.expected_sh_special_args)
|
||||||
|
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_git_no_commits_error(self, sh):
|
def test_git_no_commits_error(self, sh):
|
||||||
# No commits: returned by 'git log'
|
# No commits: returned by 'git log'
|
||||||
err = b"fatal: your current branch 'master' does not have any commits yet"
|
err = b"fatal: your current branch 'main' does not have any commits yet"
|
||||||
|
|
||||||
sh.git.side_effect = ErrorReturnCode("git log -1 --pretty=%H", b"", err)
|
sh.git.side_effect = ErrorReturnCode("git log -1 --pretty=%H", b"", err)
|
||||||
|
|
||||||
|
@ -64,25 +61,38 @@ class GitTests(BaseTestCase):
|
||||||
|
|
||||||
# assert that commit message was read using git command
|
# assert that commit message was read using git command
|
||||||
sh.git.assert_called_once_with("log", "-1", "--pretty=%H", **self.expected_sh_special_args)
|
sh.git.assert_called_once_with("log", "-1", "--pretty=%H", **self.expected_sh_special_args)
|
||||||
sh.git.reset_mock()
|
|
||||||
|
|
||||||
|
@patch("gitlint.git.sh")
|
||||||
|
def test_git_no_commits_get_branch(self, sh):
|
||||||
|
"""Check that we can still read the current branch name when there's no commits. This is useful when
|
||||||
|
when trying to lint the first commit using the --staged flag.
|
||||||
|
"""
|
||||||
# Unknown reference 'HEAD' commits: returned by 'git rev-parse'
|
# Unknown reference 'HEAD' commits: returned by 'git rev-parse'
|
||||||
err = (b"HEAD"
|
err = (
|
||||||
b"fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree."
|
b"HEAD"
|
||||||
b"Use '--' to separate paths from revisions, like this:"
|
b"fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree."
|
||||||
b"'git <command> [<revision>...] -- [<file>...]'")
|
b"Use '--' to separate paths from revisions, like this:"
|
||||||
|
b"'git <command> [<revision>...] -- [<file>...]'"
|
||||||
|
)
|
||||||
|
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
"#\n", # git config --get core.commentchar
|
"#\n", # git config --get core.commentchar
|
||||||
ErrorReturnCode("rev-parse --abbrev-ref HEAD", b"", err)
|
ErrorReturnCode("rev-parse --abbrev-ref HEAD", b"", err),
|
||||||
|
"test-branch", # git branch --show-current
|
||||||
]
|
]
|
||||||
|
|
||||||
with self.assertRaisesMessage(GitContextError, expected_msg):
|
context = GitContext.from_commit_msg("test")
|
||||||
context = GitContext.from_commit_msg("test")
|
self.assertEqual(context.current_branch, "test-branch")
|
||||||
context.current_branch
|
|
||||||
|
|
||||||
# assert that commit message was read using git command
|
# assert that we try using `git rev-parse` first, and if that fails (as will be the case with the first commit),
|
||||||
sh.git.assert_called_with("rev-parse", "--abbrev-ref", "HEAD", _tty_out=False, _cwd=None)
|
# we fallback to `git branch --show-current` to determine the current branch name.
|
||||||
|
expected_calls = [
|
||||||
|
call("config", "--get", "core.commentchar", _tty_out=False, _cwd=None, _ok_code=[0, 1]),
|
||||||
|
call("rev-parse", "--abbrev-ref", "HEAD", _tty_out=False, _cwd=None),
|
||||||
|
call("branch", "--show-current", _tty_out=False, _cwd=None),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(sh.git.mock_calls, expected_calls)
|
||||||
|
|
||||||
@patch("gitlint.git._git")
|
@patch("gitlint.git._git")
|
||||||
def test_git_commentchar(self, git):
|
def test_git_commentchar(self, git):
|
||||||
|
@ -93,11 +103,10 @@ class GitTests(BaseTestCase):
|
||||||
git.return_value = "ä"
|
git.return_value = "ä"
|
||||||
self.assertEqual(git_commentchar(), "ä")
|
self.assertEqual(git_commentchar(), "ä")
|
||||||
|
|
||||||
git.return_value = ';\n'
|
git.return_value = ";\n"
|
||||||
self.assertEqual(git_commentchar(os.path.join("/föo", "bar")), ';')
|
self.assertEqual(git_commentchar(os.path.join("/föo", "bar")), ";")
|
||||||
|
|
||||||
git.assert_called_with("config", "--get", "core.commentchar", _ok_code=[0, 1],
|
git.assert_called_with("config", "--get", "core.commentchar", _ok_code=[0, 1], _cwd=os.path.join("/föo", "bar"))
|
||||||
_cwd=os.path.join("/föo", "bar"))
|
|
||||||
|
|
||||||
@patch("gitlint.git._git")
|
@patch("gitlint.git._git")
|
||||||
def test_git_hooks_dir(self, git):
|
def test_git_hooks_dir(self, git):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import dateutil
|
import dateutil
|
||||||
|
|
||||||
|
@ -9,29 +9,33 @@ import arrow
|
||||||
from unittest.mock import patch, call
|
from unittest.mock import patch, call
|
||||||
|
|
||||||
from gitlint.tests.base import BaseTestCase
|
from gitlint.tests.base import BaseTestCase
|
||||||
from gitlint.git import GitContext, GitCommit, GitContextError, LocalGitCommit, StagedLocalGitCommit, GitCommitMessage
|
from gitlint.git import (
|
||||||
|
GitChangedFileStats,
|
||||||
|
GitContext,
|
||||||
|
GitCommit,
|
||||||
|
GitContextError,
|
||||||
|
LocalGitCommit,
|
||||||
|
StagedLocalGitCommit,
|
||||||
|
GitCommitMessage,
|
||||||
|
GitChangedFileStats,
|
||||||
|
)
|
||||||
from gitlint.shell import ErrorReturnCode
|
from gitlint.shell import ErrorReturnCode
|
||||||
|
|
||||||
|
|
||||||
class GitCommitTests(BaseTestCase):
|
class GitCommitTests(BaseTestCase):
|
||||||
|
|
||||||
# Expected special_args passed to 'sh'
|
# Expected special_args passed to 'sh'
|
||||||
expected_sh_special_args = {
|
expected_sh_special_args = {"_tty_out": False, "_cwd": "fåke/path"}
|
||||||
'_tty_out': False,
|
|
||||||
'_cwd': "fåke/path"
|
|
||||||
}
|
|
||||||
|
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_get_latest_commit(self, sh):
|
def test_get_latest_commit(self, sh):
|
||||||
sample_sha = "d8ac47e9f2923c7f22d8668e3a1ed04eb4cdbca9"
|
sample_sha = "d8ac47e9f2923c7f22d8668e3a1ed04eb4cdbca9"
|
||||||
|
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
sample_sha,
|
sample_sha,
|
||||||
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\ncömmit-title\n\ncömmit-body",
|
||||||
"cömmit-title\n\ncömmit-body",
|
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
"file1.txt\npåth/to/file2.txt\n",
|
"4\t15\tfile1.txt\n-\t-\tpåth/to/file2.bin\n",
|
||||||
"foöbar\n* hürdur\n"
|
"foöbar\n* hürdur\n",
|
||||||
]
|
]
|
||||||
|
|
||||||
context = GitContext.from_local_repository("fåke/path")
|
context = GitContext.from_local_repository("fåke/path")
|
||||||
|
@ -39,10 +43,17 @@ class GitCommitTests(BaseTestCase):
|
||||||
expected_calls = [
|
expected_calls = [
|
||||||
call("log", "-1", "--pretty=%H", **self.expected_sh_special_args),
|
call("log", "-1", "--pretty=%H", **self.expected_sh_special_args),
|
||||||
call("log", sample_sha, "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args),
|
call("log", sample_sha, "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args),
|
||||||
call('config', '--get', 'core.commentchar', _ok_code=[0, 1], **self.expected_sh_special_args),
|
call("config", "--get", "core.commentchar", _ok_code=[0, 1], **self.expected_sh_special_args),
|
||||||
call('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', sample_sha,
|
call(
|
||||||
**self.expected_sh_special_args),
|
"diff-tree",
|
||||||
call('branch', '--contains', sample_sha, **self.expected_sh_special_args)
|
"--no-commit-id",
|
||||||
|
"--numstat",
|
||||||
|
"-r",
|
||||||
|
"--root",
|
||||||
|
sample_sha,
|
||||||
|
**self.expected_sh_special_args,
|
||||||
|
),
|
||||||
|
call("branch", "--contains", sample_sha, **self.expected_sh_special_args),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Only first 'git log' call should've happened at this point
|
# Only first 'git log' call should've happened at this point
|
||||||
|
@ -55,18 +66,26 @@ class GitCommitTests(BaseTestCase):
|
||||||
self.assertEqual(last_commit.message.body, ["", "cömmit-body"])
|
self.assertEqual(last_commit.message.body, ["", "cömmit-body"])
|
||||||
self.assertEqual(last_commit.author_name, "test åuthor")
|
self.assertEqual(last_commit.author_name, "test åuthor")
|
||||||
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
||||||
self.assertEqual(last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15,
|
self.assertEqual(
|
||||||
tzinfo=dateutil.tz.tzoffset("+0100", 3600)))
|
last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15, tzinfo=dateutil.tz.tzoffset("+0100", 3600))
|
||||||
|
)
|
||||||
self.assertListEqual(last_commit.parents, ["åbc"])
|
self.assertListEqual(last_commit.parents, ["åbc"])
|
||||||
self.assertFalse(last_commit.is_merge_commit)
|
self.assertFalse(last_commit.is_merge_commit)
|
||||||
self.assertFalse(last_commit.is_fixup_commit)
|
self.assertFalse(last_commit.is_fixup_commit)
|
||||||
|
self.assertFalse(last_commit.is_fixup_amend_commit)
|
||||||
self.assertFalse(last_commit.is_squash_commit)
|
self.assertFalse(last_commit.is_squash_commit)
|
||||||
self.assertFalse(last_commit.is_revert_commit)
|
self.assertFalse(last_commit.is_revert_commit)
|
||||||
|
|
||||||
# First 2 'git log' calls should've happened at this point
|
# First 2 'git log' calls should've happened at this point
|
||||||
self.assertListEqual(sh.git.mock_calls, expected_calls[:3])
|
self.assertListEqual(sh.git.mock_calls, expected_calls[:3])
|
||||||
|
|
||||||
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.txt"])
|
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.bin"])
|
||||||
|
expected_file_stats = {
|
||||||
|
"file1.txt": GitChangedFileStats("file1.txt", 4, 15),
|
||||||
|
"påth/to/file2.bin": GitChangedFileStats("påth/to/file2.bin", None, None),
|
||||||
|
}
|
||||||
|
self.assertDictEqual(last_commit.changed_files_stats, expected_file_stats)
|
||||||
|
|
||||||
# 'git diff-tree' should have happened at this point
|
# 'git diff-tree' should have happened at this point
|
||||||
self.assertListEqual(sh.git.mock_calls, expected_calls[:4])
|
self.assertListEqual(sh.git.mock_calls, expected_calls[:4])
|
||||||
|
|
||||||
|
@ -74,18 +93,17 @@ class GitCommitTests(BaseTestCase):
|
||||||
# All expected calls should've happened at this point
|
# All expected calls should've happened at this point
|
||||||
self.assertListEqual(sh.git.mock_calls, expected_calls)
|
self.assertListEqual(sh.git.mock_calls, expected_calls)
|
||||||
|
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_from_local_repository_specific_refspec(self, sh):
|
def test_from_local_repository_specific_refspec(self, sh):
|
||||||
sample_refspec = "åbc123..def456"
|
sample_refspec = "åbc123..def456"
|
||||||
sample_sha = "åbc123"
|
sample_sha = "åbc123"
|
||||||
|
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
sample_sha, # git rev-list <sample_refspec>
|
sample_sha, # git rev-list <sample_refspec>
|
||||||
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\ncömmit-title\n\ncömmit-body",
|
||||||
"cömmit-title\n\ncömmit-body",
|
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
"file1.txt\npåth/to/file2.txt\n",
|
"7\t10\tfile1.txt\n9\t12\tpåth/to/file2.txt\n",
|
||||||
"foöbar\n* hürdur\n"
|
"foöbar\n* hürdur\n",
|
||||||
]
|
]
|
||||||
|
|
||||||
context = GitContext.from_local_repository("fåke/path", refspec=sample_refspec)
|
context = GitContext.from_local_repository("fåke/path", refspec=sample_refspec)
|
||||||
|
@ -93,10 +111,17 @@ class GitCommitTests(BaseTestCase):
|
||||||
expected_calls = [
|
expected_calls = [
|
||||||
call("rev-list", sample_refspec, **self.expected_sh_special_args),
|
call("rev-list", sample_refspec, **self.expected_sh_special_args),
|
||||||
call("log", sample_sha, "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args),
|
call("log", sample_sha, "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args),
|
||||||
call('config', '--get', 'core.commentchar', _ok_code=[0, 1], **self.expected_sh_special_args),
|
call("config", "--get", "core.commentchar", _ok_code=[0, 1], **self.expected_sh_special_args),
|
||||||
call('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', sample_sha,
|
call(
|
||||||
**self.expected_sh_special_args),
|
"diff-tree",
|
||||||
call('branch', '--contains', sample_sha, **self.expected_sh_special_args)
|
"--no-commit-id",
|
||||||
|
"--numstat",
|
||||||
|
"-r",
|
||||||
|
"--root",
|
||||||
|
sample_sha,
|
||||||
|
**self.expected_sh_special_args,
|
||||||
|
),
|
||||||
|
call("branch", "--contains", sample_sha, **self.expected_sh_special_args),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Only first 'git log' call should've happened at this point
|
# Only first 'git log' call should've happened at this point
|
||||||
|
@ -109,11 +134,13 @@ class GitCommitTests(BaseTestCase):
|
||||||
self.assertEqual(last_commit.message.body, ["", "cömmit-body"])
|
self.assertEqual(last_commit.message.body, ["", "cömmit-body"])
|
||||||
self.assertEqual(last_commit.author_name, "test åuthor")
|
self.assertEqual(last_commit.author_name, "test åuthor")
|
||||||
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
||||||
self.assertEqual(last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15,
|
self.assertEqual(
|
||||||
tzinfo=dateutil.tz.tzoffset("+0100", 3600)))
|
last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15, tzinfo=dateutil.tz.tzoffset("+0100", 3600))
|
||||||
|
)
|
||||||
self.assertListEqual(last_commit.parents, ["åbc"])
|
self.assertListEqual(last_commit.parents, ["åbc"])
|
||||||
self.assertFalse(last_commit.is_merge_commit)
|
self.assertFalse(last_commit.is_merge_commit)
|
||||||
self.assertFalse(last_commit.is_fixup_commit)
|
self.assertFalse(last_commit.is_fixup_commit)
|
||||||
|
self.assertFalse(last_commit.is_fixup_amend_commit)
|
||||||
self.assertFalse(last_commit.is_squash_commit)
|
self.assertFalse(last_commit.is_squash_commit)
|
||||||
self.assertFalse(last_commit.is_revert_commit)
|
self.assertFalse(last_commit.is_revert_commit)
|
||||||
|
|
||||||
|
@ -121,6 +148,12 @@ class GitCommitTests(BaseTestCase):
|
||||||
self.assertListEqual(sh.git.mock_calls, expected_calls[:3])
|
self.assertListEqual(sh.git.mock_calls, expected_calls[:3])
|
||||||
|
|
||||||
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.txt"])
|
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.txt"])
|
||||||
|
expected_file_stats = {
|
||||||
|
"file1.txt": GitChangedFileStats("file1.txt", 7, 10),
|
||||||
|
"påth/to/file2.txt": GitChangedFileStats("påth/to/file2.txt", 9, 12),
|
||||||
|
}
|
||||||
|
self.assertDictEqual(last_commit.changed_files_stats, expected_file_stats)
|
||||||
|
|
||||||
# 'git diff-tree' should have happened at this point
|
# 'git diff-tree' should have happened at this point
|
||||||
self.assertListEqual(sh.git.mock_calls, expected_calls[:4])
|
self.assertListEqual(sh.git.mock_calls, expected_calls[:4])
|
||||||
|
|
||||||
|
@ -128,28 +161,34 @@ class GitCommitTests(BaseTestCase):
|
||||||
# All expected calls should've happened at this point
|
# All expected calls should've happened at this point
|
||||||
self.assertListEqual(sh.git.mock_calls, expected_calls)
|
self.assertListEqual(sh.git.mock_calls, expected_calls)
|
||||||
|
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_from_local_repository_specific_commit_hash(self, sh):
|
def test_from_local_repository_specific_commit_hash(self, sh):
|
||||||
sample_hash = "åbc123"
|
sample_hash = "åbc123"
|
||||||
|
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
sample_hash, # git log -1 <sample_hash>
|
sample_hash, # git log -1 <sample_hash>
|
||||||
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\ncömmit-title\n\ncömmit-body",
|
||||||
"cömmit-title\n\ncömmit-body",
|
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
"file1.txt\npåth/to/file2.txt\n",
|
"8\t3\tfile1.txt\n1\t4\tpåth/to/file2.txt\n",
|
||||||
"foöbar\n* hürdur\n"
|
"foöbar\n* hürdur\n",
|
||||||
]
|
]
|
||||||
|
|
||||||
context = GitContext.from_local_repository("fåke/path", commit_hash=sample_hash)
|
context = GitContext.from_local_repository("fåke/path", commit_hashes=[sample_hash])
|
||||||
# assert that commit info was read using git command
|
# assert that commit info was read using git command
|
||||||
expected_calls = [
|
expected_calls = [
|
||||||
call("log", "-1", sample_hash, "--pretty=%H", **self.expected_sh_special_args),
|
call("log", "-1", sample_hash, "--pretty=%H", **self.expected_sh_special_args),
|
||||||
call("log", sample_hash, "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args),
|
call("log", sample_hash, "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args),
|
||||||
call('config', '--get', 'core.commentchar', _ok_code=[0, 1], **self.expected_sh_special_args),
|
call("config", "--get", "core.commentchar", _ok_code=[0, 1], **self.expected_sh_special_args),
|
||||||
call('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', sample_hash,
|
call(
|
||||||
**self.expected_sh_special_args),
|
"diff-tree",
|
||||||
call('branch', '--contains', sample_hash, **self.expected_sh_special_args)
|
"--no-commit-id",
|
||||||
|
"--numstat",
|
||||||
|
"-r",
|
||||||
|
"--root",
|
||||||
|
sample_hash,
|
||||||
|
**self.expected_sh_special_args,
|
||||||
|
),
|
||||||
|
call("branch", "--contains", sample_hash, **self.expected_sh_special_args),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Only first 'git log' call should've happened at this point
|
# Only first 'git log' call should've happened at this point
|
||||||
|
@ -162,11 +201,13 @@ class GitCommitTests(BaseTestCase):
|
||||||
self.assertEqual(last_commit.message.body, ["", "cömmit-body"])
|
self.assertEqual(last_commit.message.body, ["", "cömmit-body"])
|
||||||
self.assertEqual(last_commit.author_name, "test åuthor")
|
self.assertEqual(last_commit.author_name, "test åuthor")
|
||||||
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
||||||
self.assertEqual(last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15,
|
self.assertEqual(
|
||||||
tzinfo=dateutil.tz.tzoffset("+0100", 3600)))
|
last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15, tzinfo=dateutil.tz.tzoffset("+0100", 3600))
|
||||||
|
)
|
||||||
self.assertListEqual(last_commit.parents, ["åbc"])
|
self.assertListEqual(last_commit.parents, ["åbc"])
|
||||||
self.assertFalse(last_commit.is_merge_commit)
|
self.assertFalse(last_commit.is_merge_commit)
|
||||||
self.assertFalse(last_commit.is_fixup_commit)
|
self.assertFalse(last_commit.is_fixup_commit)
|
||||||
|
self.assertFalse(last_commit.is_fixup_amend_commit)
|
||||||
self.assertFalse(last_commit.is_squash_commit)
|
self.assertFalse(last_commit.is_squash_commit)
|
||||||
self.assertFalse(last_commit.is_revert_commit)
|
self.assertFalse(last_commit.is_revert_commit)
|
||||||
|
|
||||||
|
@ -174,6 +215,11 @@ class GitCommitTests(BaseTestCase):
|
||||||
self.assertListEqual(sh.git.mock_calls, expected_calls[:3])
|
self.assertListEqual(sh.git.mock_calls, expected_calls[:3])
|
||||||
|
|
||||||
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.txt"])
|
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.txt"])
|
||||||
|
expected_file_stats = {
|
||||||
|
"file1.txt": GitChangedFileStats("file1.txt", 8, 3),
|
||||||
|
"påth/to/file2.txt": GitChangedFileStats("påth/to/file2.txt", 1, 4),
|
||||||
|
}
|
||||||
|
self.assertDictEqual(last_commit.changed_files_stats, expected_file_stats)
|
||||||
# 'git diff-tree' should have happened at this point
|
# 'git diff-tree' should have happened at this point
|
||||||
self.assertListEqual(sh.git.mock_calls, expected_calls[:4])
|
self.assertListEqual(sh.git.mock_calls, expected_calls[:4])
|
||||||
|
|
||||||
|
@ -181,17 +227,103 @@ class GitCommitTests(BaseTestCase):
|
||||||
# All expected calls should've happened at this point
|
# All expected calls should've happened at this point
|
||||||
self.assertListEqual(sh.git.mock_calls, expected_calls)
|
self.assertListEqual(sh.git.mock_calls, expected_calls)
|
||||||
|
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
|
def test_from_local_repository_multiple_commit_hashes(self, sh):
|
||||||
|
hashes = ["åbc123", "dęf456", "ghí789"]
|
||||||
|
sh.git.side_effect = [
|
||||||
|
*hashes,
|
||||||
|
f"test åuthor {hashes[0]}\x00test-emåil-{hashes[0]}@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
||||||
|
f"cömmit-title {hashes[0]}\n\ncömmit-body {hashes[0]}",
|
||||||
|
"#", # git config --get core.commentchar
|
||||||
|
f"test åuthor {hashes[1]}\x00test-emåil-{hashes[1]}@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
||||||
|
f"cömmit-title {hashes[1]}\n\ncömmit-body {hashes[1]}",
|
||||||
|
f"test åuthor {hashes[2]}\x00test-emåil-{hashes[2]}@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
||||||
|
f"cömmit-title {hashes[2]}\n\ncömmit-body {hashes[2]}",
|
||||||
|
f"2\t5\tfile1-{hashes[0]}.txt\n7\t1\tpåth/to/file2.txt\n",
|
||||||
|
f"2\t5\tfile1-{hashes[1]}.txt\n7\t1\tpåth/to/file2.txt\n",
|
||||||
|
f"2\t5\tfile1-{hashes[2]}.txt\n7\t1\tpåth/to/file2.txt\n",
|
||||||
|
f"foöbar-{hashes[0]}\n* hürdur\n",
|
||||||
|
f"foöbar-{hashes[1]}\n* hürdur\n",
|
||||||
|
f"foöbar-{hashes[2]}\n* hürdur\n",
|
||||||
|
]
|
||||||
|
|
||||||
|
expected_calls = [
|
||||||
|
call("log", "-1", hashes[0], "--pretty=%H", **self.expected_sh_special_args),
|
||||||
|
call("log", "-1", hashes[1], "--pretty=%H", **self.expected_sh_special_args),
|
||||||
|
call("log", "-1", hashes[2], "--pretty=%H", **self.expected_sh_special_args),
|
||||||
|
call("log", hashes[0], "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args),
|
||||||
|
call("config", "--get", "core.commentchar", _ok_code=[0, 1], **self.expected_sh_special_args),
|
||||||
|
call("log", hashes[1], "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args),
|
||||||
|
call("log", hashes[2], "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args),
|
||||||
|
call(
|
||||||
|
"diff-tree", "--no-commit-id", "--numstat", "-r", "--root", hashes[0], **self.expected_sh_special_args
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
"diff-tree", "--no-commit-id", "--numstat", "-r", "--root", hashes[1], **self.expected_sh_special_args
|
||||||
|
),
|
||||||
|
call(
|
||||||
|
"diff-tree", "--no-commit-id", "--numstat", "-r", "--root", hashes[2], **self.expected_sh_special_args
|
||||||
|
),
|
||||||
|
call("branch", "--contains", hashes[0], **self.expected_sh_special_args),
|
||||||
|
call("branch", "--contains", hashes[1], **self.expected_sh_special_args),
|
||||||
|
call("branch", "--contains", hashes[2], **self.expected_sh_special_args),
|
||||||
|
]
|
||||||
|
|
||||||
|
context = GitContext.from_local_repository("fåke/path", commit_hashes=hashes)
|
||||||
|
|
||||||
|
# Only first set of 'git log' calls should've happened at this point
|
||||||
|
self.assertEqual(sh.git.mock_calls, expected_calls[:3])
|
||||||
|
|
||||||
|
for i, commit in enumerate(context.commits):
|
||||||
|
expected_hash = hashes[i]
|
||||||
|
self.assertIsInstance(commit, LocalGitCommit)
|
||||||
|
self.assertEqual(commit.sha, expected_hash)
|
||||||
|
self.assertEqual(commit.message.title, f"cömmit-title {expected_hash}")
|
||||||
|
self.assertEqual(commit.message.body, ["", f"cömmit-body {expected_hash}"])
|
||||||
|
self.assertEqual(commit.author_name, f"test åuthor {expected_hash}")
|
||||||
|
self.assertEqual(commit.author_email, f"test-emåil-{expected_hash}@foo.com")
|
||||||
|
self.assertEqual(
|
||||||
|
commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15, tzinfo=dateutil.tz.tzoffset("+0100", 3600))
|
||||||
|
)
|
||||||
|
self.assertListEqual(commit.parents, ["åbc"])
|
||||||
|
self.assertFalse(commit.is_merge_commit)
|
||||||
|
self.assertFalse(commit.is_fixup_commit)
|
||||||
|
self.assertFalse(commit.is_fixup_amend_commit)
|
||||||
|
self.assertFalse(commit.is_squash_commit)
|
||||||
|
self.assertFalse(commit.is_revert_commit)
|
||||||
|
|
||||||
|
# All 'git log' calls should've happened at this point
|
||||||
|
self.assertListEqual(sh.git.mock_calls, expected_calls[:7])
|
||||||
|
|
||||||
|
for i, commit in enumerate(context.commits):
|
||||||
|
expected_hash = hashes[i]
|
||||||
|
self.assertListEqual(commit.changed_files, [f"file1-{expected_hash}.txt", "påth/to/file2.txt"])
|
||||||
|
expected_file_stats = {
|
||||||
|
f"file1-{expected_hash}.txt": GitChangedFileStats(f"file1-{expected_hash}.txt", 2, 5),
|
||||||
|
"påth/to/file2.txt": GitChangedFileStats("påth/to/file2.txt", 7, 1),
|
||||||
|
}
|
||||||
|
self.assertDictEqual(commit.changed_files_stats, expected_file_stats)
|
||||||
|
|
||||||
|
# 'git diff-tree' should have happened at this point
|
||||||
|
self.assertListEqual(sh.git.mock_calls, expected_calls[:10])
|
||||||
|
|
||||||
|
for i, commit in enumerate(context.commits):
|
||||||
|
expected_hash = hashes[i]
|
||||||
|
self.assertListEqual(commit.branches, [f"foöbar-{expected_hash}", "hürdur"])
|
||||||
|
|
||||||
|
# All expected calls should've happened at this point
|
||||||
|
self.assertListEqual(sh.git.mock_calls, expected_calls)
|
||||||
|
|
||||||
|
@patch("gitlint.git.sh")
|
||||||
def test_get_latest_commit_merge_commit(self, sh):
|
def test_get_latest_commit_merge_commit(self, sh):
|
||||||
sample_sha = "d8ac47e9f2923c7f22d8668e3a1ed04eb4cdbca9"
|
sample_sha = "d8ac47e9f2923c7f22d8668e3a1ed04eb4cdbca9"
|
||||||
|
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
sample_sha,
|
sample_sha,
|
||||||
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc def\n"
|
'test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc def\nMerge "foo bår commit"',
|
||||||
"Merge \"foo bår commit\"",
|
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
"file1.txt\npåth/to/file2.txt\n",
|
"6\t2\tfile1.txt\n1\t4\tpåth/to/file2.txt\n",
|
||||||
"foöbar\n* hürdur\n"
|
"foöbar\n* hürdur\n",
|
||||||
]
|
]
|
||||||
|
|
||||||
context = GitContext.from_local_repository("fåke/path")
|
context = GitContext.from_local_repository("fåke/path")
|
||||||
|
@ -199,10 +331,17 @@ class GitCommitTests(BaseTestCase):
|
||||||
expected_calls = [
|
expected_calls = [
|
||||||
call("log", "-1", "--pretty=%H", **self.expected_sh_special_args),
|
call("log", "-1", "--pretty=%H", **self.expected_sh_special_args),
|
||||||
call("log", sample_sha, "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args),
|
call("log", sample_sha, "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args),
|
||||||
call('config', '--get', 'core.commentchar', _ok_code=[0, 1], **self.expected_sh_special_args),
|
call("config", "--get", "core.commentchar", _ok_code=[0, 1], **self.expected_sh_special_args),
|
||||||
call('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', sample_sha,
|
call(
|
||||||
**self.expected_sh_special_args),
|
"diff-tree",
|
||||||
call('branch', '--contains', sample_sha, **self.expected_sh_special_args)
|
"--no-commit-id",
|
||||||
|
"--numstat",
|
||||||
|
"-r",
|
||||||
|
"--root",
|
||||||
|
sample_sha,
|
||||||
|
**self.expected_sh_special_args,
|
||||||
|
),
|
||||||
|
call("branch", "--contains", sample_sha, **self.expected_sh_special_args),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Only first 'git log' call should've happened at this point
|
# Only first 'git log' call should've happened at this point
|
||||||
|
@ -211,15 +350,17 @@ class GitCommitTests(BaseTestCase):
|
||||||
last_commit = context.commits[-1]
|
last_commit = context.commits[-1]
|
||||||
self.assertIsInstance(last_commit, LocalGitCommit)
|
self.assertIsInstance(last_commit, LocalGitCommit)
|
||||||
self.assertEqual(last_commit.sha, sample_sha)
|
self.assertEqual(last_commit.sha, sample_sha)
|
||||||
self.assertEqual(last_commit.message.title, "Merge \"foo bår commit\"")
|
self.assertEqual(last_commit.message.title, 'Merge "foo bår commit"')
|
||||||
self.assertEqual(last_commit.message.body, [])
|
self.assertEqual(last_commit.message.body, [])
|
||||||
self.assertEqual(last_commit.author_name, "test åuthor")
|
self.assertEqual(last_commit.author_name, "test åuthor")
|
||||||
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
||||||
self.assertEqual(last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15,
|
self.assertEqual(
|
||||||
tzinfo=dateutil.tz.tzoffset("+0100", 3600)))
|
last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15, tzinfo=dateutil.tz.tzoffset("+0100", 3600))
|
||||||
|
)
|
||||||
self.assertListEqual(last_commit.parents, ["åbc", "def"])
|
self.assertListEqual(last_commit.parents, ["åbc", "def"])
|
||||||
self.assertTrue(last_commit.is_merge_commit)
|
self.assertTrue(last_commit.is_merge_commit)
|
||||||
self.assertFalse(last_commit.is_fixup_commit)
|
self.assertFalse(last_commit.is_fixup_commit)
|
||||||
|
self.assertFalse(last_commit.is_fixup_amend_commit)
|
||||||
self.assertFalse(last_commit.is_squash_commit)
|
self.assertFalse(last_commit.is_squash_commit)
|
||||||
self.assertFalse(last_commit.is_revert_commit)
|
self.assertFalse(last_commit.is_revert_commit)
|
||||||
|
|
||||||
|
@ -227,6 +368,11 @@ class GitCommitTests(BaseTestCase):
|
||||||
self.assertListEqual(sh.git.mock_calls, expected_calls[:3])
|
self.assertListEqual(sh.git.mock_calls, expected_calls[:3])
|
||||||
|
|
||||||
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.txt"])
|
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.txt"])
|
||||||
|
expected_file_stats = {
|
||||||
|
"file1.txt": GitChangedFileStats("file1.txt", 6, 2),
|
||||||
|
"påth/to/file2.txt": GitChangedFileStats("påth/to/file2.txt", 1, 4),
|
||||||
|
}
|
||||||
|
self.assertDictEqual(last_commit.changed_files_stats, expected_file_stats)
|
||||||
# 'git diff-tree' should have happened at this point
|
# 'git diff-tree' should have happened at this point
|
||||||
self.assertListEqual(sh.git.mock_calls, expected_calls[:4])
|
self.assertListEqual(sh.git.mock_calls, expected_calls[:4])
|
||||||
|
|
||||||
|
@ -234,19 +380,19 @@ class GitCommitTests(BaseTestCase):
|
||||||
# All expected calls should've happened at this point
|
# All expected calls should've happened at this point
|
||||||
self.assertListEqual(sh.git.mock_calls, expected_calls)
|
self.assertListEqual(sh.git.mock_calls, expected_calls)
|
||||||
|
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_get_latest_commit_fixup_squash_commit(self, sh):
|
def test_get_latest_commit_fixup_squash_commit(self, sh):
|
||||||
commit_types = ["fixup", "squash"]
|
commit_prefixes = {"fixup": "is_fixup_commit", "squash": "is_squash_commit", "amend": "is_fixup_amend_commit"}
|
||||||
for commit_type in commit_types:
|
for commit_type in commit_prefixes.keys():
|
||||||
sample_sha = "d8ac47e9f2923c7f22d8668e3a1ed04eb4cdbca9"
|
sample_sha = "d8ac47e9f2923c7f22d8668e3a1ed04eb4cdbca9"
|
||||||
|
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
sample_sha,
|
sample_sha,
|
||||||
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
||||||
f"{commit_type}! \"foo bår commit\"",
|
f'{commit_type}! "foo bår commit"',
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
"file1.txt\npåth/to/file2.txt\n",
|
"8\t2\tfile1.txt\n7\t3\tpåth/to/file2.txt\n",
|
||||||
"foöbar\n* hürdur\n"
|
"foöbar\n* hürdur\n",
|
||||||
]
|
]
|
||||||
|
|
||||||
context = GitContext.from_local_repository("fåke/path")
|
context = GitContext.from_local_repository("fåke/path")
|
||||||
|
@ -254,10 +400,17 @@ class GitCommitTests(BaseTestCase):
|
||||||
expected_calls = [
|
expected_calls = [
|
||||||
call("log", "-1", "--pretty=%H", **self.expected_sh_special_args),
|
call("log", "-1", "--pretty=%H", **self.expected_sh_special_args),
|
||||||
call("log", sample_sha, "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args),
|
call("log", sample_sha, "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args),
|
||||||
call('config', '--get', 'core.commentchar', _ok_code=[0, 1], **self.expected_sh_special_args),
|
call("config", "--get", "core.commentchar", _ok_code=[0, 1], **self.expected_sh_special_args),
|
||||||
call('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', sample_sha,
|
call(
|
||||||
**self.expected_sh_special_args),
|
"diff-tree",
|
||||||
call('branch', '--contains', sample_sha, **self.expected_sh_special_args)
|
"--no-commit-id",
|
||||||
|
"--numstat",
|
||||||
|
"-r",
|
||||||
|
"--root",
|
||||||
|
sample_sha,
|
||||||
|
**self.expected_sh_special_args,
|
||||||
|
),
|
||||||
|
call("branch", "--contains", sample_sha, **self.expected_sh_special_args),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Only first 'git log' call should've happened at this point
|
# Only first 'git log' call should've happened at this point
|
||||||
|
@ -266,27 +419,31 @@ class GitCommitTests(BaseTestCase):
|
||||||
last_commit = context.commits[-1]
|
last_commit = context.commits[-1]
|
||||||
self.assertIsInstance(last_commit, LocalGitCommit)
|
self.assertIsInstance(last_commit, LocalGitCommit)
|
||||||
self.assertEqual(last_commit.sha, sample_sha)
|
self.assertEqual(last_commit.sha, sample_sha)
|
||||||
self.assertEqual(last_commit.message.title, f"{commit_type}! \"foo bår commit\"")
|
self.assertEqual(last_commit.message.title, f'{commit_type}! "foo bår commit"')
|
||||||
self.assertEqual(last_commit.message.body, [])
|
self.assertEqual(last_commit.message.body, [])
|
||||||
self.assertEqual(last_commit.author_name, "test åuthor")
|
self.assertEqual(last_commit.author_name, "test åuthor")
|
||||||
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
||||||
self.assertEqual(last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15,
|
self.assertEqual(
|
||||||
tzinfo=dateutil.tz.tzoffset("+0100", 3600)))
|
last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15, tzinfo=dateutil.tz.tzoffset("+0100", 3600))
|
||||||
|
)
|
||||||
self.assertListEqual(last_commit.parents, ["åbc"])
|
self.assertListEqual(last_commit.parents, ["åbc"])
|
||||||
|
|
||||||
# First 2 'git log' calls should've happened at this point
|
# First 2 'git log' calls should've happened at this point
|
||||||
self.assertEqual(sh.git.mock_calls, expected_calls[:3])
|
self.assertEqual(sh.git.mock_calls, expected_calls[:3])
|
||||||
|
|
||||||
# Asserting that squash and fixup are correct
|
# Asserting that squash and fixup are correct
|
||||||
for type in commit_types:
|
for type, attr in commit_prefixes.items():
|
||||||
attr = "is_" + type + "_commit"
|
|
||||||
self.assertEqual(getattr(last_commit, attr), commit_type == type)
|
self.assertEqual(getattr(last_commit, attr), commit_type == type)
|
||||||
|
|
||||||
self.assertFalse(last_commit.is_merge_commit)
|
self.assertFalse(last_commit.is_merge_commit)
|
||||||
self.assertFalse(last_commit.is_revert_commit)
|
self.assertFalse(last_commit.is_revert_commit)
|
||||||
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.txt"])
|
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.txt"])
|
||||||
|
expected_file_stats = {
|
||||||
|
"file1.txt": GitChangedFileStats("file1.txt", 8, 2),
|
||||||
|
"påth/to/file2.txt": GitChangedFileStats("påth/to/file2.txt", 7, 3),
|
||||||
|
}
|
||||||
|
self.assertDictEqual(last_commit.changed_files_stats, expected_file_stats)
|
||||||
|
|
||||||
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.txt"])
|
|
||||||
# 'git diff-tree' should have happened at this point
|
# 'git diff-tree' should have happened at this point
|
||||||
self.assertListEqual(sh.git.mock_calls, expected_calls[:4])
|
self.assertListEqual(sh.git.mock_calls, expected_calls[:4])
|
||||||
|
|
||||||
|
@ -302,14 +459,16 @@ class GitCommitTests(BaseTestCase):
|
||||||
gitcontext = GitContext.from_commit_msg(self.get_sample("commit_message/sample1"))
|
gitcontext = GitContext.from_commit_msg(self.get_sample("commit_message/sample1"))
|
||||||
|
|
||||||
expected_title = "Commit title contåining 'WIP', as well as trailing punctuation."
|
expected_title = "Commit title contåining 'WIP', as well as trailing punctuation."
|
||||||
expected_body = ["This line should be empty",
|
expected_body = [
|
||||||
"This is the first line of the commit message body and it is meant to test a " +
|
"This line should be empty",
|
||||||
"line that exceeds the maximum line length of 80 characters.",
|
"This is the first line of the commit message body and it is meant to test a "
|
||||||
"This line has a tråiling space. ",
|
+ "line that exceeds the maximum line length of 80 characters.",
|
||||||
"This line has a trailing tab.\t"]
|
"This line has a tråiling space. ",
|
||||||
|
"This line has a trailing tab.\t",
|
||||||
|
]
|
||||||
expected_full = expected_title + "\n" + "\n".join(expected_body)
|
expected_full = expected_title + "\n" + "\n".join(expected_body)
|
||||||
expected_original = expected_full + (
|
expected_original = (
|
||||||
"\n# This is a cömmented line\n"
|
expected_full + "\n# This is a cömmented line\n"
|
||||||
"# ------------------------ >8 ------------------------\n"
|
"# ------------------------ >8 ------------------------\n"
|
||||||
"# Anything after this line should be cleaned up\n"
|
"# Anything after this line should be cleaned up\n"
|
||||||
"# this line appears on `git commit -v` command\n"
|
"# this line appears on `git commit -v` command\n"
|
||||||
|
@ -335,6 +494,7 @@ class GitCommitTests(BaseTestCase):
|
||||||
self.assertListEqual(commit.branches, [])
|
self.assertListEqual(commit.branches, [])
|
||||||
self.assertFalse(commit.is_merge_commit)
|
self.assertFalse(commit.is_merge_commit)
|
||||||
self.assertFalse(commit.is_fixup_commit)
|
self.assertFalse(commit.is_fixup_commit)
|
||||||
|
self.assertFalse(commit.is_fixup_amend_commit)
|
||||||
self.assertFalse(commit.is_squash_commit)
|
self.assertFalse(commit.is_squash_commit)
|
||||||
self.assertFalse(commit.is_revert_commit)
|
self.assertFalse(commit.is_revert_commit)
|
||||||
self.assertEqual(len(gitcontext.commits), 1)
|
self.assertEqual(len(gitcontext.commits), 1)
|
||||||
|
@ -355,6 +515,7 @@ class GitCommitTests(BaseTestCase):
|
||||||
self.assertListEqual(commit.branches, [])
|
self.assertListEqual(commit.branches, [])
|
||||||
self.assertFalse(commit.is_merge_commit)
|
self.assertFalse(commit.is_merge_commit)
|
||||||
self.assertFalse(commit.is_fixup_commit)
|
self.assertFalse(commit.is_fixup_commit)
|
||||||
|
self.assertFalse(commit.is_fixup_amend_commit)
|
||||||
self.assertFalse(commit.is_squash_commit)
|
self.assertFalse(commit.is_squash_commit)
|
||||||
self.assertFalse(commit.is_revert_commit)
|
self.assertFalse(commit.is_revert_commit)
|
||||||
self.assertEqual(len(gitcontext.commits), 1)
|
self.assertEqual(len(gitcontext.commits), 1)
|
||||||
|
@ -376,6 +537,7 @@ class GitCommitTests(BaseTestCase):
|
||||||
self.assertListEqual(commit.branches, [])
|
self.assertListEqual(commit.branches, [])
|
||||||
self.assertFalse(commit.is_merge_commit)
|
self.assertFalse(commit.is_merge_commit)
|
||||||
self.assertFalse(commit.is_fixup_commit)
|
self.assertFalse(commit.is_fixup_commit)
|
||||||
|
self.assertFalse(commit.is_fixup_amend_commit)
|
||||||
self.assertFalse(commit.is_squash_commit)
|
self.assertFalse(commit.is_squash_commit)
|
||||||
self.assertFalse(commit.is_revert_commit)
|
self.assertFalse(commit.is_revert_commit)
|
||||||
self.assertEqual(len(gitcontext.commits), 1)
|
self.assertEqual(len(gitcontext.commits), 1)
|
||||||
|
@ -400,6 +562,7 @@ class GitCommitTests(BaseTestCase):
|
||||||
self.assertFalse(commit.is_merge_commit)
|
self.assertFalse(commit.is_merge_commit)
|
||||||
self.assertFalse(commit.is_fixup_commit)
|
self.assertFalse(commit.is_fixup_commit)
|
||||||
self.assertFalse(commit.is_squash_commit)
|
self.assertFalse(commit.is_squash_commit)
|
||||||
|
self.assertFalse(commit.is_fixup_amend_commit)
|
||||||
self.assertFalse(commit.is_revert_commit)
|
self.assertFalse(commit.is_revert_commit)
|
||||||
self.assertEqual(len(gitcontext.commits), 1)
|
self.assertEqual(len(gitcontext.commits), 1)
|
||||||
|
|
||||||
|
@ -421,18 +584,19 @@ class GitCommitTests(BaseTestCase):
|
||||||
self.assertListEqual(commit.branches, [])
|
self.assertListEqual(commit.branches, [])
|
||||||
self.assertTrue(commit.is_merge_commit)
|
self.assertTrue(commit.is_merge_commit)
|
||||||
self.assertFalse(commit.is_fixup_commit)
|
self.assertFalse(commit.is_fixup_commit)
|
||||||
|
self.assertFalse(commit.is_fixup_amend_commit)
|
||||||
self.assertFalse(commit.is_squash_commit)
|
self.assertFalse(commit.is_squash_commit)
|
||||||
self.assertFalse(commit.is_revert_commit)
|
self.assertFalse(commit.is_revert_commit)
|
||||||
self.assertEqual(len(gitcontext.commits), 1)
|
self.assertEqual(len(gitcontext.commits), 1)
|
||||||
|
|
||||||
def test_from_commit_msg_revert_commit(self):
|
def test_from_commit_msg_revert_commit(self):
|
||||||
commit_msg = "Revert \"Prev commit message\"\n\nThis reverts commit a8ad67e04164a537198dea94a4fde81c5592ae9c."
|
commit_msg = 'Revert "Prev commit message"\n\nThis reverts commit a8ad67e04164a537198dea94a4fde81c5592ae9c.'
|
||||||
gitcontext = GitContext.from_commit_msg(commit_msg)
|
gitcontext = GitContext.from_commit_msg(commit_msg)
|
||||||
commit = gitcontext.commits[-1]
|
commit = gitcontext.commits[-1]
|
||||||
|
|
||||||
self.assertIsInstance(commit, GitCommit)
|
self.assertIsInstance(commit, GitCommit)
|
||||||
self.assertFalse(isinstance(commit, LocalGitCommit))
|
self.assertFalse(isinstance(commit, LocalGitCommit))
|
||||||
self.assertEqual(commit.message.title, "Revert \"Prev commit message\"")
|
self.assertEqual(commit.message.title, 'Revert "Prev commit message"')
|
||||||
self.assertEqual(commit.message.body, ["", "This reverts commit a8ad67e04164a537198dea94a4fde81c5592ae9c."])
|
self.assertEqual(commit.message.body, ["", "This reverts commit a8ad67e04164a537198dea94a4fde81c5592ae9c."])
|
||||||
self.assertEqual(commit.message.full, commit_msg)
|
self.assertEqual(commit.message.full, commit_msg)
|
||||||
self.assertEqual(commit.message.original, commit_msg)
|
self.assertEqual(commit.message.original, commit_msg)
|
||||||
|
@ -443,13 +607,16 @@ class GitCommitTests(BaseTestCase):
|
||||||
self.assertListEqual(commit.branches, [])
|
self.assertListEqual(commit.branches, [])
|
||||||
self.assertFalse(commit.is_merge_commit)
|
self.assertFalse(commit.is_merge_commit)
|
||||||
self.assertFalse(commit.is_fixup_commit)
|
self.assertFalse(commit.is_fixup_commit)
|
||||||
|
self.assertFalse(commit.is_fixup_amend_commit)
|
||||||
self.assertFalse(commit.is_squash_commit)
|
self.assertFalse(commit.is_squash_commit)
|
||||||
self.assertTrue(commit.is_revert_commit)
|
self.assertTrue(commit.is_revert_commit)
|
||||||
self.assertEqual(len(gitcontext.commits), 1)
|
self.assertEqual(len(gitcontext.commits), 1)
|
||||||
|
|
||||||
def test_from_commit_msg_fixup_squash_commit(self):
|
def test_from_commit_msg_fixup_squash_amend_commit(self):
|
||||||
commit_types = ["fixup", "squash"]
|
# mapping between cleanup commit prefixes and the commit object attribute
|
||||||
for commit_type in commit_types:
|
commit_prefixes = {"fixup": "is_fixup_commit", "squash": "is_squash_commit", "amend": "is_fixup_amend_commit"}
|
||||||
|
|
||||||
|
for commit_type in commit_prefixes.keys():
|
||||||
commit_msg = f"{commit_type}! Test message"
|
commit_msg = f"{commit_type}! Test message"
|
||||||
gitcontext = GitContext.from_commit_msg(commit_msg)
|
gitcontext = GitContext.from_commit_msg(commit_msg)
|
||||||
commit = gitcontext.commits[-1]
|
commit = gitcontext.commits[-1]
|
||||||
|
@ -469,34 +636,33 @@ class GitCommitTests(BaseTestCase):
|
||||||
self.assertFalse(commit.is_merge_commit)
|
self.assertFalse(commit.is_merge_commit)
|
||||||
self.assertFalse(commit.is_revert_commit)
|
self.assertFalse(commit.is_revert_commit)
|
||||||
# Asserting that squash and fixup are correct
|
# Asserting that squash and fixup are correct
|
||||||
for type in commit_types:
|
for type, commit_attr_name in commit_prefixes.items():
|
||||||
attr = "is_" + type + "_commit"
|
self.assertEqual(getattr(commit, commit_attr_name), commit_type == type)
|
||||||
self.assertEqual(getattr(commit, attr), commit_type == type)
|
|
||||||
|
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
@patch('arrow.now')
|
@patch("arrow.now")
|
||||||
def test_staged_commit(self, now, sh):
|
def test_staged_commit(self, now, sh):
|
||||||
# StagedLocalGitCommit()
|
# StagedLocalGitCommit()
|
||||||
|
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
"test åuthor\n", # git config --get user.name
|
"test åuthor\n", # git config --get user.name
|
||||||
"test-emåil@foo.com\n", # git config --get user.email
|
"test-emåil@foo.com\n", # git config --get user.email
|
||||||
"my-brånch\n", # git rev-parse --abbrev-ref HEAD
|
"my-brånch\n", # git rev-parse --abbrev-ref HEAD
|
||||||
"file1.txt\npåth/to/file2.txt\n",
|
"4\t2\tfile1.txt\n13\t9\tpåth/to/file2.txt\n",
|
||||||
]
|
]
|
||||||
now.side_effect = [arrow.get("2020-02-19T12:18:46.675182+01:00")]
|
now.side_effect = [arrow.get("2020-02-19T12:18:46.675182+01:00")]
|
||||||
|
|
||||||
# We use a fixup commit, just to test a non-default path
|
# We use a fixup commit, just to test a non-default path
|
||||||
context = GitContext.from_staged_commit("fixup! Foōbar 123\n\ncömmit-body\n", "fåke/path")
|
context = GitContext.from_staged_commit("fixup! Foōbar 123\n\ncömmit-body\n", "fåke/path")
|
||||||
|
|
||||||
# git calls we're expexting
|
# git calls we're expecting
|
||||||
expected_calls = [
|
expected_calls = [
|
||||||
call('config', '--get', 'core.commentchar', _ok_code=[0, 1], **self.expected_sh_special_args),
|
call("config", "--get", "core.commentchar", _ok_code=[0, 1], **self.expected_sh_special_args),
|
||||||
call('config', '--get', 'user.name', **self.expected_sh_special_args),
|
call("config", "--get", "user.name", **self.expected_sh_special_args),
|
||||||
call('config', '--get', 'user.email', **self.expected_sh_special_args),
|
call("config", "--get", "user.email", **self.expected_sh_special_args),
|
||||||
call("rev-parse", "--abbrev-ref", "HEAD", **self.expected_sh_special_args),
|
call("rev-parse", "--abbrev-ref", "HEAD", **self.expected_sh_special_args),
|
||||||
call("diff", "--staged", "--name-only", "-r", **self.expected_sh_special_args)
|
call("diff", "--staged", "--numstat", "-r", **self.expected_sh_special_args),
|
||||||
]
|
]
|
||||||
|
|
||||||
last_commit = context.commits[-1]
|
last_commit = context.commits[-1]
|
||||||
|
@ -513,13 +679,15 @@ class GitCommitTests(BaseTestCase):
|
||||||
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
||||||
self.assertListEqual(sh.git.mock_calls, expected_calls[0:3])
|
self.assertListEqual(sh.git.mock_calls, expected_calls[0:3])
|
||||||
|
|
||||||
self.assertEqual(last_commit.date, datetime.datetime(2020, 2, 19, 12, 18, 46,
|
self.assertEqual(
|
||||||
tzinfo=dateutil.tz.tzoffset("+0100", 3600)))
|
last_commit.date, datetime.datetime(2020, 2, 19, 12, 18, 46, tzinfo=dateutil.tz.tzoffset("+0100", 3600))
|
||||||
|
)
|
||||||
now.assert_called_once()
|
now.assert_called_once()
|
||||||
|
|
||||||
self.assertListEqual(last_commit.parents, [])
|
self.assertListEqual(last_commit.parents, [])
|
||||||
self.assertFalse(last_commit.is_merge_commit)
|
self.assertFalse(last_commit.is_merge_commit)
|
||||||
self.assertTrue(last_commit.is_fixup_commit)
|
self.assertTrue(last_commit.is_fixup_commit)
|
||||||
|
self.assertFalse(last_commit.is_fixup_amend_commit)
|
||||||
self.assertFalse(last_commit.is_squash_commit)
|
self.assertFalse(last_commit.is_squash_commit)
|
||||||
self.assertFalse(last_commit.is_revert_commit)
|
self.assertFalse(last_commit.is_revert_commit)
|
||||||
|
|
||||||
|
@ -527,42 +695,48 @@ class GitCommitTests(BaseTestCase):
|
||||||
self.assertListEqual(sh.git.mock_calls, expected_calls[0:4])
|
self.assertListEqual(sh.git.mock_calls, expected_calls[0:4])
|
||||||
|
|
||||||
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.txt"])
|
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.txt"])
|
||||||
|
expected_file_stats = {
|
||||||
|
"file1.txt": GitChangedFileStats("file1.txt", 4, 2),
|
||||||
|
"påth/to/file2.txt": GitChangedFileStats("påth/to/file2.txt", 13, 9),
|
||||||
|
}
|
||||||
|
self.assertDictEqual(last_commit.changed_files_stats, expected_file_stats)
|
||||||
|
|
||||||
self.assertListEqual(sh.git.mock_calls, expected_calls[0:5])
|
self.assertListEqual(sh.git.mock_calls, expected_calls[0:5])
|
||||||
|
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_staged_commit_with_missing_username(self, sh):
|
def test_staged_commit_with_missing_username(self, sh):
|
||||||
# StagedLocalGitCommit()
|
|
||||||
|
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
ErrorReturnCode('git config --get user.name', b"", b""),
|
ErrorReturnCode("git config --get user.name", b"", b""),
|
||||||
]
|
]
|
||||||
|
|
||||||
expected_msg = "Missing git configuration: please set user.name"
|
expected_msg = "Missing git configuration: please set user.name"
|
||||||
with self.assertRaisesMessage(GitContextError, expected_msg):
|
with self.assertRaisesMessage(GitContextError, expected_msg):
|
||||||
ctx = GitContext.from_staged_commit("Foōbar 123\n\ncömmit-body\n", "fåke/path")
|
ctx = GitContext.from_staged_commit("Foōbar 123\n\ncömmit-body\n", "fåke/path")
|
||||||
[str(commit) for commit in ctx.commits]
|
ctx.commits[0].author_name # accessing this attribute should raise an exception
|
||||||
|
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_staged_commit_with_missing_email(self, sh):
|
def test_staged_commit_with_missing_email(self, sh):
|
||||||
# StagedLocalGitCommit()
|
|
||||||
|
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
"#", # git config --get core.commentchar
|
"#", # git config --get core.commentchar
|
||||||
"test åuthor\n", # git config --get user.name
|
ErrorReturnCode("git config --get user.email", b"", b""),
|
||||||
ErrorReturnCode('git config --get user.name', b"", b""),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
expected_msg = "Missing git configuration: please set user.email"
|
expected_msg = "Missing git configuration: please set user.email"
|
||||||
with self.assertRaisesMessage(GitContextError, expected_msg):
|
with self.assertRaisesMessage(GitContextError, expected_msg):
|
||||||
ctx = GitContext.from_staged_commit("Foōbar 123\n\ncömmit-body\n", "fåke/path")
|
ctx = GitContext.from_staged_commit("Foōbar 123\n\ncömmit-body\n", "fåke/path")
|
||||||
[str(commit) for commit in ctx.commits]
|
ctx.commits[0].author_email # accessing this attribute should raise an exception
|
||||||
|
|
||||||
def test_gitcommitmessage_equality(self):
|
def test_gitcommitmessage_equality(self):
|
||||||
commit_message1 = GitCommitMessage(GitContext(), "tëst\n\nfoo", "tëst\n\nfoo", "tēst", ["", "föo"])
|
commit_message1 = GitCommitMessage(GitContext(), "tëst\n\nfoo", "tëst\n\nfoo", "tēst", ["", "föo"])
|
||||||
attrs = ['original', 'full', 'title', 'body']
|
attrs = ["original", "full", "title", "body"]
|
||||||
self.object_equality_test(commit_message1, attrs, {"context": commit_message1.context})
|
self.object_equality_test(commit_message1, attrs, {"context": commit_message1.context})
|
||||||
|
|
||||||
|
def test_gitchangedfilestats_equality(self):
|
||||||
|
changed_file_stats = GitChangedFileStats(Path("foö/bar"), 5, 13)
|
||||||
|
attrs = ["filepath", "additions", "deletions"]
|
||||||
|
self.object_equality_test(changed_file_stats, attrs)
|
||||||
|
|
||||||
@patch("gitlint.git._git")
|
@patch("gitlint.git._git")
|
||||||
def test_gitcommit_equality(self, git):
|
def test_gitcommit_equality(self, git):
|
||||||
# git will be called to setup the context (commentchar and current_branch), just return the same value
|
# git will be called to setup the context (commentchar and current_branch), just return the same value
|
||||||
|
@ -573,14 +747,32 @@ class GitCommitTests(BaseTestCase):
|
||||||
now = datetime.datetime.utcnow()
|
now = datetime.datetime.utcnow()
|
||||||
context1 = GitContext()
|
context1 = GitContext()
|
||||||
commit_message1 = GitCommitMessage(context1, "tëst\n\nfoo", "tëst\n\nfoo", "tēst", ["", "föo"])
|
commit_message1 = GitCommitMessage(context1, "tëst\n\nfoo", "tëst\n\nfoo", "tēst", ["", "föo"])
|
||||||
commit1 = GitCommit(context1, commit_message1, "shä", now, "Jöhn Smith", "jöhn.smith@test.com", None,
|
commit1 = GitCommit(
|
||||||
["föo/bar"], ["brånch1", "brånch2"])
|
context1,
|
||||||
|
commit_message1,
|
||||||
|
"shä",
|
||||||
|
now,
|
||||||
|
"Jöhn Smith",
|
||||||
|
"jöhn.smith@test.com",
|
||||||
|
None,
|
||||||
|
{"föo/bar": GitChangedFileStats("föo/bar", 5, 13)},
|
||||||
|
["brånch1", "brånch2"],
|
||||||
|
)
|
||||||
context1.commits = [commit1]
|
context1.commits = [commit1]
|
||||||
|
|
||||||
context2 = GitContext()
|
context2 = GitContext()
|
||||||
commit_message2 = GitCommitMessage(context2, "tëst\n\nfoo", "tëst\n\nfoo", "tēst", ["", "föo"])
|
commit_message2 = GitCommitMessage(context2, "tëst\n\nfoo", "tëst\n\nfoo", "tēst", ["", "föo"])
|
||||||
commit2 = GitCommit(context2, commit_message1, "shä", now, "Jöhn Smith", "jöhn.smith@test.com", None,
|
commit2 = GitCommit(
|
||||||
["föo/bar"], ["brånch1", "brånch2"])
|
context2,
|
||||||
|
commit_message1,
|
||||||
|
"shä",
|
||||||
|
now,
|
||||||
|
"Jöhn Smith",
|
||||||
|
"jöhn.smith@test.com",
|
||||||
|
None,
|
||||||
|
{"föo/bar": GitChangedFileStats("föo/bar", 5, 13)},
|
||||||
|
["brånch1", "brånch2"],
|
||||||
|
)
|
||||||
context2.commits = [commit2]
|
context2.commits = [commit2]
|
||||||
|
|
||||||
self.assertEqual(context1, context2)
|
self.assertEqual(context1, context2)
|
||||||
|
@ -588,15 +780,29 @@ class GitCommitTests(BaseTestCase):
|
||||||
self.assertEqual(commit1, commit2)
|
self.assertEqual(commit1, commit2)
|
||||||
|
|
||||||
# Check that objects are unequal when changing a single attribute
|
# Check that objects are unequal when changing a single attribute
|
||||||
kwargs = {'message': commit1.message, 'sha': commit1.sha, 'date': commit1.date,
|
kwargs = {
|
||||||
'author_name': commit1.author_name, 'author_email': commit1.author_email, 'parents': commit1.parents,
|
"message": commit1.message,
|
||||||
'changed_files': commit1.changed_files, 'branches': commit1.branches}
|
"sha": commit1.sha,
|
||||||
|
"date": commit1.date,
|
||||||
|
"author_name": commit1.author_name,
|
||||||
|
"author_email": commit1.author_email,
|
||||||
|
"parents": commit1.parents,
|
||||||
|
"branches": commit1.branches,
|
||||||
|
}
|
||||||
|
|
||||||
self.object_equality_test(commit1, kwargs.keys(), {"context": commit1.context})
|
self.object_equality_test(
|
||||||
|
commit1,
|
||||||
|
kwargs.keys(),
|
||||||
|
{"context": commit1.context, "changed_files_stats": {"föo/bar": GitChangedFileStats("föo/bar", 5, 13)}},
|
||||||
|
)
|
||||||
|
|
||||||
# Check that the is_* attributes that are affected by the commit message affect equality
|
# Check that the is_* attributes that are affected by the commit message affect equality
|
||||||
special_messages = {'is_merge_commit': "Merge: foöbar", 'is_fixup_commit': "fixup! foöbar",
|
special_messages = {
|
||||||
'is_squash_commit': "squash! foöbar", 'is_revert_commit': "Revert: foöbar"}
|
"is_merge_commit": "Merge: foöbar",
|
||||||
|
"is_fixup_commit": "fixup! foöbar",
|
||||||
|
"is_squash_commit": "squash! foöbar",
|
||||||
|
"is_revert_commit": "Revert: foöbar",
|
||||||
|
}
|
||||||
for key in special_messages:
|
for key in special_messages:
|
||||||
kwargs_copy = copy.deepcopy(kwargs)
|
kwargs_copy = copy.deepcopy(kwargs)
|
||||||
clone1 = GitCommit(context=commit1.context, **kwargs_copy)
|
clone1 = GitCommit(context=commit1.context, **kwargs_copy)
|
||||||
|
@ -607,6 +813,10 @@ class GitCommitTests(BaseTestCase):
|
||||||
clone2.message = GitCommitMessage.from_full_message(context1, "foöbar")
|
clone2.message = GitCommitMessage.from_full_message(context1, "foöbar")
|
||||||
self.assertNotEqual(clone1, clone2)
|
self.assertNotEqual(clone1, clone2)
|
||||||
|
|
||||||
|
# Check changed_files and changed_files_stats
|
||||||
|
commit2.changed_files_stats = {"föo/bar2": GitChangedFileStats("föo/bar2", 5, 13)}
|
||||||
|
self.assertNotEqual(commit1, commit2)
|
||||||
|
|
||||||
@patch("gitlint.git.git_commentchar")
|
@patch("gitlint.git.git_commentchar")
|
||||||
def test_commit_msg_custom_commentchar(self, patched):
|
def test_commit_msg_custom_commentchar(self, patched):
|
||||||
patched.return_value = "ä"
|
patched.return_value = "ä"
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from unittest.mock import patch, call
|
from unittest.mock import patch, call
|
||||||
|
|
||||||
from gitlint.tests.base import BaseTestCase
|
from gitlint.tests.base import BaseTestCase
|
||||||
|
@ -7,24 +5,16 @@ from gitlint.git import GitContext
|
||||||
|
|
||||||
|
|
||||||
class GitContextTests(BaseTestCase):
|
class GitContextTests(BaseTestCase):
|
||||||
|
|
||||||
# Expected special_args passed to 'sh'
|
# Expected special_args passed to 'sh'
|
||||||
expected_sh_special_args = {
|
expected_sh_special_args = {"_tty_out": False, "_cwd": "fåke/path"}
|
||||||
'_tty_out': False,
|
|
||||||
'_cwd': "fåke/path"
|
|
||||||
}
|
|
||||||
|
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_gitcontext(self, sh):
|
def test_gitcontext(self, sh):
|
||||||
|
sh.git.side_effect = ["#", "\nfoöbar\n"] # git config --get core.commentchar
|
||||||
sh.git.side_effect = [
|
|
||||||
"#", # git config --get core.commentchar
|
|
||||||
"\nfoöbar\n"
|
|
||||||
]
|
|
||||||
|
|
||||||
expected_calls = [
|
expected_calls = [
|
||||||
call("config", "--get", "core.commentchar", _ok_code=[0, 1], **self.expected_sh_special_args),
|
call("config", "--get", "core.commentchar", _ok_code=[0, 1], **self.expected_sh_special_args),
|
||||||
call("rev-parse", "--abbrev-ref", "HEAD", **self.expected_sh_special_args)
|
call("rev-parse", "--abbrev-ref", "HEAD", **self.expected_sh_special_args),
|
||||||
]
|
]
|
||||||
|
|
||||||
context = GitContext("fåke/path")
|
context = GitContext("fåke/path")
|
||||||
|
@ -38,12 +28,11 @@ class GitContextTests(BaseTestCase):
|
||||||
self.assertEqual(context.current_branch, "foöbar")
|
self.assertEqual(context.current_branch, "foöbar")
|
||||||
self.assertEqual(sh.git.mock_calls, expected_calls)
|
self.assertEqual(sh.git.mock_calls, expected_calls)
|
||||||
|
|
||||||
@patch('gitlint.git.sh')
|
@patch("gitlint.git.sh")
|
||||||
def test_gitcontext_equality(self, sh):
|
def test_gitcontext_equality(self, sh):
|
||||||
|
|
||||||
sh.git.side_effect = [
|
sh.git.side_effect = [
|
||||||
"û\n", # context1: git config --get core.commentchar
|
"û\n", # context1: git config --get core.commentchar
|
||||||
"û\n", # context2: git config --get core.commentchar
|
"û\n", # context2: git config --get core.commentchar
|
||||||
"my-brånch\n", # context1: git rev-parse --abbrev-ref HEAD
|
"my-brånch\n", # context1: git rev-parse --abbrev-ref HEAD
|
||||||
"my-brånch\n", # context2: git rev-parse --abbrev-ref HEAD
|
"my-brånch\n", # context2: git rev-parse --abbrev-ref HEAD
|
||||||
]
|
]
|
||||||
|
@ -68,17 +57,17 @@ class GitContextTests(BaseTestCase):
|
||||||
# Different comment_char
|
# Different comment_char
|
||||||
context3 = GitContext("fåke/path")
|
context3 = GitContext("fåke/path")
|
||||||
context3.commits = ["fōo", "bår"]
|
context3.commits = ["fōo", "bår"]
|
||||||
sh.git.side_effect = ([
|
sh.git.side_effect = [
|
||||||
"ç\n", # context3: git config --get core.commentchar
|
"ç\n", # context3: git config --get core.commentchar
|
||||||
"my-brånch\n" # context3: git rev-parse --abbrev-ref HEAD
|
"my-brånch\n", # context3: git rev-parse --abbrev-ref HEAD
|
||||||
])
|
]
|
||||||
self.assertNotEqual(context1, context3)
|
self.assertNotEqual(context1, context3)
|
||||||
|
|
||||||
# Different current_branch
|
# Different current_branch
|
||||||
context4 = GitContext("fåke/path")
|
context4 = GitContext("fåke/path")
|
||||||
context4.commits = ["fōo", "bår"]
|
context4.commits = ["fōo", "bår"]
|
||||||
sh.git.side_effect = ([
|
sh.git.side_effect = [
|
||||||
"û\n", # context4: git config --get core.commentchar
|
"û\n", # context4: git config --get core.commentchar
|
||||||
"different-brånch\n" # context4: git rev-parse --abbrev-ref HEAD
|
"different-brånch\n", # context4: git rev-parse --abbrev-ref HEAD
|
||||||
])
|
]
|
||||||
self.assertNotEqual(context1, context4)
|
self.assertNotEqual(context1, context4)
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from gitlint.tests.base import BaseTestCase
|
from gitlint.tests.base import BaseTestCase
|
||||||
from gitlint import rules
|
from gitlint import rules
|
||||||
|
|
||||||
|
@ -17,7 +16,7 @@ class BodyRuleTests(BaseTestCase):
|
||||||
self.assertListEqual(violations, [expected_violation])
|
self.assertListEqual(violations, [expected_violation])
|
||||||
|
|
||||||
# set line length to 120, and check no violation on length 73
|
# set line length to 120, and check no violation on length 73
|
||||||
rule = rules.BodyMaxLineLength({'line-length': 120})
|
rule = rules.BodyMaxLineLength({"line-length": 120})
|
||||||
violations = rule.validate("å" * 73, None)
|
violations = rule.validate("å" * 73, None)
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
|
||||||
|
@ -100,14 +99,14 @@ class BodyRuleTests(BaseTestCase):
|
||||||
# set line length to 120, and check violation on length 21
|
# set line length to 120, and check violation on length 21
|
||||||
expected_violation = rules.RuleViolation("B5", "Body message is too short (21<120)", "å" * 21, 3)
|
expected_violation = rules.RuleViolation("B5", "Body message is too short (21<120)", "å" * 21, 3)
|
||||||
|
|
||||||
rule = rules.BodyMinLength({'min-length': 120})
|
rule = rules.BodyMinLength({"min-length": 120})
|
||||||
commit = self.gitcommit("Title\n\n{0}\n".format("å" * 21)) # pylint: disable=consider-using-f-string
|
commit = self.gitcommit("Title\n\n{}\n".format("å" * 21)) # pylint: disable=consider-using-f-string
|
||||||
violations = rule.validate(commit)
|
violations = rule.validate(commit)
|
||||||
self.assertListEqual(violations, [expected_violation])
|
self.assertListEqual(violations, [expected_violation])
|
||||||
|
|
||||||
# Make sure we don't get the error if the body-length is exactly the min-length
|
# Make sure we don't get the error if the body-length is exactly the min-length
|
||||||
rule = rules.BodyMinLength({'min-length': 8})
|
rule = rules.BodyMinLength({"min-length": 8})
|
||||||
commit = self.gitcommit("Tïtle\n\n{0}\n".format("å" * 8)) # pylint: disable=consider-using-f-string
|
commit = self.gitcommit("Tïtle\n\n{}\n".format("å" * 8)) # pylint: disable=consider-using-f-string
|
||||||
violations = rule.validate(commit)
|
violations = rule.validate(commit)
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
|
||||||
|
@ -145,7 +144,7 @@ class BodyRuleTests(BaseTestCase):
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
|
||||||
# assert error for merge commits if ignore-merge-commits is disabled
|
# assert error for merge commits if ignore-merge-commits is disabled
|
||||||
rule = rules.BodyMissing({'ignore-merge-commits': False})
|
rule = rules.BodyMissing({"ignore-merge-commits": False})
|
||||||
violations = rule.validate(commit)
|
violations = rule.validate(commit)
|
||||||
expected_violation = rules.RuleViolation("B6", "Body message is missing", None, 3)
|
expected_violation = rules.RuleViolation("B6", "Body message is missing", None, 3)
|
||||||
self.assertListEqual(violations, [expected_violation])
|
self.assertListEqual(violations, [expected_violation])
|
||||||
|
@ -159,7 +158,7 @@ class BodyRuleTests(BaseTestCase):
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
|
||||||
# assert no error when no files have changed but certain files need to be mentioned on change
|
# assert no error when no files have changed but certain files need to be mentioned on change
|
||||||
rule = rules.BodyChangedFileMention({'files': "bar.txt,föo/test.py"})
|
rule = rules.BodyChangedFileMention({"files": "bar.txt,föo/test.py"})
|
||||||
commit = self.gitcommit("This is a test\n\nHere is a mention of föo/test.py")
|
commit = self.gitcommit("This is a test\n\nHere is a mention of föo/test.py")
|
||||||
violations = rule.validate(commit)
|
violations = rule.validate(commit)
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
@ -201,29 +200,29 @@ class BodyRuleTests(BaseTestCase):
|
||||||
|
|
||||||
# assert no violation on matching regex
|
# assert no violation on matching regex
|
||||||
# (also note that first body line - in between title and rest of body - is ignored)
|
# (also note that first body line - in between title and rest of body - is ignored)
|
||||||
rule = rules.BodyRegexMatches({'regex': "^Bödy(.*)"})
|
rule = rules.BodyRegexMatches({"regex": "^Bödy(.*)"})
|
||||||
violations = rule.validate(commit)
|
violations = rule.validate(commit)
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
|
||||||
# assert we can do end matching (and last empty line is ignored)
|
# 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)
|
# (also note that first body line - in between title and rest of body - is ignored)
|
||||||
rule = rules.BodyRegexMatches({'regex': "My-Commit-Tag: föo$"})
|
rule = rules.BodyRegexMatches({"regex": "My-Commit-Tag: föo$"})
|
||||||
violations = rule.validate(commit)
|
violations = rule.validate(commit)
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
|
||||||
# common use-case: matching that a given line is present
|
# common use-case: matching that a given line is present
|
||||||
rule = rules.BodyRegexMatches({'regex': "(.*)Föo(.*)"})
|
rule = rules.BodyRegexMatches({"regex": "(.*)Föo(.*)"})
|
||||||
violations = rule.validate(commit)
|
violations = rule.validate(commit)
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
|
||||||
# assert violation on non-matching body
|
# assert violation on non-matching body
|
||||||
rule = rules.BodyRegexMatches({'regex': "^Tëst(.*)Foo"})
|
rule = rules.BodyRegexMatches({"regex": "^Tëst(.*)Foo"})
|
||||||
violations = rule.validate(commit)
|
violations = rule.validate(commit)
|
||||||
expected_violation = rules.RuleViolation("B8", "Body does not match regex (^Tëst(.*)Foo)", None, 6)
|
expected_violation = rules.RuleViolation("B8", "Body does not match regex (^Tëst(.*)Foo)", None, 6)
|
||||||
self.assertListEqual(violations, [expected_violation])
|
self.assertListEqual(violations, [expected_violation])
|
||||||
|
|
||||||
# assert no violation on None regex
|
# assert no violation on None regex
|
||||||
rule = rules.BodyRegexMatches({'regex': None})
|
rule = rules.BodyRegexMatches({"regex": None})
|
||||||
violations = rule.validate(commit)
|
violations = rule.validate(commit)
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
|
||||||
|
@ -231,6 +230,6 @@ class BodyRuleTests(BaseTestCase):
|
||||||
bodies = ["åbc", "åbc\n", "åbc\nföo\n", "åbc\n\n", "åbc\nföo\nblå", "åbc\nföo\nblå\n"]
|
bodies = ["åbc", "åbc\n", "åbc\nföo\n", "åbc\n\n", "åbc\nföo\nblå", "åbc\nföo\nblå\n"]
|
||||||
for body in bodies:
|
for body in bodies:
|
||||||
commit = self.gitcommit(body)
|
commit = self.gitcommit(body)
|
||||||
rule = rules.BodyRegexMatches({'regex': ".*"})
|
rule = rules.BodyRegexMatches({"regex": ".*"})
|
||||||
violations = rule.validate(commit)
|
violations = rule.validate(commit)
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
from gitlint.tests.base import BaseTestCase, EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING
|
||||||
from gitlint.tests.base import BaseTestCase
|
|
||||||
from gitlint import rules
|
from gitlint import rules
|
||||||
from gitlint.config import LintConfig
|
from gitlint.config import LintConfig
|
||||||
|
|
||||||
|
@ -22,20 +21,25 @@ class ConfigurationRuleTests(BaseTestCase):
|
||||||
rule.apply(config, commit)
|
rule.apply(config, commit)
|
||||||
self.assertEqual(config, expected_config)
|
self.assertEqual(config, expected_config)
|
||||||
|
|
||||||
expected_log_message = "DEBUG: gitlint.rules Ignoring commit because of rule 'I1': " + \
|
expected_log_messages = [
|
||||||
"Commit title 'Releäse' matches the regex '^Releäse(.*)', ignoring rules: all"
|
EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING.format("I1", "ignore-by-title"),
|
||||||
self.assert_log_contains(expected_log_message)
|
"DEBUG: gitlint.rules Ignoring commit because of rule 'I1': "
|
||||||
|
"Commit title 'Releäse' matches the regex '^Releäse(.*)', ignoring rules: all",
|
||||||
|
]
|
||||||
|
self.assert_logged(expected_log_messages)
|
||||||
|
|
||||||
# Matching regex with specific ignore
|
# Matching regex with specific ignore
|
||||||
rule = rules.IgnoreByTitle({"regex": "^Releäse(.*)",
|
rule = rules.IgnoreByTitle({"regex": "^Releäse(.*)", "ignore": "T1,B2"})
|
||||||
"ignore": "T1,B2"})
|
|
||||||
expected_config = LintConfig()
|
expected_config = LintConfig()
|
||||||
expected_config.ignore = "T1,B2"
|
expected_config.ignore = "T1,B2"
|
||||||
rule.apply(config, commit)
|
rule.apply(config, commit)
|
||||||
self.assertEqual(config, expected_config)
|
self.assertEqual(config, expected_config)
|
||||||
|
|
||||||
expected_log_message = "DEBUG: gitlint.rules Ignoring commit because of rule 'I1': " + \
|
expected_log_messages += [
|
||||||
|
"DEBUG: gitlint.rules Ignoring commit because of rule 'I1': "
|
||||||
"Commit title 'Releäse' matches the regex '^Releäse(.*)', ignoring rules: T1,B2"
|
"Commit title 'Releäse' matches the regex '^Releäse(.*)', ignoring rules: T1,B2"
|
||||||
|
]
|
||||||
|
self.assert_logged(expected_log_messages)
|
||||||
|
|
||||||
def test_ignore_by_body(self):
|
def test_ignore_by_body(self):
|
||||||
commit = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line")
|
commit = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line")
|
||||||
|
@ -54,22 +58,26 @@ class ConfigurationRuleTests(BaseTestCase):
|
||||||
rule.apply(config, commit)
|
rule.apply(config, commit)
|
||||||
self.assertEqual(config, expected_config)
|
self.assertEqual(config, expected_config)
|
||||||
|
|
||||||
expected_log_message = "DEBUG: gitlint.rules Ignoring commit because of rule 'I2': " + \
|
expected_log_messages = [
|
||||||
"Commit message line ' a relëase body' matches the regex '(.*)relëase(.*)'," + \
|
EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING.format("I2", "ignore-by-body"),
|
||||||
" ignoring rules: all"
|
"DEBUG: gitlint.rules Ignoring commit because of rule 'I2': "
|
||||||
self.assert_log_contains(expected_log_message)
|
"Commit message line ' a relëase body' matches the regex '(.*)relëase(.*)',"
|
||||||
|
" ignoring rules: all",
|
||||||
|
]
|
||||||
|
self.assert_logged(expected_log_messages)
|
||||||
|
|
||||||
# Matching regex with specific ignore
|
# Matching regex with specific ignore
|
||||||
rule = rules.IgnoreByBody({"regex": "(.*)relëase(.*)",
|
rule = rules.IgnoreByBody({"regex": "(.*)relëase(.*)", "ignore": "T1,B2"})
|
||||||
"ignore": "T1,B2"})
|
|
||||||
expected_config = LintConfig()
|
expected_config = LintConfig()
|
||||||
expected_config.ignore = "T1,B2"
|
expected_config.ignore = "T1,B2"
|
||||||
rule.apply(config, commit)
|
rule.apply(config, commit)
|
||||||
self.assertEqual(config, expected_config)
|
self.assertEqual(config, expected_config)
|
||||||
|
|
||||||
expected_log_message = "DEBUG: gitlint.rules Ignoring commit because of rule 'I2': " + \
|
expected_log_messages += [
|
||||||
|
"DEBUG: gitlint.rules Ignoring commit because of rule 'I2': "
|
||||||
"Commit message line ' a relëase body' matches the regex '(.*)relëase(.*)', ignoring rules: T1,B2"
|
"Commit message line ' a relëase body' matches the regex '(.*)relëase(.*)', ignoring rules: T1,B2"
|
||||||
self.assert_log_contains(expected_log_message)
|
]
|
||||||
|
self.assert_logged(expected_log_messages)
|
||||||
|
|
||||||
def test_ignore_by_author_name(self):
|
def test_ignore_by_author_name(self):
|
||||||
commit = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line", author_name="Tëst nåme")
|
commit = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line", author_name="Tëst nåme")
|
||||||
|
@ -88,10 +96,13 @@ class ConfigurationRuleTests(BaseTestCase):
|
||||||
rule.apply(config, commit)
|
rule.apply(config, commit)
|
||||||
self.assertEqual(config, expected_config)
|
self.assertEqual(config, expected_config)
|
||||||
|
|
||||||
expected_log_message = ("DEBUG: gitlint.rules Ignoring commit because of rule 'I4': "
|
expected_log_messages = [
|
||||||
"Commit Author Name 'Tëst nåme' matches the regex '(.*)ëst(.*)',"
|
EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING.format("I4", "ignore-by-author-name"),
|
||||||
" ignoring rules: all")
|
"DEBUG: gitlint.rules Ignoring commit because of rule 'I4': "
|
||||||
self.assert_log_contains(expected_log_message)
|
"Commit Author Name 'Tëst nåme' matches the regex '(.*)ëst(.*)',"
|
||||||
|
" ignoring rules: all",
|
||||||
|
]
|
||||||
|
self.assert_logged(expected_log_messages)
|
||||||
|
|
||||||
# Matching regex with specific ignore
|
# Matching regex with specific ignore
|
||||||
rule = rules.IgnoreByAuthorName({"regex": "(.*)nåme", "ignore": "T1,B2"})
|
rule = rules.IgnoreByAuthorName({"regex": "(.*)nåme", "ignore": "T1,B2"})
|
||||||
|
@ -100,9 +111,11 @@ class ConfigurationRuleTests(BaseTestCase):
|
||||||
rule.apply(config, commit)
|
rule.apply(config, commit)
|
||||||
self.assertEqual(config, expected_config)
|
self.assertEqual(config, expected_config)
|
||||||
|
|
||||||
expected_log_message = ("DEBUG: gitlint.rules Ignoring commit because of rule 'I4': "
|
expected_log_messages += [
|
||||||
"Commit Author Name 'Tëst nåme' matches the regex '(.*)nåme', ignoring rules: T1,B2")
|
"DEBUG: gitlint.rules Ignoring commit because of rule 'I4': "
|
||||||
self.assert_log_contains(expected_log_message)
|
"Commit Author Name 'Tëst nåme' matches the regex '(.*)nåme', ignoring rules: T1,B2"
|
||||||
|
]
|
||||||
|
self.assert_logged(expected_log_messages)
|
||||||
|
|
||||||
def test_ignore_body_lines(self):
|
def test_ignore_body_lines(self):
|
||||||
commit1 = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line")
|
commit1 = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line")
|
||||||
|
@ -128,8 +141,11 @@ class ConfigurationRuleTests(BaseTestCase):
|
||||||
expected_commit.message.original = commit1.message.original
|
expected_commit.message.original = commit1.message.original
|
||||||
self.assertEqual(commit1, expected_commit)
|
self.assertEqual(commit1, expected_commit)
|
||||||
self.assertEqual(config, LintConfig()) # config shouldn't have been modified
|
self.assertEqual(config, LintConfig()) # config shouldn't have been modified
|
||||||
self.assert_log_contains("DEBUG: gitlint.rules Ignoring line ' a relëase body' because it " +
|
expected_log_messages = [
|
||||||
"matches '(.*)relëase(.*)'")
|
EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING.format("I3", "ignore-body-lines"),
|
||||||
|
"DEBUG: gitlint.rules Ignoring line ' a relëase body' because it " + "matches '(.*)relëase(.*)'",
|
||||||
|
]
|
||||||
|
self.assert_logged(expected_log_messages)
|
||||||
|
|
||||||
# Non-Matching regex: no changes expected
|
# Non-Matching regex: no changes expected
|
||||||
commit1 = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line")
|
commit1 = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line")
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
from gitlint.tests.base import BaseTestCase, EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING
|
||||||
from gitlint.tests.base import BaseTestCase
|
|
||||||
from gitlint.rules import AuthorValidEmail, RuleViolation
|
from gitlint.rules import AuthorValidEmail, RuleViolation
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,8 +7,13 @@ class MetaRuleTests(BaseTestCase):
|
||||||
rule = AuthorValidEmail()
|
rule = AuthorValidEmail()
|
||||||
|
|
||||||
# valid email addresses
|
# valid email addresses
|
||||||
valid_email_addresses = ["föo@bar.com", "Jöhn.Doe@bar.com", "jöhn+doe@bar.com", "jöhn/doe@bar.com",
|
valid_email_addresses = [
|
||||||
"jöhn.doe@subdomain.bar.com"]
|
"föo@bar.com",
|
||||||
|
"Jöhn.Doe@bar.com",
|
||||||
|
"jöhn+doe@bar.com",
|
||||||
|
"jöhn/doe@bar.com",
|
||||||
|
"jöhn.doe@subdomain.bar.com",
|
||||||
|
]
|
||||||
for email in valid_email_addresses:
|
for email in valid_email_addresses:
|
||||||
commit = self.gitcommit("", author_email=email)
|
commit = self.gitcommit("", author_email=email)
|
||||||
violations = rule.validate(commit)
|
violations = rule.validate(commit)
|
||||||
|
@ -22,19 +26,32 @@ class MetaRuleTests(BaseTestCase):
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
|
||||||
# Invalid email addresses: no TLD, no domain, no @, space anywhere (=valid but not allowed by gitlint)
|
# Invalid email addresses: no TLD, no domain, no @, space anywhere (=valid but not allowed by gitlint)
|
||||||
invalid_email_addresses = ["föo@bar", "JöhnDoe", "Jöhn Doe", "Jöhn Doe@foo.com", " JöhnDoe@foo.com",
|
invalid_email_addresses = [
|
||||||
"JöhnDoe@ foo.com", "JöhnDoe@foo. com", "JöhnDoe@foo. com", "@bår.com",
|
"föo@bar",
|
||||||
"föo@.com"]
|
"JöhnDoe",
|
||||||
|
"Jöhn Doe",
|
||||||
|
"Jöhn Doe@foo.com",
|
||||||
|
" JöhnDoe@foo.com",
|
||||||
|
"JöhnDoe@ foo.com",
|
||||||
|
"JöhnDoe@foo. com",
|
||||||
|
"JöhnDoe@foo. com",
|
||||||
|
"@bår.com",
|
||||||
|
"föo@.com",
|
||||||
|
]
|
||||||
for email in invalid_email_addresses:
|
for email in invalid_email_addresses:
|
||||||
commit = self.gitcommit("", author_email=email)
|
commit = self.gitcommit("", author_email=email)
|
||||||
violations = rule.validate(commit)
|
violations = rule.validate(commit)
|
||||||
self.assertListEqual(violations,
|
self.assertListEqual(violations, [RuleViolation("M1", "Author email for commit is invalid", email)])
|
||||||
[RuleViolation("M1", "Author email for commit is invalid", email)])
|
|
||||||
|
# Ensure nothing is logged, this relates specifically to a deprecation warning on the use of
|
||||||
|
# re.match vs re.search in the rules (see issue #254)
|
||||||
|
# If no custom regex is used, the rule uses the default regex in combination with re.search
|
||||||
|
self.assert_logged([])
|
||||||
|
|
||||||
def test_author_valid_email_rule_custom_regex(self):
|
def test_author_valid_email_rule_custom_regex(self):
|
||||||
# regex=None -> the rule isn't applied
|
# regex=None -> the rule isn't applied
|
||||||
rule = AuthorValidEmail()
|
rule = AuthorValidEmail()
|
||||||
rule.options['regex'].set(None)
|
rule.options["regex"].set(None)
|
||||||
emailadresses = ["föo", None, "hür dür"]
|
emailadresses = ["föo", None, "hür dür"]
|
||||||
for email in emailadresses:
|
for email in emailadresses:
|
||||||
commit = self.gitcommit("", author_email=email)
|
commit = self.gitcommit("", author_email=email)
|
||||||
|
@ -42,9 +59,8 @@ class MetaRuleTests(BaseTestCase):
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
|
||||||
# Custom domain
|
# Custom domain
|
||||||
rule = AuthorValidEmail({'regex': "[^@]+@bår.com"})
|
rule = AuthorValidEmail({"regex": "[^@]+@bår.com"})
|
||||||
valid_email_addresses = [
|
valid_email_addresses = ["föo@bår.com", "Jöhn.Doe@bår.com", "jöhn+doe@bår.com", "jöhn/doe@bår.com"]
|
||||||
"föo@bår.com", "Jöhn.Doe@bår.com", "jöhn+doe@bår.com", "jöhn/doe@bår.com"]
|
|
||||||
for email in valid_email_addresses:
|
for email in valid_email_addresses:
|
||||||
commit = self.gitcommit("", author_email=email)
|
commit = self.gitcommit("", author_email=email)
|
||||||
violations = rule.validate(commit)
|
violations = rule.validate(commit)
|
||||||
|
@ -55,5 +71,7 @@ class MetaRuleTests(BaseTestCase):
|
||||||
for email in invalid_email_addresses:
|
for email in invalid_email_addresses:
|
||||||
commit = self.gitcommit("", author_email=email)
|
commit = self.gitcommit("", author_email=email)
|
||||||
violations = rule.validate(commit)
|
violations = rule.validate(commit)
|
||||||
self.assertListEqual(violations,
|
self.assertListEqual(violations, [RuleViolation("M1", "Author email for commit is invalid", email)])
|
||||||
[RuleViolation("M1", "Author email for commit is invalid", email)])
|
|
||||||
|
# When a custom regex is used, a warning should be logged by default
|
||||||
|
self.assert_logged([EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING.format("M1", "author-valid-email")])
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from gitlint.tests.base import BaseTestCase
|
from gitlint.tests.base import BaseTestCase
|
||||||
from gitlint.rules import Rule, RuleViolation
|
from gitlint.rules import Rule, RuleViolation
|
||||||
|
|
||||||
|
|
||||||
class RuleTests(BaseTestCase):
|
class RuleTests(BaseTestCase):
|
||||||
|
|
||||||
def test_rule_equality(self):
|
def test_rule_equality(self):
|
||||||
self.assertEqual(Rule(), Rule())
|
self.assertEqual(Rule(), Rule())
|
||||||
# Ensure rules are not equal if they differ on their attributes
|
# Ensure rules are not equal if they differ on their attributes
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from gitlint.tests.base import BaseTestCase
|
from gitlint.tests.base import BaseTestCase
|
||||||
from gitlint.rules import TitleMaxLength, TitleTrailingWhitespace, TitleHardTab, TitleMustNotContainWord, \
|
from gitlint.rules import (
|
||||||
TitleTrailingPunctuation, TitleLeadingWhitespace, TitleRegexMatches, RuleViolation, TitleMinLength
|
TitleMaxLength,
|
||||||
|
TitleTrailingWhitespace,
|
||||||
|
TitleHardTab,
|
||||||
|
TitleMustNotContainWord,
|
||||||
|
TitleTrailingPunctuation,
|
||||||
|
TitleLeadingWhitespace,
|
||||||
|
TitleRegexMatches,
|
||||||
|
RuleViolation,
|
||||||
|
TitleMinLength,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TitleRuleTests(BaseTestCase):
|
class TitleRuleTests(BaseTestCase):
|
||||||
|
@ -18,7 +26,7 @@ class TitleRuleTests(BaseTestCase):
|
||||||
self.assertListEqual(violations, [expected_violation])
|
self.assertListEqual(violations, [expected_violation])
|
||||||
|
|
||||||
# set line length to 120, and check no violation on length 73
|
# set line length to 120, and check no violation on length 73
|
||||||
rule = TitleMaxLength({'line-length': 120})
|
rule = TitleMaxLength({"line-length": 120})
|
||||||
violations = rule.validate("å" * 73, None)
|
violations = rule.validate("å" * 73, None)
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
|
||||||
|
@ -85,31 +93,37 @@ class TitleRuleTests(BaseTestCase):
|
||||||
|
|
||||||
# match literally
|
# match literally
|
||||||
violations = rule.validate("WIP This is å test", None)
|
violations = rule.validate("WIP This is å test", None)
|
||||||
expected_violation = RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
expected_violation = RuleViolation(
|
||||||
"WIP This is å test")
|
"T5", "Title contains the word 'WIP' (case-insensitive)", "WIP This is å test"
|
||||||
|
)
|
||||||
self.assertListEqual(violations, [expected_violation])
|
self.assertListEqual(violations, [expected_violation])
|
||||||
|
|
||||||
# match case insensitive
|
# match case insensitive
|
||||||
violations = rule.validate("wip This is å test", None)
|
violations = rule.validate("wip This is å test", None)
|
||||||
expected_violation = RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
expected_violation = RuleViolation(
|
||||||
"wip This is å test")
|
"T5", "Title contains the word 'WIP' (case-insensitive)", "wip This is å test"
|
||||||
|
)
|
||||||
self.assertListEqual(violations, [expected_violation])
|
self.assertListEqual(violations, [expected_violation])
|
||||||
|
|
||||||
# match if there is a colon after the word
|
# match if there is a colon after the word
|
||||||
violations = rule.validate("WIP:This is å test", None)
|
violations = rule.validate("WIP:This is å test", None)
|
||||||
expected_violation = RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
expected_violation = RuleViolation(
|
||||||
"WIP:This is å test")
|
"T5", "Title contains the word 'WIP' (case-insensitive)", "WIP:This is å test"
|
||||||
|
)
|
||||||
self.assertListEqual(violations, [expected_violation])
|
self.assertListEqual(violations, [expected_violation])
|
||||||
|
|
||||||
# match multiple words
|
# match multiple words
|
||||||
rule = TitleMustNotContainWord({'words': "wip,test,å"})
|
rule = TitleMustNotContainWord({"words": "wip,test,å"})
|
||||||
violations = rule.validate("WIP:This is å test", None)
|
violations = rule.validate("WIP:This is å test", None)
|
||||||
expected_violation = RuleViolation("T5", "Title contains the word 'wip' (case-insensitive)",
|
expected_violation = RuleViolation(
|
||||||
"WIP:This is å test")
|
"T5", "Title contains the word 'wip' (case-insensitive)", "WIP:This is å test"
|
||||||
expected_violation2 = RuleViolation("T5", "Title contains the word 'test' (case-insensitive)",
|
)
|
||||||
"WIP:This is å test")
|
expected_violation2 = RuleViolation(
|
||||||
expected_violation3 = RuleViolation("T5", "Title contains the word 'å' (case-insensitive)",
|
"T5", "Title contains the word 'test' (case-insensitive)", "WIP:This is å test"
|
||||||
"WIP:This is å test")
|
)
|
||||||
|
expected_violation3 = RuleViolation(
|
||||||
|
"T5", "Title contains the word 'å' (case-insensitive)", "WIP:This is å test"
|
||||||
|
)
|
||||||
self.assertListEqual(violations, [expected_violation, expected_violation2, expected_violation3])
|
self.assertListEqual(violations, [expected_violation, expected_violation2, expected_violation3])
|
||||||
|
|
||||||
def test_leading_whitespace(self):
|
def test_leading_whitespace(self):
|
||||||
|
@ -143,12 +157,12 @@ class TitleRuleTests(BaseTestCase):
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
|
||||||
# assert no violation on matching regex
|
# assert no violation on matching regex
|
||||||
rule = TitleRegexMatches({'regex': "^US[0-9]*: å"})
|
rule = TitleRegexMatches({"regex": "^US[0-9]*: å"})
|
||||||
violations = rule.validate(commit.message.title, commit)
|
violations = rule.validate(commit.message.title, commit)
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
|
||||||
# assert violation when no matching regex
|
# assert violation when no matching regex
|
||||||
rule = TitleRegexMatches({'regex': "^UÅ[0-9]*"})
|
rule = TitleRegexMatches({"regex": "^UÅ[0-9]*"})
|
||||||
violations = rule.validate(commit.message.title, commit)
|
violations = rule.validate(commit.message.title, commit)
|
||||||
expected_violation = RuleViolation("T7", "Title does not match regex (^UÅ[0-9]*)", "US1234: åbc")
|
expected_violation = RuleViolation("T7", "Title does not match regex (^UÅ[0-9]*)", "US1234: åbc")
|
||||||
self.assertListEqual(violations, [expected_violation])
|
self.assertListEqual(violations, [expected_violation])
|
||||||
|
@ -166,12 +180,12 @@ class TitleRuleTests(BaseTestCase):
|
||||||
self.assertListEqual(violations, [expected_violation])
|
self.assertListEqual(violations, [expected_violation])
|
||||||
|
|
||||||
# set line length to 3, and check no violation on length 4
|
# set line length to 3, and check no violation on length 4
|
||||||
rule = TitleMinLength({'min-length': 3})
|
rule = TitleMinLength({"min-length": 3})
|
||||||
violations = rule.validate("å" * 4, None)
|
violations = rule.validate("å" * 4, None)
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
|
||||||
# assert no violations on length 3 (this asserts we've implemented a *strict* less than)
|
# assert no violations on length 3 (this asserts we've implemented a *strict* less than)
|
||||||
rule = TitleMinLength({'min-length': 3})
|
rule = TitleMinLength({"min-length": 3})
|
||||||
violations = rule.validate("å" * 3, None)
|
violations = rule.validate("å" * 3, None)
|
||||||
self.assertIsNone(violations)
|
self.assertIsNone(violations)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
@ -33,7 +31,7 @@ class UserRuleTests(BaseTestCase):
|
||||||
# Do some basic asserts on our user rule
|
# Do some basic asserts on our user rule
|
||||||
self.assertEqual(classes[0].id, "UC1")
|
self.assertEqual(classes[0].id, "UC1")
|
||||||
self.assertEqual(classes[0].name, "my-üser-commit-rule")
|
self.assertEqual(classes[0].name, "my-üser-commit-rule")
|
||||||
expected_option = options.IntOption('violation-count', 1, "Number of violåtions to return")
|
expected_option = options.IntOption("violation-count", 1, "Number of violåtions to return")
|
||||||
self.assertListEqual(classes[0].options_spec, [expected_option])
|
self.assertListEqual(classes[0].options_spec, [expected_option])
|
||||||
self.assertTrue(hasattr(classes[0], "validate"))
|
self.assertTrue(hasattr(classes[0], "validate"))
|
||||||
|
|
||||||
|
@ -44,10 +42,15 @@ class UserRuleTests(BaseTestCase):
|
||||||
self.assertListEqual(violations, [rules.RuleViolation("UC1", "Commit violåtion 1", "Contënt 1", 1)])
|
self.assertListEqual(violations, [rules.RuleViolation("UC1", "Commit violåtion 1", "Contënt 1", 1)])
|
||||||
|
|
||||||
# Have it return more violations
|
# Have it return more violations
|
||||||
rule_class.options['violation-count'].value = 2
|
rule_class.options["violation-count"].value = 2
|
||||||
violations = rule_class.validate("false-commit-object (ignored)")
|
violations = rule_class.validate("false-commit-object (ignored)")
|
||||||
self.assertListEqual(violations, [rules.RuleViolation("UC1", "Commit violåtion 1", "Contënt 1", 1),
|
self.assertListEqual(
|
||||||
rules.RuleViolation("UC1", "Commit violåtion 2", "Contënt 2", 2)])
|
violations,
|
||||||
|
[
|
||||||
|
rules.RuleViolation("UC1", "Commit violåtion 1", "Contënt 1", 1),
|
||||||
|
rules.RuleViolation("UC1", "Commit violåtion 2", "Contënt 2", 2),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def test_extra_path_specified_by_file(self):
|
def test_extra_path_specified_by_file(self):
|
||||||
# Test that find_rule_classes can handle an extra path given as a file name instead of a directory
|
# Test that find_rule_classes can handle an extra path given as a file name instead of a directory
|
||||||
|
@ -67,7 +70,7 @@ class UserRuleTests(BaseTestCase):
|
||||||
classes = find_rule_classes(user_rule_path)
|
classes = find_rule_classes(user_rule_path)
|
||||||
|
|
||||||
# convert classes to strings and sort them so we can compare them
|
# convert classes to strings and sort them so we can compare them
|
||||||
class_strings = sorted([str(clazz) for clazz in classes])
|
class_strings = sorted(str(clazz) for clazz in classes)
|
||||||
expected = ["<class 'my_commit_rules.MyUserCommitRule'>", "<class 'parent_package.InitFileRule'>"]
|
expected = ["<class 'my_commit_rules.MyUserCommitRule'>", "<class 'parent_package.InitFileRule'>"]
|
||||||
self.assertListEqual(class_strings, expected)
|
self.assertListEqual(class_strings, expected)
|
||||||
|
|
||||||
|
@ -96,23 +99,23 @@ class UserRuleTests(BaseTestCase):
|
||||||
|
|
||||||
def test_assert_valid_rule_class(self):
|
def test_assert_valid_rule_class(self):
|
||||||
class MyLineRuleClass(rules.LineRule):
|
class MyLineRuleClass(rules.LineRule):
|
||||||
id = 'UC1'
|
id = "UC1"
|
||||||
name = 'my-lïne-rule'
|
name = "my-lïne-rule"
|
||||||
target = rules.CommitMessageTitle
|
target = rules.CommitMessageTitle
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class MyCommitRuleClass(rules.CommitRule):
|
class MyCommitRuleClass(rules.CommitRule):
|
||||||
id = 'UC2'
|
id = "UC2"
|
||||||
name = 'my-cömmit-rule'
|
name = "my-cömmit-rule"
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class MyConfigurationRuleClass(rules.ConfigurationRule):
|
class MyConfigurationRuleClass(rules.ConfigurationRule):
|
||||||
id = 'UC3'
|
id = "UC3"
|
||||||
name = 'my-cönfiguration-rule'
|
name = "my-cönfiguration-rule"
|
||||||
|
|
||||||
def apply(self):
|
def apply(self):
|
||||||
pass
|
pass
|
||||||
|
@ -125,8 +128,9 @@ class UserRuleTests(BaseTestCase):
|
||||||
def test_assert_valid_rule_class_negative(self):
|
def test_assert_valid_rule_class_negative(self):
|
||||||
# general test to make sure that incorrect rules will raise an exception
|
# general test to make sure that incorrect rules will raise an exception
|
||||||
user_rule_path = self.get_sample_path("user_rules/incorrect_linerule")
|
user_rule_path = self.get_sample_path("user_rules/incorrect_linerule")
|
||||||
with self.assertRaisesMessage(UserRuleError,
|
with self.assertRaisesMessage(
|
||||||
"User-defined rule class 'MyUserLineRule' must have a 'validate' method"):
|
UserRuleError, "User-defined rule class 'MyUserLineRule' must have a 'validate' method"
|
||||||
|
):
|
||||||
find_rule_classes(user_rule_path)
|
find_rule_classes(user_rule_path)
|
||||||
|
|
||||||
def test_assert_valid_rule_class_negative_parent(self):
|
def test_assert_valid_rule_class_negative_parent(self):
|
||||||
|
@ -134,13 +138,14 @@ class UserRuleTests(BaseTestCase):
|
||||||
class MyRuleClass:
|
class MyRuleClass:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
expected_msg = "User-defined rule class 'MyRuleClass' must extend from gitlint.rules.LineRule, " + \
|
expected_msg = (
|
||||||
"gitlint.rules.CommitRule or gitlint.rules.ConfigurationRule"
|
"User-defined rule class 'MyRuleClass' must extend from gitlint.rules.LineRule, "
|
||||||
|
"gitlint.rules.CommitRule or gitlint.rules.ConfigurationRule"
|
||||||
|
)
|
||||||
with self.assertRaisesMessage(UserRuleError, expected_msg):
|
with self.assertRaisesMessage(UserRuleError, expected_msg):
|
||||||
assert_valid_rule_class(MyRuleClass)
|
assert_valid_rule_class(MyRuleClass)
|
||||||
|
|
||||||
def test_assert_valid_rule_class_negative_id(self):
|
def test_assert_valid_rule_class_negative_id(self):
|
||||||
|
|
||||||
for parent_class in [rules.LineRule, rules.CommitRule]:
|
for parent_class in [rules.LineRule, rules.CommitRule]:
|
||||||
|
|
||||||
class MyRuleClass(parent_class):
|
class MyRuleClass(parent_class):
|
||||||
|
@ -159,8 +164,9 @@ class UserRuleTests(BaseTestCase):
|
||||||
# Rule ids must not start with one of the reserved id letters
|
# Rule ids must not start with one of the reserved id letters
|
||||||
for letter in ["T", "R", "B", "M", "I"]:
|
for letter in ["T", "R", "B", "M", "I"]:
|
||||||
MyRuleClass.id = letter + "1"
|
MyRuleClass.id = letter + "1"
|
||||||
expected_msg = f"The id '{letter}' of 'MyRuleClass' is invalid. " + \
|
expected_msg = (
|
||||||
"Gitlint reserves ids starting with R,T,B,M,I"
|
f"The id '{letter}' of 'MyRuleClass' is invalid. Gitlint reserves ids starting with R,T,B,M,I"
|
||||||
|
)
|
||||||
with self.assertRaisesMessage(UserRuleError, expected_msg):
|
with self.assertRaisesMessage(UserRuleError, expected_msg):
|
||||||
assert_valid_rule_class(MyRuleClass)
|
assert_valid_rule_class(MyRuleClass)
|
||||||
|
|
||||||
|
@ -181,7 +187,6 @@ class UserRuleTests(BaseTestCase):
|
||||||
assert_valid_rule_class(MyRuleClass)
|
assert_valid_rule_class(MyRuleClass)
|
||||||
|
|
||||||
def test_assert_valid_rule_class_negative_option_spec(self):
|
def test_assert_valid_rule_class_negative_option_spec(self):
|
||||||
|
|
||||||
for parent_class in [rules.LineRule, rules.CommitRule]:
|
for parent_class in [rules.LineRule, rules.CommitRule]:
|
||||||
|
|
||||||
class MyRuleClass(parent_class):
|
class MyRuleClass(parent_class):
|
||||||
|
@ -190,8 +195,10 @@ class UserRuleTests(BaseTestCase):
|
||||||
|
|
||||||
# if set, option_spec must be a list of gitlint options
|
# if set, option_spec must be a list of gitlint options
|
||||||
MyRuleClass.options_spec = "föo"
|
MyRuleClass.options_spec = "föo"
|
||||||
expected_msg = "The options_spec attribute of user-defined rule class 'MyRuleClass' must be a list " + \
|
expected_msg = (
|
||||||
|
"The options_spec attribute of user-defined rule class 'MyRuleClass' must be a list "
|
||||||
"of gitlint.options.RuleOption"
|
"of gitlint.options.RuleOption"
|
||||||
|
)
|
||||||
with self.assertRaisesMessage(UserRuleError, expected_msg):
|
with self.assertRaisesMessage(UserRuleError, expected_msg):
|
||||||
assert_valid_rule_class(MyRuleClass)
|
assert_valid_rule_class(MyRuleClass)
|
||||||
|
|
||||||
|
@ -201,21 +208,23 @@ class UserRuleTests(BaseTestCase):
|
||||||
assert_valid_rule_class(MyRuleClass)
|
assert_valid_rule_class(MyRuleClass)
|
||||||
|
|
||||||
def test_assert_valid_rule_class_negative_validate(self):
|
def test_assert_valid_rule_class_negative_validate(self):
|
||||||
|
|
||||||
baseclasses = [rules.LineRule, rules.CommitRule]
|
baseclasses = [rules.LineRule, rules.CommitRule]
|
||||||
for clazz in baseclasses:
|
for clazz in baseclasses:
|
||||||
|
|
||||||
class MyRuleClass(clazz):
|
class MyRuleClass(clazz):
|
||||||
id = "UC1"
|
id = "UC1"
|
||||||
name = "my-rüle-class"
|
name = "my-rüle-class"
|
||||||
|
|
||||||
with self.assertRaisesMessage(UserRuleError,
|
with self.assertRaisesMessage(
|
||||||
"User-defined rule class 'MyRuleClass' must have a 'validate' method"):
|
UserRuleError, "User-defined rule class 'MyRuleClass' must have a 'validate' method"
|
||||||
|
):
|
||||||
assert_valid_rule_class(MyRuleClass)
|
assert_valid_rule_class(MyRuleClass)
|
||||||
|
|
||||||
# validate attribute - not a method
|
# validate attribute - not a method
|
||||||
MyRuleClass.validate = "föo"
|
MyRuleClass.validate = "föo"
|
||||||
with self.assertRaisesMessage(UserRuleError,
|
with self.assertRaisesMessage(
|
||||||
"User-defined rule class 'MyRuleClass' must have a 'validate' method"):
|
UserRuleError, "User-defined rule class 'MyRuleClass' must have a 'validate' method"
|
||||||
|
):
|
||||||
assert_valid_rule_class(MyRuleClass)
|
assert_valid_rule_class(MyRuleClass)
|
||||||
|
|
||||||
def test_assert_valid_rule_class_negative_apply(self):
|
def test_assert_valid_rule_class_negative_apply(self):
|
||||||
|
@ -241,8 +250,10 @@ class UserRuleTests(BaseTestCase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# no target
|
# no target
|
||||||
expected_msg = "The target attribute of the user-defined LineRule class 'MyRuleClass' must be either " + \
|
expected_msg = (
|
||||||
"gitlint.rules.CommitMessageTitle or gitlint.rules.CommitMessageBody"
|
"The target attribute of the user-defined LineRule class 'MyRuleClass' must be either "
|
||||||
|
"gitlint.rules.CommitMessageTitle or gitlint.rules.CommitMessageBody"
|
||||||
|
)
|
||||||
with self.assertRaisesMessage(UserRuleError, expected_msg):
|
with self.assertRaisesMessage(UserRuleError, expected_msg):
|
||||||
assert_valid_rule_class(MyRuleClass)
|
assert_valid_rule_class(MyRuleClass)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
amend! WIP: This is a fixup cömmit with violations.
|
2
gitlint-core/gitlint/tests/samples/config/AUTHORS
Normal file
2
gitlint-core/gitlint/tests/samples/config/AUTHORS
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
John Doe <john.doe@mail.com>
|
||||||
|
Bob Smith <bob.smith@mail.com>
|
|
@ -1,3 +1,2 @@
|
||||||
# flake8: noqa
|
|
||||||
# This is invalid python code which will cause an import exception
|
# This is invalid python code which will cause an import exception
|
||||||
class MyObject:
|
class MyObject:
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from gitlint.rules import LineRule
|
from gitlint.rules import LineRule
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from gitlint.rules import CommitRule, RuleViolation
|
from gitlint.rules import CommitRule, RuleViolation
|
||||||
from gitlint.options import IntOption
|
from gitlint.options import IntOption
|
||||||
|
|
||||||
|
@ -7,11 +5,11 @@ from gitlint.options import IntOption
|
||||||
class MyUserCommitRule(CommitRule):
|
class MyUserCommitRule(CommitRule):
|
||||||
name = "my-üser-commit-rule"
|
name = "my-üser-commit-rule"
|
||||||
id = "UC1"
|
id = "UC1"
|
||||||
options_spec = [IntOption('violation-count', 1, "Number of violåtions to return")]
|
options_spec = [IntOption("violation-count", 1, "Number of violåtions to return")]
|
||||||
|
|
||||||
def validate(self, _commit):
|
def validate(self, _commit):
|
||||||
violations = []
|
violations = []
|
||||||
for i in range(1, self.options['violation-count'].value + 1):
|
for i in range(1, self.options["violation-count"].value + 1):
|
||||||
violations.append(RuleViolation(self.id, "Commit violåtion %d" % i, "Contënt %d" % i, i))
|
violations.append(RuleViolation(self.id, "Commit violåtion %d" % i, "Contënt %d" % i, i))
|
||||||
|
|
||||||
return violations
|
return violations
|
||||||
|
@ -19,6 +17,7 @@ class MyUserCommitRule(CommitRule):
|
||||||
|
|
||||||
# The below code is present so that we can test that we actually ignore it
|
# The below code is present so that we can test that we actually ignore it
|
||||||
|
|
||||||
|
|
||||||
def func_should_be_ignored():
|
def func_should_be_ignored():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# This file is meant to test that we can also load rules from __init__.py files, this was an issue with pypy before.
|
# This file is meant to test that we can also load rules from __init__.py files, this was an issue with pypy before.
|
||||||
|
|
||||||
from gitlint.rules import CommitRule
|
from gitlint.rules import CommitRule
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from gitlint.rules import CommitRule
|
from gitlint.rules import CommitRule
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from gitlint.tests.base import BaseTestCase
|
from gitlint.tests.base import BaseTestCase
|
||||||
from gitlint.cache import PropertyCache, cache
|
from gitlint.cache import PropertyCache, cache
|
||||||
|
|
||||||
|
|
||||||
class CacheTests(BaseTestCase):
|
class CacheTests(BaseTestCase):
|
||||||
|
|
||||||
class MyClass(PropertyCache):
|
class MyClass(PropertyCache):
|
||||||
""" Simple class that has cached properties, used for testing. """
|
"""Simple class that has cached properties, used for testing."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
PropertyCache.__init__(self)
|
PropertyCache.__init__(self)
|
||||||
|
|
23
gitlint-core/gitlint/tests/test_deprecation.py
Normal file
23
gitlint-core/gitlint/tests/test_deprecation.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from gitlint.config import LintConfig
|
||||||
|
from gitlint.deprecation import Deprecation
|
||||||
|
from gitlint.rules import IgnoreByTitle
|
||||||
|
from gitlint.tests.base import EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING, BaseTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecationTests(BaseTestCase):
|
||||||
|
def test_get_regex_method(self):
|
||||||
|
config = LintConfig()
|
||||||
|
Deprecation.config = config
|
||||||
|
rule = IgnoreByTitle({"regex": "^Releäse(.*)"})
|
||||||
|
|
||||||
|
# When general.regex-style-search=True, we expect regex.search to be returned and no warning to be logged
|
||||||
|
config.regex_style_search = True
|
||||||
|
regex_method = Deprecation.get_regex_method(rule, rule.options["regex"])
|
||||||
|
self.assertEqual(regex_method, rule.options["regex"].value.search)
|
||||||
|
self.assert_logged([])
|
||||||
|
|
||||||
|
# When general.regex-style-search=False, we expect regex.match to be returned and a warning to be logged
|
||||||
|
config.regex_style_search = False
|
||||||
|
regex_method = Deprecation.get_regex_method(rule, rule.options["regex"])
|
||||||
|
self.assertEqual(regex_method, rule.options["regex"].value.match)
|
||||||
|
self.assert_logged([EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING.format("I1", "ignore-by-title")])
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
|
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
|
||||||
|
@ -14,9 +12,9 @@ class DisplayTests(BaseTestCase):
|
||||||
display = Display(LintConfig())
|
display = Display(LintConfig())
|
||||||
display.config.verbosity = 2
|
display.config.verbosity = 2
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
# Non exact outputting, should output both v and vv output
|
# Non exact outputting, should output both v and vv output
|
||||||
with patch('gitlint.display.stdout', new=StringIO()) as stdout:
|
with patch("gitlint.display.stdout", new=StringIO()) as stdout:
|
||||||
display.v("tëst")
|
display.v("tëst")
|
||||||
display.vv("tëst2")
|
display.vv("tëst2")
|
||||||
# vvvv should be ignored regardless
|
# vvvv should be ignored regardless
|
||||||
|
@ -25,7 +23,7 @@ class DisplayTests(BaseTestCase):
|
||||||
self.assertEqual("tëst\ntëst2\n", stdout.getvalue())
|
self.assertEqual("tëst\ntëst2\n", stdout.getvalue())
|
||||||
|
|
||||||
# exact outputting, should only output v
|
# exact outputting, should only output v
|
||||||
with patch('gitlint.display.stdout', new=StringIO()) as stdout:
|
with patch("gitlint.display.stdout", new=StringIO()) as stdout:
|
||||||
display.v("tëst", exact=True)
|
display.v("tëst", exact=True)
|
||||||
display.vv("tëst2", exact=True)
|
display.vv("tëst2", exact=True)
|
||||||
# vvvv should be ignored regardless
|
# vvvv should be ignored regardless
|
||||||
|
@ -33,16 +31,16 @@ class DisplayTests(BaseTestCase):
|
||||||
display.vvv("tëst3.2", exact=True)
|
display.vvv("tëst3.2", exact=True)
|
||||||
self.assertEqual("tëst2\n", stdout.getvalue())
|
self.assertEqual("tëst2\n", stdout.getvalue())
|
||||||
|
|
||||||
# standard error should be empty throughtout all of this
|
# standard error should be empty throughout all of this
|
||||||
self.assertEqual('', stderr.getvalue())
|
self.assertEqual("", stderr.getvalue())
|
||||||
|
|
||||||
def test_e(self):
|
def test_e(self):
|
||||||
display = Display(LintConfig())
|
display = Display(LintConfig())
|
||||||
display.config.verbosity = 2
|
display.config.verbosity = 2
|
||||||
|
|
||||||
with patch('gitlint.display.stdout', new=StringIO()) as stdout:
|
with patch("gitlint.display.stdout", new=StringIO()) as stdout:
|
||||||
# Non exact outputting, should output both v and vv output
|
# Non exact outputting, should output both v and vv output
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
display.e("tëst")
|
display.e("tëst")
|
||||||
display.ee("tëst2")
|
display.ee("tëst2")
|
||||||
# vvvv should be ignored regardless
|
# vvvv should be ignored regardless
|
||||||
|
@ -51,7 +49,7 @@ class DisplayTests(BaseTestCase):
|
||||||
self.assertEqual("tëst\ntëst2\n", stderr.getvalue())
|
self.assertEqual("tëst\ntëst2\n", stderr.getvalue())
|
||||||
|
|
||||||
# exact outputting, should only output v
|
# exact outputting, should only output v
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
display.e("tëst", exact=True)
|
display.e("tëst", exact=True)
|
||||||
display.ee("tëst2", exact=True)
|
display.ee("tëst2", exact=True)
|
||||||
# vvvv should be ignored regardless
|
# vvvv should be ignored regardless
|
||||||
|
@ -59,5 +57,5 @@ class DisplayTests(BaseTestCase):
|
||||||
display.eee("tëst3.2", exact=True)
|
display.eee("tëst3.2", exact=True)
|
||||||
self.assertEqual("tëst2\n", stderr.getvalue())
|
self.assertEqual("tëst2\n", stderr.getvalue())
|
||||||
|
|
||||||
# standard output should be empty throughtout all of this
|
# standard output should be empty throughout all of this
|
||||||
self.assertEqual('', stdout.getvalue())
|
self.assertEqual("", stdout.getvalue())
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from unittest.mock import patch, ANY, mock_open
|
from unittest.mock import patch, ANY, mock_open
|
||||||
|
|
||||||
from gitlint.tests.base import BaseTestCase
|
from gitlint.tests.base import BaseTestCase
|
||||||
from gitlint.config import LintConfig
|
from gitlint.config import LintConfig
|
||||||
from gitlint.hooks import GitHookInstaller, GitHookInstallerError, COMMIT_MSG_HOOK_SRC_PATH, COMMIT_MSG_HOOK_DST_PATH, \
|
from gitlint.hooks import (
|
||||||
GITLINT_HOOK_IDENTIFIER
|
GitHookInstaller,
|
||||||
|
GitHookInstallerError,
|
||||||
|
COMMIT_MSG_HOOK_SRC_PATH,
|
||||||
|
COMMIT_MSG_HOOK_DST_PATH,
|
||||||
|
GITLINT_HOOK_IDENTIFIER,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HookTests(BaseTestCase):
|
class HookTests(BaseTestCase):
|
||||||
|
@patch("gitlint.hooks.git_hooks_dir")
|
||||||
@patch('gitlint.hooks.git_hooks_dir')
|
|
||||||
def test_commit_msg_hook_path(self, git_hooks_dir):
|
def test_commit_msg_hook_path(self, git_hooks_dir):
|
||||||
git_hooks_dir.return_value = os.path.join("/föo", "bar")
|
git_hooks_dir.return_value = os.path.join("/föo", "bar")
|
||||||
lint_config = LintConfig()
|
lint_config = LintConfig()
|
||||||
|
@ -24,12 +26,12 @@ class HookTests(BaseTestCase):
|
||||||
self.assertEqual(path, expected_path)
|
self.assertEqual(path, expected_path)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@patch('os.chmod')
|
@patch("os.chmod")
|
||||||
@patch('os.stat')
|
@patch("os.stat")
|
||||||
@patch('gitlint.hooks.shutil.copy')
|
@patch("gitlint.hooks.shutil.copy")
|
||||||
@patch('os.path.exists', return_value=False)
|
@patch("os.path.exists", return_value=False)
|
||||||
@patch('os.path.isdir', return_value=True)
|
@patch("os.path.isdir", return_value=True)
|
||||||
@patch('gitlint.hooks.git_hooks_dir')
|
@patch("gitlint.hooks.git_hooks_dir")
|
||||||
def test_install_commit_msg_hook(git_hooks_dir, isdir, path_exists, copy, stat, chmod):
|
def test_install_commit_msg_hook(git_hooks_dir, isdir, path_exists, copy, stat, chmod):
|
||||||
lint_config = LintConfig()
|
lint_config = LintConfig()
|
||||||
lint_config.target = os.path.join("/hür", "dur")
|
lint_config.target = os.path.join("/hür", "dur")
|
||||||
|
@ -43,10 +45,10 @@ class HookTests(BaseTestCase):
|
||||||
chmod.assert_called_once_with(expected_dst, ANY)
|
chmod.assert_called_once_with(expected_dst, ANY)
|
||||||
git_hooks_dir.assert_called_with(lint_config.target)
|
git_hooks_dir.assert_called_with(lint_config.target)
|
||||||
|
|
||||||
@patch('gitlint.hooks.shutil.copy')
|
@patch("gitlint.hooks.shutil.copy")
|
||||||
@patch('os.path.exists', return_value=False)
|
@patch("os.path.exists", return_value=False)
|
||||||
@patch('os.path.isdir', return_value=True)
|
@patch("os.path.isdir", return_value=True)
|
||||||
@patch('gitlint.hooks.git_hooks_dir')
|
@patch("gitlint.hooks.git_hooks_dir")
|
||||||
def test_install_commit_msg_hook_negative(self, git_hooks_dir, isdir, path_exists, copy):
|
def test_install_commit_msg_hook_negative(self, git_hooks_dir, isdir, path_exists, copy):
|
||||||
lint_config = LintConfig()
|
lint_config = LintConfig()
|
||||||
lint_config.target = os.path.join("/hür", "dur")
|
lint_config.target = os.path.join("/hür", "dur")
|
||||||
|
@ -64,22 +66,24 @@ class HookTests(BaseTestCase):
|
||||||
isdir.return_value = True
|
isdir.return_value = True
|
||||||
path_exists.return_value = True
|
path_exists.return_value = True
|
||||||
expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH)
|
expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH)
|
||||||
expected_msg = f"There is already a commit-msg hook file present in {expected_dst}.\n" + \
|
expected_msg = (
|
||||||
"gitlint currently does not support appending to an existing commit-msg file."
|
f"There is already a commit-msg hook file present in {expected_dst}.\n"
|
||||||
|
"gitlint currently does not support appending to an existing commit-msg file."
|
||||||
|
)
|
||||||
with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
|
with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
|
||||||
GitHookInstaller.install_commit_msg_hook(lint_config)
|
GitHookInstaller.install_commit_msg_hook(lint_config)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@patch('os.remove')
|
@patch("os.remove")
|
||||||
@patch('os.path.exists', return_value=True)
|
@patch("os.path.exists", return_value=True)
|
||||||
@patch('os.path.isdir', return_value=True)
|
@patch("os.path.isdir", return_value=True)
|
||||||
@patch('gitlint.hooks.git_hooks_dir')
|
@patch("gitlint.hooks.git_hooks_dir")
|
||||||
def test_uninstall_commit_msg_hook(git_hooks_dir, isdir, path_exists, remove):
|
def test_uninstall_commit_msg_hook(git_hooks_dir, isdir, path_exists, remove):
|
||||||
lint_config = LintConfig()
|
lint_config = LintConfig()
|
||||||
git_hooks_dir.return_value = os.path.join("/föo", "bar", ".git", "hooks")
|
git_hooks_dir.return_value = os.path.join("/föo", "bar", ".git", "hooks")
|
||||||
lint_config.target = os.path.join("/hür", "dur")
|
lint_config.target = os.path.join("/hür", "dur")
|
||||||
read_data = "#!/bin/sh\n" + GITLINT_HOOK_IDENTIFIER
|
read_data = "#!/bin/sh\n" + GITLINT_HOOK_IDENTIFIER
|
||||||
with patch('gitlint.hooks.io.open', mock_open(read_data=read_data), create=True):
|
with patch("builtins.open", mock_open(read_data=read_data), create=True):
|
||||||
GitHookInstaller.uninstall_commit_msg_hook(lint_config)
|
GitHookInstaller.uninstall_commit_msg_hook(lint_config)
|
||||||
|
|
||||||
expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH)
|
expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH)
|
||||||
|
@ -88,10 +92,10 @@ class HookTests(BaseTestCase):
|
||||||
remove.assert_called_with(expected_dst)
|
remove.assert_called_with(expected_dst)
|
||||||
git_hooks_dir.assert_called_with(lint_config.target)
|
git_hooks_dir.assert_called_with(lint_config.target)
|
||||||
|
|
||||||
@patch('os.remove')
|
@patch("os.remove")
|
||||||
@patch('os.path.exists', return_value=True)
|
@patch("os.path.exists", return_value=True)
|
||||||
@patch('os.path.isdir', return_value=True)
|
@patch("os.path.isdir", return_value=True)
|
||||||
@patch('gitlint.hooks.git_hooks_dir')
|
@patch("gitlint.hooks.git_hooks_dir")
|
||||||
def test_uninstall_commit_msg_hook_negative(self, git_hooks_dir, isdir, path_exists, remove):
|
def test_uninstall_commit_msg_hook_negative(self, git_hooks_dir, isdir, path_exists, remove):
|
||||||
lint_config = LintConfig()
|
lint_config = LintConfig()
|
||||||
lint_config.target = os.path.join("/hür", "dur")
|
lint_config.target = os.path.join("/hür", "dur")
|
||||||
|
@ -122,10 +126,12 @@ class HookTests(BaseTestCase):
|
||||||
path_exists.return_value = True
|
path_exists.return_value = True
|
||||||
read_data = "#!/bin/sh\nfoo"
|
read_data = "#!/bin/sh\nfoo"
|
||||||
expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH)
|
expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH)
|
||||||
expected_msg = f"The commit-msg hook in {expected_dst} was not installed by gitlint " + \
|
expected_msg = (
|
||||||
"(or it was modified).\nUninstallation of 3th party or modified gitlint hooks " + \
|
f"The commit-msg hook in {expected_dst} was not installed by gitlint "
|
||||||
"is not supported."
|
"(or it was modified).\nUninstallation of 3th party or modified gitlint hooks "
|
||||||
with patch('gitlint.hooks.io.open', mock_open(read_data=read_data), create=True):
|
"is not supported."
|
||||||
|
)
|
||||||
|
with patch("builtins.open", mock_open(read_data=read_data), create=True):
|
||||||
with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
|
with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
|
||||||
GitHookInstaller.uninstall_commit_msg_hook(lint_config)
|
GitHookInstaller.uninstall_commit_msg_hook(lint_config)
|
||||||
remove.assert_not_called()
|
remove.assert_not_called()
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
|
from unittest.mock import patch # pylint: disable=no-name-in-module, import-error
|
||||||
|
@ -11,23 +9,26 @@ from gitlint.config import LintConfig, LintConfigBuilder
|
||||||
|
|
||||||
|
|
||||||
class LintTests(BaseTestCase):
|
class LintTests(BaseTestCase):
|
||||||
|
|
||||||
def test_lint_sample1(self):
|
def test_lint_sample1(self):
|
||||||
linter = GitLinter(LintConfig())
|
linter = GitLinter(LintConfig())
|
||||||
gitcontext = self.gitcontext(self.get_sample("commit_message/sample1"))
|
gitcontext = self.gitcontext(self.get_sample("commit_message/sample1"))
|
||||||
violations = linter.lint(gitcontext.commits[-1])
|
violations = linter.lint(gitcontext.commits[-1])
|
||||||
expected_errors = [RuleViolation("T3", "Title has trailing punctuation (.)",
|
# fmt: off
|
||||||
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
|
expected_errors = [
|
||||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
RuleViolation("T3", "Title has trailing punctuation (.)",
|
||||||
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
|
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
|
||||||
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
|
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
||||||
RuleViolation("B1", "Line exceeds max length (135>80)",
|
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
|
||||||
"This is the first line of the commit message body and it is meant to test " +
|
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
|
||||||
"a line that exceeds the maximum line length of 80 characters.", 3),
|
RuleViolation("B1", "Line exceeds max length (135>80)",
|
||||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a tråiling space. ", 4),
|
"This is the first line of the commit message body and it is meant to test " +
|
||||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing tab.\t", 5),
|
"a line that exceeds the maximum line length of 80 characters.", 3),
|
||||||
RuleViolation("B3", "Line contains hard tab characters (\\t)",
|
RuleViolation("B2", "Line has trailing whitespace", "This line has a tråiling space. ", 4),
|
||||||
"This line has a trailing tab.\t", 5)]
|
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing tab.\t", 5),
|
||||||
|
RuleViolation("B3", "Line contains hard tab characters (\\t)",
|
||||||
|
"This line has a trailing tab.\t", 5)
|
||||||
|
]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
self.assertListEqual(violations, expected_errors)
|
self.assertListEqual(violations, expected_errors)
|
||||||
|
|
||||||
|
@ -35,9 +36,10 @@ class LintTests(BaseTestCase):
|
||||||
linter = GitLinter(LintConfig())
|
linter = GitLinter(LintConfig())
|
||||||
gitcontext = self.gitcontext(self.get_sample("commit_message/sample2"))
|
gitcontext = self.gitcontext(self.get_sample("commit_message/sample2"))
|
||||||
violations = linter.lint(gitcontext.commits[-1])
|
violations = linter.lint(gitcontext.commits[-1])
|
||||||
expected = [RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
expected = [
|
||||||
"Just a title contåining WIP", 1),
|
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", "Just a title contåining WIP", 1),
|
||||||
RuleViolation("B6", "Body message is missing", None, 3)]
|
RuleViolation("B6", "Body message is missing", None, 3),
|
||||||
|
]
|
||||||
|
|
||||||
self.assertListEqual(violations, expected)
|
self.assertListEqual(violations, expected)
|
||||||
|
|
||||||
|
@ -46,20 +48,24 @@ class LintTests(BaseTestCase):
|
||||||
gitcontext = self.gitcontext(self.get_sample("commit_message/sample3"))
|
gitcontext = self.gitcontext(self.get_sample("commit_message/sample3"))
|
||||||
violations = linter.lint(gitcontext.commits[-1])
|
violations = linter.lint(gitcontext.commits[-1])
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
title = " Commit title containing 'WIP', \tleading and tråiling whitespace and longer than 72 characters."
|
title = " Commit title containing 'WIP', \tleading and tråiling whitespace and longer than 72 characters."
|
||||||
expected = [RuleViolation("T1", "Title exceeds max length (95>72)", title, 1),
|
expected = [
|
||||||
RuleViolation("T3", "Title has trailing punctuation (.)", title, 1),
|
RuleViolation("T1", "Title exceeds max length (95>72)", title, 1),
|
||||||
RuleViolation("T4", "Title contains hard tab characters (\\t)", title, 1),
|
RuleViolation("T3", "Title has trailing punctuation (.)", title, 1),
|
||||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", title, 1),
|
RuleViolation("T4", "Title contains hard tab characters (\\t)", title, 1),
|
||||||
RuleViolation("T6", "Title has leading whitespace", title, 1),
|
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", title, 1),
|
||||||
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
|
RuleViolation("T6", "Title has leading whitespace", title, 1),
|
||||||
RuleViolation("B1", "Line exceeds max length (101>80)",
|
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
|
||||||
"This is the first line is meånt to test a line that exceeds the maximum line " +
|
RuleViolation("B1", "Line exceeds max length (101>80)",
|
||||||
"length of 80 characters.", 3),
|
"This is the first line is meånt to test a line that exceeds the maximum line " +
|
||||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing space. ", 4),
|
"length of 80 characters.", 3),
|
||||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a tråiling tab.\t", 5),
|
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing space. ", 4),
|
||||||
RuleViolation("B3", "Line contains hard tab characters (\\t)",
|
RuleViolation("B2", "Line has trailing whitespace", "This line has a tråiling tab.\t", 5),
|
||||||
"This line has a tråiling tab.\t", 5)]
|
RuleViolation("B3", "Line contains hard tab characters (\\t)",
|
||||||
|
"This line has a tråiling tab.\t", 5)
|
||||||
|
]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
self.assertListEqual(violations, expected)
|
self.assertListEqual(violations, expected)
|
||||||
|
|
||||||
|
@ -82,26 +88,28 @@ class LintTests(BaseTestCase):
|
||||||
|
|
||||||
title = " Commit title containing 'WIP', \tleading and tråiling whitespace and longer than 72 characters."
|
title = " Commit title containing 'WIP', \tleading and tråiling whitespace and longer than 72 characters."
|
||||||
# expect only certain violations because sample5 has a 'gitlint-ignore: T3, T6, body-max-line-length'
|
# expect only certain violations because sample5 has a 'gitlint-ignore: T3, T6, body-max-line-length'
|
||||||
expected = [RuleViolation("T1", "Title exceeds max length (95>72)", title, 1),
|
expected = [
|
||||||
RuleViolation("T4", "Title contains hard tab characters (\\t)", title, 1),
|
RuleViolation("T1", "Title exceeds max length (95>72)", title, 1),
|
||||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", title, 1),
|
RuleViolation("T4", "Title contains hard tab characters (\\t)", title, 1),
|
||||||
RuleViolation("B4", "Second line is not empty", "This line should be ëmpty", 2),
|
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", title, 1),
|
||||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a tråiling space. ", 4),
|
RuleViolation("B4", "Second line is not empty", "This line should be ëmpty", 2),
|
||||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing tab.\t", 5),
|
RuleViolation("B2", "Line has trailing whitespace", "This line has a tråiling space. ", 4),
|
||||||
RuleViolation("B3", "Line contains hard tab characters (\\t)",
|
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing tab.\t", 5),
|
||||||
"This line has a trailing tab.\t", 5)]
|
RuleViolation("B3", "Line contains hard tab characters (\\t)", "This line has a trailing tab.\t", 5),
|
||||||
|
]
|
||||||
self.assertListEqual(violations, expected)
|
self.assertListEqual(violations, expected)
|
||||||
|
|
||||||
def test_lint_meta(self):
|
def test_lint_meta(self):
|
||||||
""" Lint sample2 but also add some metadata to the commit so we that gets linted as well """
|
"""Lint sample2 but also add some metadata to the commit so we that gets linted as well"""
|
||||||
linter = GitLinter(LintConfig())
|
linter = GitLinter(LintConfig())
|
||||||
gitcontext = self.gitcontext(self.get_sample("commit_message/sample2"))
|
gitcontext = self.gitcontext(self.get_sample("commit_message/sample2"))
|
||||||
gitcontext.commits[0].author_email = "foo bår"
|
gitcontext.commits[0].author_email = "foo bår"
|
||||||
violations = linter.lint(gitcontext.commits[-1])
|
violations = linter.lint(gitcontext.commits[-1])
|
||||||
expected = [RuleViolation("M1", "Author email for commit is invalid", "foo bår", None),
|
expected = [
|
||||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
RuleViolation("M1", "Author email for commit is invalid", "foo bår", None),
|
||||||
"Just a title contåining WIP", 1),
|
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", "Just a title contåining WIP", 1),
|
||||||
RuleViolation("B6", "Body message is missing", None, 3)]
|
RuleViolation("B6", "Body message is missing", None, 3),
|
||||||
|
]
|
||||||
|
|
||||||
self.assertListEqual(violations, expected)
|
self.assertListEqual(violations, expected)
|
||||||
|
|
||||||
|
@ -111,9 +119,10 @@ class LintTests(BaseTestCase):
|
||||||
linter = GitLinter(lint_config)
|
linter = GitLinter(lint_config)
|
||||||
violations = linter.lint(self.gitcommit(self.get_sample("commit_message/sample3")))
|
violations = linter.lint(self.gitcommit(self.get_sample("commit_message/sample3")))
|
||||||
|
|
||||||
expected = [RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
|
expected = [
|
||||||
RuleViolation("B3", "Line contains hard tab characters (\\t)",
|
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
|
||||||
"This line has a tråiling tab.\t", 5)]
|
RuleViolation("B3", "Line contains hard tab characters (\\t)", "This line has a tråiling tab.\t", 5),
|
||||||
|
]
|
||||||
|
|
||||||
self.assertListEqual(violations, expected)
|
self.assertListEqual(violations, expected)
|
||||||
|
|
||||||
|
@ -135,8 +144,9 @@ class LintTests(BaseTestCase):
|
||||||
violations = linter.lint(self.gitcommit(self.get_sample("commit_message/sample2")))
|
violations = linter.lint(self.gitcommit(self.get_sample("commit_message/sample2")))
|
||||||
|
|
||||||
# Normally we'd expect a B6 violation, but that one is skipped because of the specific ignore set above
|
# Normally we'd expect a B6 violation, but that one is skipped because of the specific ignore set above
|
||||||
expected = [RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
expected = [
|
||||||
"Just a title contåining WIP", 1)]
|
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", "Just a title contåining WIP", 1)
|
||||||
|
]
|
||||||
|
|
||||||
self.assertListEqual(violations, expected)
|
self.assertListEqual(violations, expected)
|
||||||
|
|
||||||
|
@ -145,22 +155,25 @@ class LintTests(BaseTestCase):
|
||||||
linter = GitLinter(lint_config)
|
linter = GitLinter(lint_config)
|
||||||
lint_config.set_rule_option("I3", "regex", "(.*)tråiling(.*)")
|
lint_config.set_rule_option("I3", "regex", "(.*)tråiling(.*)")
|
||||||
violations = linter.lint(self.gitcommit(self.get_sample("commit_message/sample1")))
|
violations = linter.lint(self.gitcommit(self.get_sample("commit_message/sample1")))
|
||||||
expected_errors = [RuleViolation("T3", "Title has trailing punctuation (.)",
|
# fmt: off
|
||||||
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
|
expected_errors = [
|
||||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
RuleViolation("T3", "Title has trailing punctuation (.)",
|
||||||
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
|
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
|
||||||
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
|
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
||||||
RuleViolation("B1", "Line exceeds max length (135>80)",
|
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
|
||||||
"This is the first line of the commit message body and it is meant to test " +
|
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
|
||||||
"a line that exceeds the maximum line length of 80 characters.", 3),
|
RuleViolation("B1", "Line exceeds max length (135>80)",
|
||||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing tab.\t", 4),
|
"This is the first line of the commit message body and it is meant to test " +
|
||||||
RuleViolation("B3", "Line contains hard tab characters (\\t)",
|
"a line that exceeds the maximum line length of 80 characters.", 3),
|
||||||
"This line has a trailing tab.\t", 4)]
|
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)
|
||||||
|
]
|
||||||
|
# fmt: on
|
||||||
self.assertListEqual(violations, expected_errors)
|
self.assertListEqual(violations, expected_errors)
|
||||||
|
|
||||||
def test_lint_special_commit(self):
|
def test_lint_special_commit(self):
|
||||||
for commit_type in ["merge", "revert", "squash", "fixup"]:
|
for commit_type in ["merge", "revert", "squash", "fixup", "fixup_amend"]:
|
||||||
commit = self.gitcommit(self.get_sample(f"commit_message/{commit_type}"))
|
commit = self.gitcommit(self.get_sample(f"commit_message/{commit_type}"))
|
||||||
lintconfig = LintConfig()
|
lintconfig = LintConfig()
|
||||||
linter = GitLinter(lintconfig)
|
linter = GitLinter(lintconfig)
|
||||||
|
@ -176,7 +189,7 @@ class LintTests(BaseTestCase):
|
||||||
self.assertTrue(len(violations) > 0)
|
self.assertTrue(len(violations) > 0)
|
||||||
|
|
||||||
def test_lint_regex_rules(self):
|
def test_lint_regex_rules(self):
|
||||||
""" Additional test for title-match-regex, body-match-regex """
|
"""Additional test for title-match-regex, body-match-regex"""
|
||||||
commit = self.gitcommit(self.get_sample("commit_message/no-violations"))
|
commit = self.gitcommit(self.get_sample("commit_message/no-violations"))
|
||||||
lintconfig = LintConfig()
|
lintconfig = LintConfig()
|
||||||
linter = GitLinter(lintconfig)
|
linter = GitLinter(lintconfig)
|
||||||
|
@ -192,46 +205,52 @@ class LintTests(BaseTestCase):
|
||||||
self.assertListEqual(violations, [])
|
self.assertListEqual(violations, [])
|
||||||
|
|
||||||
# Non-matching regexes should return violations
|
# Non-matching regexes should return violations
|
||||||
rule_regexes = [("title-match-regex", ), ("body-match-regex",)]
|
rule_regexes = [("title-match-regex",), ("body-match-regex",)]
|
||||||
lintconfig.set_rule_option("title-match-regex", "regex", "^Tïtle")
|
lintconfig.set_rule_option("title-match-regex", "regex", "^Tïtle")
|
||||||
lintconfig.set_rule_option("body-match-regex", "regex", "Sügned-Off-By: (.*)$")
|
lintconfig.set_rule_option("body-match-regex", "regex", "Sügned-Off-By: (.*)$")
|
||||||
expected_violations = [RuleViolation("T7", "Title does not match regex (^Tïtle)", "Normal Commit Tïtle", 1),
|
expected_violations = [
|
||||||
RuleViolation("B8", "Body does not match regex (Sügned-Off-By: (.*)$)", None, 6)]
|
RuleViolation("T7", "Title does not match regex (^Tïtle)", "Normal Commit Tïtle", 1),
|
||||||
|
RuleViolation("B8", "Body does not match regex (Sügned-Off-By: (.*)$)", None, 6),
|
||||||
|
]
|
||||||
violations = linter.lint(commit)
|
violations = linter.lint(commit)
|
||||||
self.assertListEqual(violations, expected_violations)
|
self.assertListEqual(violations, expected_violations)
|
||||||
|
|
||||||
def test_print_violations(self):
|
def test_print_violations(self):
|
||||||
violations = [RuleViolation("RULE_ID_1", "Error Messåge 1", "Violating Content 1", None),
|
violations = [
|
||||||
RuleViolation("RULE_ID_2", "Error Message 2", "Violåting Content 2", 2)]
|
RuleViolation("RULE_ID_1", "Error Messåge 1", "Violating Content 1", None),
|
||||||
|
RuleViolation("RULE_ID_2", "Error Message 2", "Violåting Content 2", 2),
|
||||||
|
]
|
||||||
linter = GitLinter(LintConfig())
|
linter = GitLinter(LintConfig())
|
||||||
|
|
||||||
# test output with increasing verbosity
|
# test output with increasing verbosity
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
linter.config.verbosity = 0
|
linter.config.verbosity = 0
|
||||||
linter.print_violations(violations)
|
linter.print_violations(violations)
|
||||||
self.assertEqual("", stderr.getvalue())
|
self.assertEqual("", stderr.getvalue())
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
linter.config.verbosity = 1
|
linter.config.verbosity = 1
|
||||||
linter.print_violations(violations)
|
linter.print_violations(violations)
|
||||||
expected = "-: RULE_ID_1\n2: RULE_ID_2\n"
|
expected = "-: RULE_ID_1\n2: RULE_ID_2\n"
|
||||||
self.assertEqual(expected, stderr.getvalue())
|
self.assertEqual(expected, stderr.getvalue())
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
linter.config.verbosity = 2
|
linter.config.verbosity = 2
|
||||||
linter.print_violations(violations)
|
linter.print_violations(violations)
|
||||||
expected = "-: RULE_ID_1 Error Messåge 1\n2: RULE_ID_2 Error Message 2\n"
|
expected = "-: RULE_ID_1 Error Messåge 1\n2: RULE_ID_2 Error Message 2\n"
|
||||||
self.assertEqual(expected, stderr.getvalue())
|
self.assertEqual(expected, stderr.getvalue())
|
||||||
|
|
||||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||||
linter.config.verbosity = 3
|
linter.config.verbosity = 3
|
||||||
linter.print_violations(violations)
|
linter.print_violations(violations)
|
||||||
expected = "-: RULE_ID_1 Error Messåge 1: \"Violating Content 1\"\n" + \
|
expected = (
|
||||||
"2: RULE_ID_2 Error Message 2: \"Violåting Content 2\"\n"
|
'-: RULE_ID_1 Error Messåge 1: "Violating Content 1"\n'
|
||||||
|
+ '2: RULE_ID_2 Error Message 2: "Violåting Content 2"\n'
|
||||||
|
)
|
||||||
self.assertEqual(expected, stderr.getvalue())
|
self.assertEqual(expected, stderr.getvalue())
|
||||||
|
|
||||||
def test_named_rules(self):
|
def test_named_rules(self):
|
||||||
""" Test that when named rules are present, both them and the original (non-named) rules executed """
|
"""Test that when named rules are present, both them and the original (non-named) rules executed"""
|
||||||
|
|
||||||
lint_config = LintConfig()
|
lint_config = LintConfig()
|
||||||
for rule_name in ["my-ïd", "another-rule-ïd"]:
|
for rule_name in ["my-ïd", "another-rule-ïd"]:
|
||||||
|
@ -240,15 +259,15 @@ class LintTests(BaseTestCase):
|
||||||
lint_config.set_rule_option(rule_id, "words", ["Föo"])
|
lint_config.set_rule_option(rule_id, "words", ["Föo"])
|
||||||
linter = GitLinter(lint_config)
|
linter = GitLinter(lint_config)
|
||||||
|
|
||||||
violations = [RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", "WIP: Föo bar", 1),
|
violations = [
|
||||||
RuleViolation("T5:another-rule-ïd", "Title contains the word 'Föo' (case-insensitive)",
|
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", "WIP: Föo bar", 1),
|
||||||
"WIP: Föo bar", 1),
|
RuleViolation("T5:another-rule-ïd", "Title contains the word 'Föo' (case-insensitive)", "WIP: Föo bar", 1),
|
||||||
RuleViolation("T5:my-ïd", "Title contains the word 'Föo' (case-insensitive)",
|
RuleViolation("T5:my-ïd", "Title contains the word 'Föo' (case-insensitive)", "WIP: Föo bar", 1),
|
||||||
"WIP: Föo bar", 1)]
|
]
|
||||||
self.assertListEqual(violations, linter.lint(self.gitcommit("WIP: Föo bar\n\nFoo bår hur dur bla bla")))
|
self.assertListEqual(violations, linter.lint(self.gitcommit("WIP: Föo bar\n\nFoo bår hur dur bla bla")))
|
||||||
|
|
||||||
def test_ignore_named_rules(self):
|
def test_ignore_named_rules(self):
|
||||||
""" Test that named rules can be ignored """
|
"""Test that named rules can be ignored"""
|
||||||
|
|
||||||
# Add named rule to lint config
|
# Add named rule to lint config
|
||||||
config_builder = LintConfigBuilder()
|
config_builder = LintConfigBuilder()
|
||||||
|
@ -259,9 +278,10 @@ class LintTests(BaseTestCase):
|
||||||
commit = self.gitcommit("WIP: Föo bar\n\nFoo bår hur dur bla bla")
|
commit = self.gitcommit("WIP: Föo bar\n\nFoo bår hur dur bla bla")
|
||||||
|
|
||||||
# By default, we expect both the violations of the regular rule as well as the named rule to show up
|
# By default, we expect both the violations of the regular rule as well as the named rule to show up
|
||||||
violations = [RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", "WIP: Föo bar", 1),
|
violations = [
|
||||||
RuleViolation("T5:my-ïd", "Title contains the word 'Föo' (case-insensitive)",
|
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", "WIP: Föo bar", 1),
|
||||||
"WIP: Föo bar", 1)]
|
RuleViolation("T5:my-ïd", "Title contains the word 'Föo' (case-insensitive)", "WIP: Föo bar", 1),
|
||||||
|
]
|
||||||
self.assertListEqual(violations, linter.lint(commit))
|
self.assertListEqual(violations, linter.lint(commit))
|
||||||
|
|
||||||
# ignore regular rule: only named rule violations show up
|
# ignore regular rule: only named rule violations show up
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
@ -9,8 +8,14 @@ from gitlint.options import IntOption, BoolOption, StrOption, ListOption, PathOp
|
||||||
|
|
||||||
class RuleOptionTests(BaseTestCase):
|
class RuleOptionTests(BaseTestCase):
|
||||||
def test_option_equality(self):
|
def test_option_equality(self):
|
||||||
options = {IntOption: 123, StrOption: "foöbar", BoolOption: False, ListOption: ["a", "b"],
|
options = {
|
||||||
PathOption: ".", RegexOption: "^foöbar(.*)"}
|
IntOption: 123,
|
||||||
|
StrOption: "foöbar",
|
||||||
|
BoolOption: False,
|
||||||
|
ListOption: ["a", "b"],
|
||||||
|
PathOption: ".",
|
||||||
|
RegexOption: "^foöbar(.*)",
|
||||||
|
}
|
||||||
for clazz, val in options.items():
|
for clazz, val in options.items():
|
||||||
# 2 options are equal if their name, value and description match
|
# 2 options are equal if their name, value and description match
|
||||||
option1 = clazz("test-öption", val, "Test Dëscription")
|
option1 = clazz("test-öption", val, "Test Dëscription")
|
||||||
|
@ -97,7 +102,7 @@ class RuleOptionTests(BaseTestCase):
|
||||||
self.assertEqual(option.value, True)
|
self.assertEqual(option.value, True)
|
||||||
|
|
||||||
# error on incorrect value
|
# error on incorrect value
|
||||||
incorrect_values = [1, -1, "foo", "bår", ["foo"], {'foo': "bar"}, None]
|
incorrect_values = [1, -1, "foo", "bår", ["foo"], {"foo": "bar"}, None]
|
||||||
for value in incorrect_values:
|
for value in incorrect_values:
|
||||||
with self.assertRaisesMessage(RuleOptionError, "Option 'tëst-name' must be either 'true' or 'false'"):
|
with self.assertRaisesMessage(RuleOptionError, "Option 'tëst-name' must be either 'true' or 'false'"):
|
||||||
option.set(value)
|
option.set(value)
|
||||||
|
@ -197,7 +202,7 @@ class RuleOptionTests(BaseTestCase):
|
||||||
self.assertEqual(option.value, self.get_sample_path())
|
self.assertEqual(option.value, self.get_sample_path())
|
||||||
|
|
||||||
# Expect exception if path type is invalid
|
# Expect exception if path type is invalid
|
||||||
option.type = 'föo'
|
option.type = "föo"
|
||||||
expected = "Option tëst-directory type must be one of: 'file', 'dir', 'both' (current: 'föo')"
|
expected = "Option tëst-directory type must be one of: 'file', 'dir', 'both' (current: 'föo')"
|
||||||
with self.assertRaisesMessage(RuleOptionError, expected):
|
with self.assertRaisesMessage(RuleOptionError, expected):
|
||||||
option.set("haha")
|
option.set("haha")
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from gitlint import utils
|
from gitlint import utils
|
||||||
|
@ -7,13 +5,12 @@ from gitlint.tests.base import BaseTestCase
|
||||||
|
|
||||||
|
|
||||||
class UtilsTests(BaseTestCase):
|
class UtilsTests(BaseTestCase):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
# Since we're messing around with `utils.PLATFORM_IS_WINDOWS` during these tests, we need to reset
|
# Since we're messing around with `utils.PLATFORM_IS_WINDOWS` during these tests, we need to reset
|
||||||
# its value after we're done this doesn't influence other tests
|
# its value after we're done this doesn't influence other tests
|
||||||
utils.PLATFORM_IS_WINDOWS = utils.platform_is_windows()
|
utils.PLATFORM_IS_WINDOWS = utils.platform_is_windows()
|
||||||
|
|
||||||
@patch('os.environ')
|
@patch("os.environ")
|
||||||
def test_use_sh_library(self, patched_env):
|
def test_use_sh_library(self, patched_env):
|
||||||
patched_env.get.return_value = "1"
|
patched_env.get.return_value = "1"
|
||||||
self.assertEqual(utils.use_sh_library(), True)
|
self.assertEqual(utils.use_sh_library(), True)
|
||||||
|
@ -25,15 +22,11 @@ class UtilsTests(BaseTestCase):
|
||||||
self.assertEqual(utils.use_sh_library(), False, invalid_val)
|
self.assertEqual(utils.use_sh_library(), False, invalid_val)
|
||||||
patched_env.get.assert_called_once_with("GITLINT_USE_SH_LIB", None)
|
patched_env.get.assert_called_once_with("GITLINT_USE_SH_LIB", None)
|
||||||
|
|
||||||
# Assert that when GITLINT_USE_SH_LIB is not set, we fallback to checking whether we're on Windows
|
# Assert that when GITLINT_USE_SH_LIB is not set, we fallback to False (not using)
|
||||||
utils.PLATFORM_IS_WINDOWS = True
|
|
||||||
patched_env.get.return_value = None
|
patched_env.get.return_value = None
|
||||||
self.assertEqual(utils.use_sh_library(), False)
|
self.assertEqual(utils.use_sh_library(), False)
|
||||||
|
|
||||||
utils.PLATFORM_IS_WINDOWS = False
|
@patch("gitlint.utils.locale")
|
||||||
self.assertEqual(utils.use_sh_library(), True)
|
|
||||||
|
|
||||||
@patch('gitlint.utils.locale')
|
|
||||||
def test_default_encoding_non_windows(self, mocked_locale):
|
def test_default_encoding_non_windows(self, mocked_locale):
|
||||||
utils.PLATFORM_IS_WINDOWS = False
|
utils.PLATFORM_IS_WINDOWS = False
|
||||||
mocked_locale.getpreferredencoding.return_value = "foöbar"
|
mocked_locale.getpreferredencoding.return_value = "foöbar"
|
||||||
|
@ -43,7 +36,7 @@ class UtilsTests(BaseTestCase):
|
||||||
mocked_locale.getpreferredencoding.return_value = False
|
mocked_locale.getpreferredencoding.return_value = False
|
||||||
self.assertEqual(utils.getpreferredencoding(), "UTF-8")
|
self.assertEqual(utils.getpreferredencoding(), "UTF-8")
|
||||||
|
|
||||||
@patch('os.environ')
|
@patch("os.environ")
|
||||||
def test_default_encoding_windows(self, patched_env):
|
def test_default_encoding_windows(self, patched_env):
|
||||||
utils.PLATFORM_IS_WINDOWS = True
|
utils.PLATFORM_IS_WINDOWS = True
|
||||||
# Mock out os.environ
|
# Mock out os.environ
|
||||||
|
|
|
@ -11,7 +11,7 @@ import locale
|
||||||
# and just executed at import-time.
|
# and just executed at import-time.
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
LOG_FORMAT = '%(levelname)s: %(name)s %(message)s'
|
LOG_FORMAT = "%(levelname)s: %(name)s %(message)s"
|
||||||
|
|
||||||
########################################################################################################################
|
########################################################################################################################
|
||||||
# PLATFORM_IS_WINDOWS
|
# PLATFORM_IS_WINDOWS
|
||||||
|
@ -31,10 +31,10 @@ PLATFORM_IS_WINDOWS = platform_is_windows()
|
||||||
|
|
||||||
|
|
||||||
def use_sh_library():
|
def use_sh_library():
|
||||||
gitlint_use_sh_lib_env = os.environ.get('GITLINT_USE_SH_LIB', None)
|
gitlint_use_sh_lib_env = os.environ.get("GITLINT_USE_SH_LIB", None)
|
||||||
if gitlint_use_sh_lib_env:
|
if gitlint_use_sh_lib_env:
|
||||||
return gitlint_use_sh_lib_env == "1"
|
return gitlint_use_sh_lib_env == "1"
|
||||||
return not PLATFORM_IS_WINDOWS
|
return False
|
||||||
|
|
||||||
|
|
||||||
USE_SH_LIB = use_sh_library()
|
USE_SH_LIB = use_sh_library()
|
||||||
|
@ -44,8 +44,8 @@ USE_SH_LIB = use_sh_library()
|
||||||
|
|
||||||
|
|
||||||
def getpreferredencoding():
|
def getpreferredencoding():
|
||||||
""" Modified version of local.getpreferredencoding() that takes into account LC_ALL, LC_CTYPE, LANG env vars
|
"""Modified version of local.getpreferredencoding() that takes into account LC_ALL, LC_CTYPE, LANG env vars
|
||||||
on windows and falls back to UTF-8. """
|
on windows and falls back to UTF-8."""
|
||||||
fallback_encoding = "UTF-8"
|
fallback_encoding = "UTF-8"
|
||||||
default_encoding = locale.getpreferredencoding() or fallback_encoding
|
default_encoding = locale.getpreferredencoding() or fallback_encoding
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ def getpreferredencoding():
|
||||||
# If encoding contains a dot: split and use second part, otherwise use everything
|
# If encoding contains a dot: split and use second part, otherwise use everything
|
||||||
dot_index = encoding.find(".")
|
dot_index = encoding.find(".")
|
||||||
if dot_index != -1:
|
if dot_index != -1:
|
||||||
default_encoding = encoding[dot_index + 1:]
|
default_encoding = encoding[dot_index + 1 :]
|
||||||
else:
|
else:
|
||||||
default_encoding = encoding
|
default_encoding = encoding
|
||||||
break
|
break
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
from __future__ import print_function
|
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
import io
|
import io
|
||||||
import re
|
import re
|
||||||
|
@ -32,7 +31,7 @@ Source code on `github.com/jorisroovers/gitlint`_.
|
||||||
# shamelessly stolen from mkdocs' setup.py: https://github.com/mkdocs/mkdocs/blob/master/setup.py
|
# shamelessly stolen from mkdocs' setup.py: https://github.com/mkdocs/mkdocs/blob/master/setup.py
|
||||||
def get_version(package):
|
def get_version(package):
|
||||||
"""Return package version as listed in `__version__` in `init.py`."""
|
"""Return package version as listed in `__version__` in `init.py`."""
|
||||||
init_py = io.open(os.path.join(package, '__init__.py'), encoding="UTF-8").read()
|
init_py = open(os.path.join(package, "__init__.py"), encoding="UTF-8").read()
|
||||||
return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
|
return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,38 +49,37 @@ setup(
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"Topic :: Software Development :: Quality Assurance",
|
"Topic :: Software Development :: Quality Assurance",
|
||||||
"Topic :: Software Development :: Testing",
|
"Topic :: Software Development :: Testing",
|
||||||
"License :: OSI Approved :: MIT License"
|
"License :: OSI Approved :: MIT License",
|
||||||
],
|
],
|
||||||
python_requires=">=3.6",
|
python_requires=">=3.6",
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'Click>=8',
|
"Click>=8",
|
||||||
'arrow>=1',
|
"arrow>=1",
|
||||||
'sh>=1.13.0 ; sys_platform != "win32"',
|
'sh>=1.13.0 ; sys_platform != "win32"',
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
'trusted-deps': [
|
"trusted-deps": [
|
||||||
'Click==8.0.3',
|
"Click==8.0.3",
|
||||||
'arrow==1.2.1',
|
"arrow==1.2.1",
|
||||||
'sh==1.14.2 ; sys_platform != "win32"',
|
'sh==1.14.2 ; sys_platform != "win32"',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
keywords='gitlint git lint',
|
keywords="gitlint git lint",
|
||||||
author='Joris Roovers',
|
author="Joris Roovers",
|
||||||
url='https://jorisroovers.github.io/gitlint',
|
url="https://jorisroovers.github.io/gitlint",
|
||||||
project_urls={
|
project_urls={
|
||||||
'Documentation': 'https://jorisroovers.github.io/gitlint',
|
"Documentation": "https://jorisroovers.github.io/gitlint",
|
||||||
'Source': 'https://github.com/jorisroovers/gitlint',
|
"Source": "https://github.com/jorisroovers/gitlint",
|
||||||
},
|
|
||||||
license='MIT',
|
|
||||||
package_data={
|
|
||||||
'gitlint': ['files/*']
|
|
||||||
},
|
},
|
||||||
|
license="MIT",
|
||||||
|
package_data={"gitlint": ["files/*"]},
|
||||||
packages=find_packages(exclude=["examples"]),
|
packages=find_packages(exclude=["examples"]),
|
||||||
entry_points={
|
entry_points={
|
||||||
"console_scripts": [
|
"console_scripts": [
|
||||||
|
@ -92,16 +90,20 @@ setup(
|
||||||
|
|
||||||
# Print a red deprecation warning for python < 3.6 users
|
# Print a red deprecation warning for python < 3.6 users
|
||||||
if sys.version_info[:2] < (3, 6):
|
if sys.version_info[:2] < (3, 6):
|
||||||
msg = "\033[31mDEPRECATION: You're using a python version that has reached end-of-life. " + \
|
msg = (
|
||||||
"Gitlint does not support Python < 3.6" + \
|
"\033[31mDEPRECATION: You're using a python version that has reached end-of-life. "
|
||||||
"Please upgrade your Python to 3.6 or above.\033[0m"
|
+ "Gitlint does not support Python < 3.6"
|
||||||
|
+ "Please upgrade your Python to 3.6 or above.\033[0m"
|
||||||
|
)
|
||||||
print(msg)
|
print(msg)
|
||||||
|
|
||||||
# Print a warning message for Windows users
|
# Print a warning message for Windows users
|
||||||
PLATFORM_IS_WINDOWS = "windows" in platform.system().lower()
|
PLATFORM_IS_WINDOWS = "windows" in platform.system().lower()
|
||||||
if PLATFORM_IS_WINDOWS:
|
if PLATFORM_IS_WINDOWS:
|
||||||
msg = "\n\n\n\n\n****************\n" + \
|
msg = (
|
||||||
"WARNING: Gitlint support for Windows is still experimental and there are some known issues: " + \
|
"\n\n\n\n\n****************\n"
|
||||||
"https://github.com/jorisroovers/gitlint/issues?q=is%3Aissue+is%3Aopen+label%3Awindows " + \
|
+ "WARNING: Gitlint support for Windows is still experimental and there are some known issues: "
|
||||||
"\n*******************"
|
+ "https://github.com/jorisroovers/gitlint/issues?q=is%3Aissue+is%3Aopen+label%3Awindows "
|
||||||
|
+ "\n*******************"
|
||||||
|
)
|
||||||
print(msg)
|
print(msg)
|
||||||
|
|
5
pyproject.toml
Normal file
5
pyproject.toml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
[tool.black]
|
||||||
|
target_version = ['py36', 'py37', 'py38','py39','py310']
|
||||||
|
line-length = 120
|
||||||
|
# extend-exclude: keep excluding files from .gitignore in addition to the ones specified
|
||||||
|
extend-exclude = "gitlint-core/gitlint/tests/samples/user_rules/import_exception/invalid_python.py"
|
97
qa/base.py
97
qa/base.py
|
@ -1,8 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# pylint: disable=bad-option-value,unidiomatic-typecheck,undefined-variable,no-else-return,
|
# pylint: disable=bad-option-value,unidiomatic-typecheck,undefined-variable,no-else-return,
|
||||||
# pylint: disable=too-many-function-args,unexpected-keyword-arg
|
# pylint: disable=too-many-function-args,unexpected-keyword-arg
|
||||||
|
|
||||||
import io
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
|
@ -20,8 +18,8 @@ from qa.utils import DEFAULT_ENCODING
|
||||||
|
|
||||||
|
|
||||||
class BaseTestCase(TestCase):
|
class BaseTestCase(TestCase):
|
||||||
""" Base class of which all gitlint integration test classes are derived.
|
"""Base class of which all gitlint integration test classes are derived.
|
||||||
Provides a number of convenience methods. """
|
Provides a number of convenience methods."""
|
||||||
|
|
||||||
# In case of assert failures, print the full error message
|
# In case of assert failures, print the full error message
|
||||||
maxDiff = None
|
maxDiff = None
|
||||||
|
@ -32,7 +30,7 @@ class BaseTestCase(TestCase):
|
||||||
GITLINT_USAGE_ERROR = 253
|
GITLINT_USAGE_ERROR = 253
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
""" Sets up the integration tests by creating a new temporary git repository """
|
"""Sets up the integration tests by creating a new temporary git repository"""
|
||||||
self.tmpfiles = []
|
self.tmpfiles = []
|
||||||
self.tmp_git_repos = []
|
self.tmp_git_repos = []
|
||||||
self.tmp_git_repo = self.create_tmp_git_repo()
|
self.tmp_git_repo = self.create_tmp_git_repo()
|
||||||
|
@ -47,7 +45,7 @@ class BaseTestCase(TestCase):
|
||||||
def assertEqualStdout(self, output, expected): # pylint: disable=invalid-name
|
def assertEqualStdout(self, output, expected): # pylint: disable=invalid-name
|
||||||
self.assertIsInstance(output, RunningCommand)
|
self.assertIsInstance(output, RunningCommand)
|
||||||
output = output.stdout.decode(DEFAULT_ENCODING)
|
output = output.stdout.decode(DEFAULT_ENCODING)
|
||||||
output = output.replace('\r', '')
|
output = output.replace("\r", "")
|
||||||
self.assertMultiLineEqual(output, expected)
|
self.assertMultiLineEqual(output, expected)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -56,11 +54,11 @@ class BaseTestCase(TestCase):
|
||||||
return os.path.realpath(f"/tmp/gitlint-test-{timestamp}")
|
return os.path.realpath(f"/tmp/gitlint-test-{timestamp}")
|
||||||
|
|
||||||
def create_tmp_git_repo(self):
|
def create_tmp_git_repo(self):
|
||||||
""" Creates a temporary git repository and returns its directory path """
|
"""Creates a temporary git repository and returns its directory path"""
|
||||||
tmp_git_repo = self.generate_temp_path()
|
tmp_git_repo = self.generate_temp_path()
|
||||||
self.tmp_git_repos.append(tmp_git_repo)
|
self.tmp_git_repos.append(tmp_git_repo)
|
||||||
|
|
||||||
git("init", tmp_git_repo)
|
git("init", "--initial-branch", "main", tmp_git_repo)
|
||||||
# configuring name and email is required in every git repot
|
# configuring name and email is required in every git repot
|
||||||
git("config", "user.name", "gitlint-test-user", _cwd=tmp_git_repo)
|
git("config", "user.name", "gitlint-test-user", _cwd=tmp_git_repo)
|
||||||
git("config", "user.email", "gitlint@test.com", _cwd=tmp_git_repo)
|
git("config", "user.email", "gitlint@test.com", _cwd=tmp_git_repo)
|
||||||
|
@ -77,29 +75,43 @@ class BaseTestCase(TestCase):
|
||||||
return tmp_git_repo
|
return tmp_git_repo
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_file(parent_dir):
|
def create_file(parent_dir, content=None):
|
||||||
""" Creates a file inside a passed directory. Returns filename."""
|
"""Creates a file inside a passed directory. Returns filename."""
|
||||||
test_filename = "test-fïle-" + str(uuid4())
|
test_filename = "test-fïle-" + str(uuid4())
|
||||||
# pylint: disable=consider-using-with
|
full_path = os.path.join(parent_dir, test_filename)
|
||||||
io.open(os.path.join(parent_dir, test_filename), 'a', encoding=DEFAULT_ENCODING).close()
|
|
||||||
|
if content:
|
||||||
|
if isinstance(content, bytes):
|
||||||
|
open_kwargs = {"mode": "wb"}
|
||||||
|
else:
|
||||||
|
open_kwargs = {"mode": "w", "encoding": DEFAULT_ENCODING}
|
||||||
|
|
||||||
|
with open(full_path, **open_kwargs) as f: # pylint: disable=unspecified-encoding
|
||||||
|
f.write(content)
|
||||||
|
else:
|
||||||
|
# pylint: disable=consider-using-with
|
||||||
|
open(full_path, "a", encoding=DEFAULT_ENCODING).close()
|
||||||
|
|
||||||
return test_filename
|
return test_filename
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_environment(envvars=None):
|
def create_environment(envvars=None):
|
||||||
""" Creates a copy of the current os.environ and adds/overwrites a given set of variables to it """
|
"""Creates a copy of the current os.environ and adds/overwrites a given set of variables to it"""
|
||||||
environment = os.environ.copy()
|
environment = os.environ.copy()
|
||||||
if envvars:
|
if envvars:
|
||||||
environment.update(envvars)
|
environment.update(envvars)
|
||||||
return environment
|
return environment
|
||||||
|
|
||||||
def create_tmp_git_config(self, contents):
|
def create_tmp_git_config(self, contents):
|
||||||
""" Creates an environment with the GIT_CONFIG variable set to a file with the given contents. """
|
"""Creates an environment with the GIT_CONFIG variable set to a file with the given contents."""
|
||||||
tmp_config = self.create_tmpfile(contents)
|
tmp_config = self.create_tmpfile(contents)
|
||||||
return self.create_environment({"GIT_CONFIG": tmp_config})
|
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):
|
def create_simple_commit(
|
||||||
""" Creates a simple commit with an empty test file.
|
self, message, *, file_contents=None, out=None, ok_code=None, env=None, git_repo=None, tty_in=False
|
||||||
:param message: Commit message for the commit. """
|
):
|
||||||
|
"""Creates a simple commit with an empty test file.
|
||||||
|
:param message: Commit message for the commit."""
|
||||||
|
|
||||||
git_repo = self.tmp_git_repo if git_repo is None else git_repo
|
git_repo = self.tmp_git_repo if git_repo is None else git_repo
|
||||||
|
|
||||||
|
@ -110,23 +122,39 @@ class BaseTestCase(TestCase):
|
||||||
environment = self.create_environment(env)
|
environment = self.create_environment(env)
|
||||||
|
|
||||||
# Create file and add to git
|
# Create file and add to git
|
||||||
test_filename = self.create_file(git_repo)
|
test_filename = self.create_file(git_repo, file_contents)
|
||||||
git("add", test_filename, _cwd=git_repo)
|
git("add", test_filename, _cwd=git_repo)
|
||||||
# https://amoffat.github.io/sh/#interactive-callbacks
|
# https://amoffat.github.io/sh/#interactive-callbacks
|
||||||
if not ok_code:
|
if not ok_code:
|
||||||
ok_code = [0]
|
ok_code = [0]
|
||||||
|
|
||||||
git("commit", "-m", message, _cwd=git_repo, _err_to_out=True, _out=out, _tty_in=tty_in,
|
git(
|
||||||
_ok_code=ok_code, _env=environment)
|
"commit",
|
||||||
|
"-m",
|
||||||
|
message,
|
||||||
|
_cwd=git_repo,
|
||||||
|
_err_to_out=True,
|
||||||
|
_out=out,
|
||||||
|
_tty_in=tty_in,
|
||||||
|
_ok_code=ok_code,
|
||||||
|
_env=environment,
|
||||||
|
)
|
||||||
return test_filename
|
return test_filename
|
||||||
|
|
||||||
def create_tmpfile(self, content):
|
def create_tmpfile(self, content):
|
||||||
""" Utility method to create temp files. These are cleaned at the end of the test """
|
"""Utility method to create temp files. These are cleaned at the end of the test"""
|
||||||
# Not using a context manager to avoid unneccessary identation in test code
|
# Not using a context manager to avoid unnecessary indentation in test code
|
||||||
tmpfile, tmpfilepath = tempfile.mkstemp()
|
tmpfile, tmpfilepath = tempfile.mkstemp()
|
||||||
self.tmpfiles.append(tmpfilepath)
|
self.tmpfiles.append(tmpfilepath)
|
||||||
with io.open(tmpfile, "w", encoding=DEFAULT_ENCODING) as f:
|
|
||||||
|
if isinstance(content, bytes):
|
||||||
|
open_kwargs = {"mode": "wb"}
|
||||||
|
else:
|
||||||
|
open_kwargs = {"mode": "w", "encoding": DEFAULT_ENCODING}
|
||||||
|
|
||||||
|
with open(tmpfile, **open_kwargs) as f: # pylint: disable=unspecified-encoding
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
return tmpfilepath
|
return tmpfilepath
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -149,11 +177,11 @@ class BaseTestCase(TestCase):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_expected(filename="", variable_dict=None):
|
def get_expected(filename="", variable_dict=None):
|
||||||
""" Utility method to read an 'expected' file and return it as a string. Optionally replace template variables
|
"""Utility method to read an 'expected' file and return it as a string. Optionally replace template variables
|
||||||
specified by variable_dict. """
|
specified by variable_dict."""
|
||||||
expected_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "expected")
|
expected_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "expected")
|
||||||
expected_path = os.path.join(expected_dir, filename)
|
expected_path = os.path.join(expected_dir, filename)
|
||||||
with io.open(expected_path, encoding=DEFAULT_ENCODING) as file:
|
with open(expected_path, encoding=DEFAULT_ENCODING) as file:
|
||||||
expected = file.read()
|
expected = file.read()
|
||||||
|
|
||||||
if variable_dict:
|
if variable_dict:
|
||||||
|
@ -162,20 +190,25 @@ class BaseTestCase(TestCase):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_system_info_dict():
|
def get_system_info_dict():
|
||||||
""" Returns a dict with items related to system values logged by `gitlint --debug` """
|
"""Returns a dict with items related to system values logged by `gitlint --debug`"""
|
||||||
expected_gitlint_version = gitlint("--version").replace("gitlint, version ", "").strip()
|
expected_gitlint_version = gitlint("--version").replace("gitlint, version ", "").strip()
|
||||||
expected_git_version = git("--version").strip()
|
expected_git_version = git("--version").strip()
|
||||||
return {'platform': platform.platform(), 'python_version': sys.version,
|
return {
|
||||||
'git_version': expected_git_version, 'gitlint_version': expected_gitlint_version,
|
"platform": platform.platform(),
|
||||||
'GITLINT_USE_SH_LIB': BaseTestCase.GITLINT_USE_SH_LIB, 'DEFAULT_ENCODING': DEFAULT_ENCODING}
|
"python_version": sys.version,
|
||||||
|
"git_version": expected_git_version,
|
||||||
|
"gitlint_version": expected_gitlint_version,
|
||||||
|
"GITLINT_USE_SH_LIB": BaseTestCase.GITLINT_USE_SH_LIB,
|
||||||
|
"DEFAULT_ENCODING": DEFAULT_ENCODING,
|
||||||
|
}
|
||||||
|
|
||||||
def get_debug_vars_last_commit(self, git_repo=None):
|
def get_debug_vars_last_commit(self, git_repo=None):
|
||||||
""" Returns a dict with items related to `gitlint --debug` output for the last commit. """
|
"""Returns a dict with items related to `gitlint --debug` output for the last commit."""
|
||||||
target_repo = git_repo if git_repo else self.tmp_git_repo
|
target_repo = git_repo if git_repo else self.tmp_git_repo
|
||||||
commit_sha = self.get_last_commit_hash(git_repo=target_repo)
|
commit_sha = self.get_last_commit_hash(git_repo=target_repo)
|
||||||
expected_date = git("log", "-1", "--pretty=%ai", _tty_out=False, _cwd=target_repo)
|
expected_date = git("log", "-1", "--pretty=%ai", _tty_out=False, _cwd=target_repo)
|
||||||
expected_date = arrow.get(str(expected_date), "YYYY-MM-DD HH:mm:ss Z").format("YYYY-MM-DD HH:mm:ss Z")
|
expected_date = arrow.get(str(expected_date), "YYYY-MM-DD HH:mm:ss Z").format("YYYY-MM-DD HH:mm:ss Z")
|
||||||
|
|
||||||
expected_kwargs = self.get_system_info_dict()
|
expected_kwargs = self.get_system_info_dict()
|
||||||
expected_kwargs.update({'target': target_repo, 'commit_sha': commit_sha, 'commit_date': expected_date})
|
expected_kwargs.update({"target": target_repo, "commit_sha": commit_sha, "commit_date": expected_date})
|
||||||
return expected_kwargs
|
return expected_kwargs
|
||||||
|
|
11
qa/expected/test_commits/test_csv_hash_list_1
Normal file
11
qa/expected/test_commits/test_csv_hash_list_1
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
Commit {commit_sha2}:
|
||||||
|
1: T3 Title has trailing punctuation (.): "Sïmple title2."
|
||||||
|
3: B6 Body message is missing
|
||||||
|
|
||||||
|
Commit {commit_sha1}:
|
||||||
|
1: T3 Title has trailing punctuation (.): "Sïmple title1."
|
||||||
|
3: B6 Body message is missing
|
||||||
|
|
||||||
|
Commit {commit_sha4}:
|
||||||
|
1: T3 Title has trailing punctuation (.): "Sïmple title4."
|
||||||
|
3: B6 Body message is missing
|
|
@ -1,3 +1,5 @@
|
||||||
|
WARNING: I1 - ignore-by-title: gitlint will be switching from using Python regex 'match' (match beginning) to 'search' (match anywhere) semantics. Please review your ignore-by-title.regex option accordingly. To remove this warning, set general.regex-style-search=True. More details: https://jorisroovers.github.io/gitlint/configuration/#regex-style-search
|
||||||
|
WARNING: I2 - ignore-by-body: gitlint will be switching from using Python regex 'match' (match beginning) to 'search' (match anywhere) semantics. Please review your ignore-by-body.regex option accordingly. To remove this warning, set general.regex-style-search=True. More details: https://jorisroovers.github.io/gitlint/configuration/#regex-style-search
|
||||||
Commit {commit_sha0}:
|
Commit {commit_sha0}:
|
||||||
1: T3 Title has trailing punctuation (.): "Sïmple title4."
|
1: T3 Title has trailing punctuation (.): "Sïmple title4."
|
||||||
|
|
||||||
|
|
|
@ -14,11 +14,13 @@ contrib: []
|
||||||
ignore:
|
ignore:
|
||||||
ignore-merge-commits: True
|
ignore-merge-commits: True
|
||||||
ignore-fixup-commits: True
|
ignore-fixup-commits: True
|
||||||
|
ignore-fixup-amend-commits: True
|
||||||
ignore-squash-commits: True
|
ignore-squash-commits: True
|
||||||
ignore-revert-commits: True
|
ignore-revert-commits: True
|
||||||
ignore-stdin: False
|
ignore-stdin: False
|
||||||
staged: True
|
staged: True
|
||||||
fail-without-commits: False
|
fail-without-commits: False
|
||||||
|
regex-style-search: False
|
||||||
verbosity: 3
|
verbosity: 3
|
||||||
debug: True
|
debug: True
|
||||||
target: {target}
|
target: {target}
|
||||||
|
@ -60,17 +62,17 @@ target: {target}
|
||||||
B8: body-match-regex
|
B8: body-match-regex
|
||||||
regex=None
|
regex=None
|
||||||
M1: author-valid-email
|
M1: author-valid-email
|
||||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||||
|
|
||||||
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
|
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
|
||||||
DEBUG: gitlint.cli Using --msg-filename.
|
DEBUG: gitlint.cli Using --msg-filename.
|
||||||
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
||||||
DEBUG: gitlint.cli Linting 1 commit(s)
|
DEBUG: gitlint.cli Linting 1 commit(s)
|
||||||
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
|
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
|
||||||
|
DEBUG: gitlint.git ('diff', '--staged', '--numstat', '-r')
|
||||||
DEBUG: gitlint.git ('config', '--get', 'user.name')
|
DEBUG: gitlint.git ('config', '--get', 'user.name')
|
||||||
DEBUG: gitlint.git ('config', '--get', 'user.email')
|
DEBUG: gitlint.git ('config', '--get', 'user.email')
|
||||||
DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
|
DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
|
||||||
DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r')
|
|
||||||
DEBUG: gitlint.lint Commit Object
|
DEBUG: gitlint.lint Commit Object
|
||||||
--- Commit Message ----
|
--- Commit Message ----
|
||||||
WIP: from fïle test.
|
WIP: from fïle test.
|
||||||
|
@ -79,10 +81,14 @@ Author: gitlint-test-user <gitlint@test.com>
|
||||||
Date: {staged_date}
|
Date: {staged_date}
|
||||||
is-merge-commit: False
|
is-merge-commit: False
|
||||||
is-fixup-commit: False
|
is-fixup-commit: False
|
||||||
|
is-fixup-amend-commit: False
|
||||||
is-squash-commit: False
|
is-squash-commit: False
|
||||||
is-revert-commit: False
|
is-revert-commit: False
|
||||||
Branches: ['master']
|
Parents: []
|
||||||
|
Branches: ['main']
|
||||||
Changed Files: {changed_files}
|
Changed Files: {changed_files}
|
||||||
|
Changed Files Stats:
|
||||||
|
{changed_files_stats}
|
||||||
-----------------------
|
-----------------------
|
||||||
1: T3 Title has trailing punctuation (.): "WIP: from fïle test."
|
1: T3 Title has trailing punctuation (.): "WIP: from fïle test."
|
||||||
1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: from fïle test."
|
1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: from fïle test."
|
||||||
|
|
|
@ -14,11 +14,13 @@ contrib: []
|
||||||
ignore:
|
ignore:
|
||||||
ignore-merge-commits: True
|
ignore-merge-commits: True
|
||||||
ignore-fixup-commits: True
|
ignore-fixup-commits: True
|
||||||
|
ignore-fixup-amend-commits: True
|
||||||
ignore-squash-commits: True
|
ignore-squash-commits: True
|
||||||
ignore-revert-commits: True
|
ignore-revert-commits: True
|
||||||
ignore-stdin: False
|
ignore-stdin: False
|
||||||
staged: True
|
staged: True
|
||||||
fail-without-commits: False
|
fail-without-commits: False
|
||||||
|
regex-style-search: False
|
||||||
verbosity: 3
|
verbosity: 3
|
||||||
debug: True
|
debug: True
|
||||||
target: {target}
|
target: {target}
|
||||||
|
@ -60,7 +62,7 @@ target: {target}
|
||||||
B8: body-match-regex
|
B8: body-match-regex
|
||||||
regex=None
|
regex=None
|
||||||
M1: author-valid-email
|
M1: author-valid-email
|
||||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||||
|
|
||||||
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
|
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
|
||||||
DEBUG: gitlint.cli Stdin data: 'WIP: Pïpe test.
|
DEBUG: gitlint.cli Stdin data: 'WIP: Pïpe test.
|
||||||
|
@ -69,10 +71,10 @@ DEBUG: gitlint.cli Stdin detected and not ignored. Using as input.
|
||||||
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
||||||
DEBUG: gitlint.cli Linting 1 commit(s)
|
DEBUG: gitlint.cli Linting 1 commit(s)
|
||||||
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
|
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
|
||||||
|
DEBUG: gitlint.git ('diff', '--staged', '--numstat', '-r')
|
||||||
DEBUG: gitlint.git ('config', '--get', 'user.name')
|
DEBUG: gitlint.git ('config', '--get', 'user.name')
|
||||||
DEBUG: gitlint.git ('config', '--get', 'user.email')
|
DEBUG: gitlint.git ('config', '--get', 'user.email')
|
||||||
DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
|
DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
|
||||||
DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r')
|
|
||||||
DEBUG: gitlint.lint Commit Object
|
DEBUG: gitlint.lint Commit Object
|
||||||
--- Commit Message ----
|
--- Commit Message ----
|
||||||
WIP: Pïpe test.
|
WIP: Pïpe test.
|
||||||
|
@ -81,10 +83,14 @@ Author: gitlint-test-user <gitlint@test.com>
|
||||||
Date: {staged_date}
|
Date: {staged_date}
|
||||||
is-merge-commit: False
|
is-merge-commit: False
|
||||||
is-fixup-commit: False
|
is-fixup-commit: False
|
||||||
|
is-fixup-amend-commit: False
|
||||||
is-squash-commit: False
|
is-squash-commit: False
|
||||||
is-revert-commit: False
|
is-revert-commit: False
|
||||||
Branches: ['master']
|
Parents: []
|
||||||
|
Branches: ['main']
|
||||||
Changed Files: {changed_files}
|
Changed Files: {changed_files}
|
||||||
|
Changed Files Stats:
|
||||||
|
{changed_files_stats}
|
||||||
-----------------------
|
-----------------------
|
||||||
1: T3 Title has trailing punctuation (.): "WIP: Pïpe test."
|
1: T3 Title has trailing punctuation (.): "WIP: Pïpe test."
|
||||||
1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Pïpe test."
|
1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Pïpe test."
|
||||||
|
|
|
@ -14,11 +14,13 @@ contrib: ['CC1', 'CT1']
|
||||||
ignore: T1,T2
|
ignore: T1,T2
|
||||||
ignore-merge-commits: True
|
ignore-merge-commits: True
|
||||||
ignore-fixup-commits: True
|
ignore-fixup-commits: True
|
||||||
|
ignore-fixup-amend-commits: True
|
||||||
ignore-squash-commits: True
|
ignore-squash-commits: True
|
||||||
ignore-revert-commits: True
|
ignore-revert-commits: True
|
||||||
ignore-stdin: True
|
ignore-stdin: True
|
||||||
staged: False
|
staged: False
|
||||||
fail-without-commits: True
|
fail-without-commits: True
|
||||||
|
regex-style-search: False
|
||||||
verbosity: 2
|
verbosity: 2
|
||||||
debug: True
|
debug: True
|
||||||
target: {target}
|
target: {target}
|
||||||
|
@ -60,7 +62,7 @@ target: {target}
|
||||||
B8: body-match-regex
|
B8: body-match-regex
|
||||||
regex=None
|
regex=None
|
||||||
M1: author-valid-email
|
M1: author-valid-email
|
||||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||||
CC1: contrib-body-requires-signed-off-by
|
CC1: contrib-body-requires-signed-off-by
|
||||||
CT1: contrib-title-conventional-commits
|
CT1: contrib-title-conventional-commits
|
||||||
types=fix,feat,chore,docs,style,refactor,perf,test,revert,ci,build
|
types=fix,feat,chore,docs,style,refactor,perf,test,revert,ci,build
|
||||||
|
@ -71,8 +73,8 @@ 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 ('log', '{commit_sha}', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B')
|
||||||
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
||||||
DEBUG: gitlint.lint Linting commit {commit_sha}
|
DEBUG: gitlint.lint Linting commit {commit_sha}
|
||||||
|
DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--numstat', '-r', '--root', '{commit_sha}')
|
||||||
DEBUG: gitlint.git ('branch', '--contains', '{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
|
DEBUG: gitlint.lint Commit Object
|
||||||
--- Commit Message ----
|
--- Commit Message ----
|
||||||
WIP: Thïs is a title thåt is a bit longer.
|
WIP: Thïs is a title thåt is a bit longer.
|
||||||
|
@ -84,10 +86,14 @@ Author: gitlint-test-user <gitlint@test.com>
|
||||||
Date: {commit_date}
|
Date: {commit_date}
|
||||||
is-merge-commit: False
|
is-merge-commit: False
|
||||||
is-fixup-commit: False
|
is-fixup-commit: False
|
||||||
|
is-fixup-amend-commit: False
|
||||||
is-squash-commit: False
|
is-squash-commit: False
|
||||||
is-revert-commit: False
|
is-revert-commit: False
|
||||||
Branches: ['master']
|
Parents: []
|
||||||
|
Branches: ['main']
|
||||||
Changed Files: {changed_files}
|
Changed Files: {changed_files}
|
||||||
|
Changed Files Stats:
|
||||||
|
{changed_files_stats}
|
||||||
-----------------------
|
-----------------------
|
||||||
1: CC1 Body does not contain a 'Signed-off-by' line
|
1: CC1 Body does not contain a 'Signed-off-by' line
|
||||||
1: CT1 Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert, ci, build
|
1: CT1 Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert, ci, build
|
||||||
|
|
|
@ -14,11 +14,13 @@ contrib: []
|
||||||
ignore:
|
ignore:
|
||||||
ignore-merge-commits: True
|
ignore-merge-commits: True
|
||||||
ignore-fixup-commits: True
|
ignore-fixup-commits: True
|
||||||
|
ignore-fixup-amend-commits: True
|
||||||
ignore-squash-commits: True
|
ignore-squash-commits: True
|
||||||
ignore-revert-commits: True
|
ignore-revert-commits: True
|
||||||
ignore-stdin: False
|
ignore-stdin: False
|
||||||
staged: True
|
staged: True
|
||||||
fail-without-commits: False
|
fail-without-commits: False
|
||||||
|
regex-style-search: False
|
||||||
verbosity: 0
|
verbosity: 0
|
||||||
debug: True
|
debug: True
|
||||||
target: {target}
|
target: {target}
|
||||||
|
@ -60,17 +62,17 @@ target: {target}
|
||||||
B8: body-match-regex
|
B8: body-match-regex
|
||||||
regex=None
|
regex=None
|
||||||
M1: author-valid-email
|
M1: author-valid-email
|
||||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||||
|
|
||||||
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
|
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
|
||||||
DEBUG: gitlint.cli Using --msg-filename.
|
DEBUG: gitlint.cli Using --msg-filename.
|
||||||
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
||||||
DEBUG: gitlint.cli Linting 1 commit(s)
|
DEBUG: gitlint.cli Linting 1 commit(s)
|
||||||
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
|
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
|
||||||
|
DEBUG: gitlint.git ('diff', '--staged', '--numstat', '-r')
|
||||||
DEBUG: gitlint.git ('config', '--get', 'user.name')
|
DEBUG: gitlint.git ('config', '--get', 'user.name')
|
||||||
DEBUG: gitlint.git ('config', '--get', 'user.email')
|
DEBUG: gitlint.git ('config', '--get', 'user.email')
|
||||||
DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
|
DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
|
||||||
DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r')
|
|
||||||
DEBUG: gitlint.lint Commit Object
|
DEBUG: gitlint.lint Commit Object
|
||||||
--- Commit Message ----
|
--- Commit Message ----
|
||||||
WIP: msg-fïlename test.
|
WIP: msg-fïlename test.
|
||||||
|
@ -79,9 +81,12 @@ Author: gitlint-test-user <gitlint@test.com>
|
||||||
Date: {date}
|
Date: {date}
|
||||||
is-merge-commit: False
|
is-merge-commit: False
|
||||||
is-fixup-commit: False
|
is-fixup-commit: False
|
||||||
|
is-fixup-amend-commit: False
|
||||||
is-squash-commit: False
|
is-squash-commit: False
|
||||||
is-revert-commit: False
|
is-revert-commit: False
|
||||||
Branches: ['master']
|
Parents: []
|
||||||
|
Branches: ['main']
|
||||||
Changed Files: []
|
Changed Files: []
|
||||||
|
Changed Files Stats: {{}}
|
||||||
-----------------------
|
-----------------------
|
||||||
DEBUG: gitlint.cli Exit Code = 3
|
DEBUG: gitlint.cli Exit Code = 3
|
||||||
|
|
|
@ -14,11 +14,13 @@ contrib: []
|
||||||
ignore: title-trailing-punctuation,B2
|
ignore: title-trailing-punctuation,B2
|
||||||
ignore-merge-commits: True
|
ignore-merge-commits: True
|
||||||
ignore-fixup-commits: True
|
ignore-fixup-commits: True
|
||||||
|
ignore-fixup-amend-commits: True
|
||||||
ignore-squash-commits: True
|
ignore-squash-commits: True
|
||||||
ignore-revert-commits: True
|
ignore-revert-commits: True
|
||||||
ignore-stdin: False
|
ignore-stdin: False
|
||||||
staged: False
|
staged: False
|
||||||
fail-without-commits: False
|
fail-without-commits: False
|
||||||
|
regex-style-search: False
|
||||||
verbosity: 2
|
verbosity: 2
|
||||||
debug: True
|
debug: True
|
||||||
target: {target}
|
target: {target}
|
||||||
|
@ -60,7 +62,7 @@ target: {target}
|
||||||
B8: body-match-regex
|
B8: body-match-regex
|
||||||
regex=None
|
regex=None
|
||||||
M1: author-valid-email
|
M1: author-valid-email
|
||||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||||
|
|
||||||
DEBUG: gitlint.cli No --msg-filename flag, no or empty data passed to stdin. Using the local repo.
|
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.git ('log', '-1', '--pretty=%H')
|
||||||
|
@ -68,8 +70,8 @@ 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 ('log', '{commit_sha}', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B')
|
||||||
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
||||||
DEBUG: gitlint.lint Linting commit {commit_sha}
|
DEBUG: gitlint.lint Linting commit {commit_sha}
|
||||||
|
DEBUG: gitlint.git ('diff-tree', '--no-commit-id', '--numstat', '-r', '--root', '{commit_sha}')
|
||||||
DEBUG: gitlint.git ('branch', '--contains', '{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
|
DEBUG: gitlint.lint Commit Object
|
||||||
--- Commit Message ----
|
--- Commit Message ----
|
||||||
WIP: Thïs is a title thåt is a bit longer.
|
WIP: Thïs is a title thåt is a bit longer.
|
||||||
|
@ -81,10 +83,14 @@ Author: gitlint-test-user <gitlint@test.com>
|
||||||
Date: {commit_date}
|
Date: {commit_date}
|
||||||
is-merge-commit: False
|
is-merge-commit: False
|
||||||
is-fixup-commit: False
|
is-fixup-commit: False
|
||||||
|
is-fixup-amend-commit: False
|
||||||
is-squash-commit: False
|
is-squash-commit: False
|
||||||
is-revert-commit: False
|
is-revert-commit: False
|
||||||
Branches: ['master']
|
Parents: []
|
||||||
|
Branches: ['main']
|
||||||
Changed Files: {changed_files}
|
Changed Files: {changed_files}
|
||||||
|
Changed Files Stats:
|
||||||
|
{changed_files_stats}
|
||||||
-----------------------
|
-----------------------
|
||||||
1: T1 Title exceeds max length (42>20)
|
1: T1 Title exceeds max length (42>20)
|
||||||
1: T5 Title contains the word 'WIP' (case-insensitive)
|
1: T5 Title contains the word 'WIP' (case-insensitive)
|
||||||
|
|
94
qa/expected/test_gitlint/test_commit_binary_file_1
Normal file
94
qa/expected/test_gitlint/test_commit_binary_file_1
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
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-fixup-amend-commits: True
|
||||||
|
ignore-squash-commits: True
|
||||||
|
ignore-revert-commits: True
|
||||||
|
ignore-stdin: False
|
||||||
|
staged: False
|
||||||
|
fail-without-commits: False
|
||||||
|
regex-style-search: 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
|
||||||
|
I4: ignore-by-author-name
|
||||||
|
ignore=all
|
||||||
|
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 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 ('diff-tree', '--no-commit-id', '--numstat', '-r', '--root', '{commit_sha}')
|
||||||
|
DEBUG: gitlint.git ('branch', '--contains', '{commit_sha}')
|
||||||
|
DEBUG: gitlint.lint Commit Object
|
||||||
|
--- Commit Message ----
|
||||||
|
Sïmple commit
|
||||||
|
|
||||||
|
--- Meta info ---------
|
||||||
|
Author: gitlint-test-user <gitlint@test.com>
|
||||||
|
Date: {commit_date}
|
||||||
|
is-merge-commit: False
|
||||||
|
is-fixup-commit: False
|
||||||
|
is-fixup-amend-commit: False
|
||||||
|
is-squash-commit: False
|
||||||
|
is-revert-commit: False
|
||||||
|
Parents: []
|
||||||
|
Branches: ['main']
|
||||||
|
Changed Files: {changed_files}
|
||||||
|
Changed Files Stats:
|
||||||
|
{changed_files_stats}
|
||||||
|
-----------------------
|
||||||
|
3: B6 Body message is missing
|
||||||
|
DEBUG: gitlint.cli Exit Code = 1
|
|
@ -1,5 +1,5 @@
|
||||||
1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Thi$ is å title"
|
1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Thi$ is å title"
|
||||||
1: UC2 Body does not contain a 'Signed-off-by' line
|
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: UC3 Branch name 'main' does not start with one of ['feature/', 'hotfix/', 'release/']
|
||||||
1: UL1 Title contains the special character '$': "WIP: Thi$ is å title"
|
1: UL1 Title contains the special character '$': "WIP: Thi$ is å title"
|
||||||
2: B4 Second line is not empty: "Content on the second line"
|
2: B4 Second line is not empty: "Content on the second line"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
1: UC2 Body does not contain a 'Signed-off-by' line
|
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: UC3 Branch name 'main' does not start with one of ['feature/', 'hotfix/', 'release/']
|
||||||
1: UL1 Title contains the special character '$'
|
1: UL1 Title contains the special character '$'
|
||||||
2: B4 Second line is not empty
|
2: B4 Second line is not empty
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Thi$ is å title"
|
1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Thi$ is å title"
|
||||||
1: UC1 Body contains too many lines (2 > 1)
|
1: UC1 Body contains too many lines (2 > 1)
|
||||||
1: UC2 Body does not contain a 'Signed-off-by' line
|
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: UC3 Branch name 'main' does not start with one of ['feature/', 'hotfix/', 'release/']
|
||||||
1: UL1 Title contains the special character '$': "WIP: Thi$ is å title"
|
1: UL1 Title contains the special character '$': "WIP: Thi$ is å title"
|
||||||
2: B4 Second line is not empty: "Content on the second line"
|
2: B4 Second line is not empty: "Content on the second line"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Thi$ is å title"
|
1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Thi$ is å title"
|
||||||
1: UC1 GitContext.current_branch: master
|
1: UC1 GitContext.current_branch: main
|
||||||
1: UC1 GitContext.commentchar: #
|
1: UC1 GitContext.commentchar: #
|
||||||
1: UC2 GitCommit.branches: ['master']
|
1: UC2 GitCommit.branches: ['main']
|
||||||
1: UC2 GitCommit.custom_prop: foöbar
|
1: UC2 GitCommit.custom_prop: foöbar
|
||||||
1: UC4 int-öption: 2
|
1: UC4 int-öption: 2
|
||||||
1: UC4 str-öption: föo
|
1: UC4 str-öption: föo
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue