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"
|
||||
strategy:
|
||||
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"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }} # Checkout pull request HEAD commit instead of merge commit
|
||||
|
||||
# Because gitlint is a tool that uses git itself under the hood, we remove git tracking from the checked out
|
||||
# code by temporarily renaming the .git directory.
|
||||
# code by temporarily renaming the .git directory.
|
||||
# This is to ensure that the tests don't have a dependency on the version control of gitlint itself.
|
||||
- name: Temporarily remove git version control from code
|
||||
run: mv .git ._git
|
||||
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
@ -40,16 +40,28 @@ jobs:
|
|||
# COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
||||
# 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
|
||||
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:
|
||||
GITLINT_USE_SH_LIB: 0
|
||||
GITLINT_USE_SH_LIB: 1
|
||||
run: ./run_tests.sh -i
|
||||
|
||||
- name: PEP8
|
||||
run: ./run_tests.sh -p
|
||||
- name: Code formatting (black)
|
||||
run: ./run_tests.sh -f
|
||||
|
||||
- name: PyLint
|
||||
run: ./run_tests.sh -l
|
||||
|
@ -79,25 +91,25 @@ jobs:
|
|||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.6]
|
||||
python-version: ["3.10"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }} # Checkout pull request HEAD commit instead of merge commit
|
||||
|
||||
# Because gitlint is a tool that uses git itself under the hood, we remove git tracking from the checked out
|
||||
# code by temporarily renaming the .git directory.
|
||||
# code by temporarily renaming the .git directory.
|
||||
# This is to ensure that the tests don't have a dependency on the version control of gitlint itself.
|
||||
- name: Temporarily remove git version control from code
|
||||
run: Rename-Item .git ._git
|
||||
|
||||
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- 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
|
||||
|
||||
- name: Install requirements
|
||||
|
@ -126,8 +138,8 @@ jobs:
|
|||
run: pytest -rw -s qa
|
||||
continue-on-error: true # Known to fail at this point
|
||||
|
||||
- name: PEP8
|
||||
run: flake8 gitlint-core qa examples
|
||||
- name: Code formatting (black)
|
||||
run: black .
|
||||
|
||||
- name: PyLint
|
||||
run: pylint gitlint-core\gitlint qa --rcfile=".pylintrc" -r n
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
- id: gitlint
|
||||
name: gitlint
|
||||
language: python
|
||||
additional_dependencies: ["./gitlint-core[trusted-deps]"]
|
||||
entry: gitlint
|
||||
args: [--staged, --msg-filename]
|
||||
stages: [commit-msg]
|
||||
- id: gitlint
|
||||
name: gitlint
|
||||
description: Checks your git commit messages for style.
|
||||
language: python
|
||||
additional_dependencies: ["./gitlint-core[trusted-deps]"]
|
||||
entry: gitlint
|
||||
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 #
|
||||
|
||||
|
||||
## 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) ##
|
||||
Contributors:
|
||||
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
|
||||
- **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))
|
||||
- Bugfixes:
|
||||
- [contrib-title-conventional-commits (CT1)](https://jorisroovers.com/gitlint/contrib_rules/#ct1-contrib-title-conventional-commits) now properly enforces the commit type ([#185](https://github.com/jorisroovers/gitlint/issues/185))
|
||||
- [contrib-title-conventional-commits (CT1)](https://jorisroovers.com/gitlint/contrib_rules/#ct1-contrib-title-conventional-commits) now supports the BREAKING CHANGE symbol "!" ([#186](https://github.com/jorisroovers/gitlint/issues/186))
|
||||
- Heads-up: [Python 3.6 will become EOL at the end of 2021](https://endoflife.date/python). It's likely that future gitlint releases will stop supporting Python 3.6 as a result. We will continue to support Python 3.6 as long as its easily doable, which in practice usually means as long as our dependencies support it.
|
||||
- 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.
|
||||
## 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).
|
||||
- **Breaking change**: The `--commits` commandline flag now strictly follows the refspec format as interpreted
|
||||
by the [`git rev-list <refspec>`](https://git-scm.com/docs/git-rev-list) command. This means
|
||||
that linting a single commit using `gitlint --commits <SHA>` won't work anymore. Instead, for single commits,
|
||||
users now need to specificy `gitlint --commits <SHA>^...<SHA>`. On the upside, this change also means
|
||||
that linting a single commit using `gitlint --commits <ref>` won't work anymore. Instead, for single commits,
|
||||
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
|
||||
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
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
# NOTE: --ulimit is required to work around a limitation in Docker
|
||||
# Details: https://github.com/jorisroovers/gitlint/issues/129
|
||||
|
||||
FROM python:3.10-alpine
|
||||
FROM python:3.11.0-alpine
|
||||
ARG GITLINT_VERSION
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
```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
|
||||
|
@ -39,16 +39,17 @@ ignore=title-trailing-punctuation, T3
|
|||
# precedence over this
|
||||
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-revert-commits=true
|
||||
ignore-fixup-commits=true
|
||||
ignore-fixup-amend-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
|
||||
|
||||
# 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.
|
||||
staged=true
|
||||
|
||||
|
@ -58,6 +59,11 @@ staged=true
|
|||
# Disabled by default.
|
||||
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.
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
|
@ -201,7 +207,7 @@ gitlint-ignore: T1, body-hard-tab
|
|||
gitlint configuration is applied in the following order of precedence:
|
||||
|
||||
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`)
|
||||
4. Environment variables (e.g.: `GITLINT_VERBOSITY=3`)
|
||||
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.
|
||||
|
||||
Default value | gitlint version | commandline flag | environment variable
|
||||
---------------|------------------|-------------------|-----------------------
|
||||
`False` | >= 0.1.0 | `--silent` | `GITLINT_SILENT`
|
||||
| Default value | gitlint version | commandline flag | environment variable |
|
||||
| ------------- | --------------- | ---------------- | -------------------- |
|
||||
| `False` | >= 0.1.0 | `--silent` | `GITLINT_SILENT` |
|
||||
|
||||
#### Examples
|
||||
```sh
|
||||
|
@ -226,14 +232,15 @@ Default value | gitlint version | commandline flag | environment variable
|
|||
gitlint --silent
|
||||
GITLINT_SILENT=1 gitlint # using env variable
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
### verbosity
|
||||
|
||||
Amount of output gitlint will show when printing errors.
|
||||
|
||||
Default value | gitlint version | commandline flag | environment variable
|
||||
---------------|------------------|-------------------|-----------------------
|
||||
3 | >= 0.1.0 | `-v` | `GITLINT_VERBOSITY`
|
||||
| Default value | gitlint version | commandline flag | environment variable |
|
||||
| ------------- | --------------- | ---------------- | -------------------- |
|
||||
| 3 | >= 0.1.0 | `-v` | `GITLINT_VERBOSITY` |
|
||||
|
||||
|
||||
#### Examples
|
||||
|
@ -252,14 +259,15 @@ GITLINT_VERBOSITY=2 gitlint # using env variable
|
|||
[general]
|
||||
verbosity=2
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
### ignore
|
||||
|
||||
Comma separated list of rules to ignore (by name or id).
|
||||
|
||||
Default value | gitlint version | commandline flag | environment variable
|
||||
---------------------------|------------------|-------------------|-----------------------
|
||||
[] (=empty list) | >= 0.1.0 | `--ignore` | `GITLINT_IGNORE`
|
||||
| Default value | gitlint version | commandline flag | environment variable |
|
||||
| ---------------- | --------------- | ---------------- | -------------------- |
|
||||
| [] (=empty list) | >= 0.1.0 | `--ignore` | `GITLINT_IGNORE` |
|
||||
|
||||
#### Examples
|
||||
```sh
|
||||
|
@ -274,14 +282,15 @@ GITLINT_IGNORE=T1,body-min-length gitlint # using env variable
|
|||
[general]
|
||||
ignore=T1,body-min-length
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
### debug
|
||||
|
||||
Enable debugging output.
|
||||
|
||||
Default value | gitlint version | commandline flag | environment variable
|
||||
---------------|------------------|-------------------|-----------------------
|
||||
false | >= 0.7.1 | `--debug` | `GITLINT_DEBUG`
|
||||
| Default value | gitlint version | commandline flag | environment variable |
|
||||
| ------------- | --------------- | ---------------- | -------------------- |
|
||||
| false | >= 0.7.1 | `--debug` | `GITLINT_DEBUG` |
|
||||
|
||||
#### Examples
|
||||
```sh
|
||||
|
@ -291,14 +300,15 @@ GITLINT_DEBUG=1 gitlint # using env variable
|
|||
# --debug is special, the following does NOT work
|
||||
# gitlint -c general.debug=true
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
### target
|
||||
|
||||
Target git repository gitlint should be linting against.
|
||||
|
||||
Default value | gitlint version | commandline flag | environment variable
|
||||
---------------------------|------------------|-------------------|-----------------------
|
||||
(empty) | >= 0.8.0 | `--target` | `GITLINT_TARGET`
|
||||
| Default value | gitlint version | commandline flag | environment variable |
|
||||
| ------------- | --------------- | ---------------- | -------------------- |
|
||||
| (empty) | >= 0.8.0 | `--target` | `GITLINT_TARGET` |
|
||||
|
||||
#### Examples
|
||||
```sh
|
||||
|
@ -312,14 +322,31 @@ GITLINT_TARGET=/home/joe/myrepo/ gitlint # using env variable
|
|||
[general]
|
||||
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
|
||||
|
||||
Path where gitlint looks for [user-defined rules](user_defined_rules.md).
|
||||
|
||||
Default value | gitlint version | commandline flag | environment variable
|
||||
---------------------------|------------------|-------------------|-----------------------
|
||||
(empty) | >= 0.8.0 | `--extra-path` | `GITLINT_EXTRA_PATH`
|
||||
| Default value | gitlint version | commandline flag | environment variable |
|
||||
| ------------- | --------------- | ---------------- | -------------------- |
|
||||
| (empty) | >= 0.8.0 | `--extra-path` | `GITLINT_EXTRA_PATH` |
|
||||
|
||||
#### Examples
|
||||
```sh
|
||||
|
@ -333,14 +360,14 @@ GITLINT_EXTRA_PATH=/home/joe/rules/ gitlint # using env variable
|
|||
[general]
|
||||
extra-path=/home/joe/rules/
|
||||
```
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
### 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
|
||||
---------------------------|------------------|-------------------|-----------------------
|
||||
(empty) | >= 0.12.0 | `--contrib` | `GITLINT_CONTRIB`
|
||||
| Default value | gitlint version | commandline flag | environment variable |
|
||||
| ------------- | --------------- | ---------------- | -------------------- |
|
||||
| (empty) | >= 0.12.0 | `--contrib` | `GITLINT_CONTRIB` |
|
||||
|
||||
#### Examples
|
||||
```sh
|
||||
|
@ -349,21 +376,31 @@ gitlint --contrib=contrib-title-conventional-commits,CC1
|
|||
# different way of doing the same
|
||||
gitlint -c general.contrib=contrib-title-conventional-commits,CC1
|
||||
# using env variable
|
||||
GITLINT_CONTRIB=contrib-title-conventional-commits,CC1 gitlint
|
||||
GITLINT_CONTRIB=contrib-title-conventional-commits,CC1 gitlint
|
||||
```
|
||||
```ini
|
||||
#.gitlint
|
||||
[general]
|
||||
contrib=contrib-title-conventional-commits,CC1
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
### staged
|
||||
|
||||
Fetch additional meta-data from the local repository when manually passing a commit message to gitlint via stdin or `--commit-msg`.
|
||||
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
|
||||
---------------|------------------|-------------------|-----------------------
|
||||
false | >= 0.13.0 | `--staged` | `GITLINT_STAGED`
|
||||
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
|
||||
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
|
||||
```sh
|
||||
|
@ -377,6 +414,7 @@ GITLINT_STAGED=1 gitlint # using env variable
|
|||
[general]
|
||||
staged=true
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
### 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
|
||||
to tell gitlint to fail on **valid but empty** commit ranges.
|
||||
|
||||
Default value | gitlint version | commandline flag | environment variable
|
||||
---------------|------------------|---------------------------|-----------------------
|
||||
false | >= 0.15.2 | `--fail-without-commits` | `GITLINT_FAIL_WITHOUT_COMMITS`
|
||||
| Default value | gitlint version | commandline flag | environment variable |
|
||||
| ------------- | --------------- | ------------------------ | ------------------------------ |
|
||||
| false | >= 0.15.2 | `--fail-without-commits` | `GITLINT_FAIL_WITHOUT_COMMITS` |
|
||||
|
||||
#### Examples
|
||||
```sh
|
||||
# CLI
|
||||
# The following will cause gitlint to hard fail (i.e. exit code > 0)
|
||||
# since HEAD..HEAD is a valid but empty commit range.
|
||||
# since HEAD..HEAD is a valid but empty commit range.
|
||||
gitlint --fail-without-commits --commits HEAD..HEAD
|
||||
GITLINT_FAIL_WITHOUT_COMMITS=1 gitlint # using env variable
|
||||
```
|
||||
|
@ -402,13 +440,79 @@ GITLINT_FAIL_WITHOUT_COMMITS=1 gitlint # using env variable
|
|||
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 any stdin data. Sometimes useful when running gitlint in a CI server.
|
||||
|
||||
Default value | gitlint version | commandline flag | environment variable
|
||||
---------------|------------------|-------------------|-----------------------
|
||||
false | >= 0.12.0 | `--ignore-stdin` | `GITLINT_IGNORE_STDIN`
|
||||
| Default value | gitlint version | commandline flag | environment variable |
|
||||
| ------------- | --------------- | ---------------- | ---------------------- |
|
||||
| false | >= 0.12.0 | `--ignore-stdin` | `GITLINT_IGNORE_STDIN` |
|
||||
|
||||
#### Examples
|
||||
```sh
|
||||
|
@ -422,14 +526,15 @@ GITLINT_IGNORE_STDIN=1 gitlint # using env variable
|
|||
[general]
|
||||
ignore-stdin=true
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
### ignore-merge-commits
|
||||
|
||||
Whether or not to ignore merge commits.
|
||||
|
||||
Default value | gitlint version | commandline flag | environment variable
|
||||
---------------|------------------|-------------------|-----------------------
|
||||
true | >= 0.7.0 | Not Available | Not Available
|
||||
| Default value | gitlint version | commandline flag | environment variable |
|
||||
| ------------- | --------------- | ---------------- | -------------------- |
|
||||
| true | >= 0.7.0 | Not Available | Not Available |
|
||||
|
||||
#### Examples
|
||||
```sh
|
||||
|
@ -441,14 +546,15 @@ gitlint -c general.ignore-merge-commits=false
|
|||
[general]
|
||||
ignore-merge-commits=false
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
### ignore-revert-commits
|
||||
|
||||
Whether or not to ignore revert commits.
|
||||
|
||||
Default value | gitlint version | commandline flag | environment variable
|
||||
---------------|------------------|-------------------|-----------------------
|
||||
true | >= 0.13.0 | Not Available | Not Available
|
||||
| Default value | gitlint version | commandline flag | environment variable |
|
||||
| ------------- | --------------- | ---------------- | -------------------- |
|
||||
| true | >= 0.13.0 | Not Available | Not Available |
|
||||
|
||||
#### Examples
|
||||
```sh
|
||||
|
@ -460,14 +566,15 @@ gitlint -c general.ignore-revert-commits=false
|
|||
[general]
|
||||
ignore-revert-commits=false
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
### ignore-fixup-commits
|
||||
|
||||
Whether or not to ignore [fixup](https://git-scm.com/docs/git-commit#git-commit---fixupltcommitgt) commits.
|
||||
|
||||
Default value | gitlint version | commandline flag | environment variable
|
||||
---------------|------------------|-------------------|-----------------------
|
||||
true | >= 0.9.0 | Not Available | Not Available
|
||||
| Default value | gitlint version | commandline flag | environment variable |
|
||||
| ------------- | --------------- | ---------------- | -------------------- |
|
||||
| true | >= 0.9.0 | Not Available | Not Available |
|
||||
|
||||
#### Examples
|
||||
```sh
|
||||
|
@ -479,14 +586,35 @@ gitlint -c general.ignore-fixup-commits=false
|
|||
[general]
|
||||
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
|
||||
|
||||
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
|
||||
---------------|------------------|-------------------|-----------------------
|
||||
true | >= 0.9.0 | Not Available | Not Available
|
||||
| Default value | gitlint version | commandline flag | environment variable |
|
||||
| ------------- | --------------- | ---------------- | -------------------- |
|
||||
| true | >= 0.9.0 | Not Available | Not Available |
|
||||
|
||||
#### Examples
|
||||
```sh
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
# Using Contrib Rules
|
||||
|
||||
_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 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
|
||||
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.
|
||||
```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.
|
||||
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 ##
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
||||
## 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
|
||||
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
|
||||
(sometimes up to a few months, this is a hobby project), but rest assured that we read your message and appreciate
|
||||
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.
|
||||
|
||||
## 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
|
||||
[enforced by CI](https://github.com/jorisroovers/gitlint/actions)). Again, please consider writing new ones
|
||||
for your functionality, not only updating existing ones to make the build pass.
|
||||
- [Documentation](https://github.com/jorisroovers/gitlint/tree/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
|
||||
can make it as part of a release. If you can already include them as part of your PR, it's a huge timesaver for us
|
||||
and it's likely that your PR will be merged and released a lot sooner. Thanks!
|
||||
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
|
||||
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
|
||||
**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
|
||||
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.
|
||||
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:
|
||||
To install gitlint for local development:
|
||||
|
||||
```sh
|
||||
python -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt -r test-requirements.txt -r doc-requirements.txt
|
||||
python setup.py develop
|
||||
```
|
||||
|
||||
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
|
||||
./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 --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 --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 --git # inception: run gitlint against itself
|
||||
./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
|
||||
./run_tests.sh --envs 36 # Run the unit tests against Python 3.6
|
||||
./run_tests.sh --envs 36,37,pypy36 # Run the unit tests against Python 3.6, Python 3.7 and Pypy3.6
|
||||
./run_tests.sh --envs 36,37 --pep8 # Run pep8 checks against Python 3.6 and Python 3.7 (also works for --git, --integration, --pep8, --stats and --lint.
|
||||
./run_tests.sh --envs all --all # Run all tests against all environments
|
||||
./run_tests.sh --all-env --all # Idem: Run all tests against all environments
|
||||
black . # format all python code
|
||||
black gitlint-core/gitlint/lint.py # format a specific file
|
||||
```
|
||||
|
||||
!!! important
|
||||
Gitlint commits and pull requests are gated on all of our tests and checks.
|
||||
## Documentation
|
||||
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
|
||||
|
||||
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
|
||||
```sh
|
||||
pip install docutils
|
||||
|
@ -89,16 +143,6 @@ export LANG=en_US.UTF-8
|
|||
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
|
||||
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
|
||||
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!
|
||||
|
||||
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:
|
||||
|
||||
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.
|
||||
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).
|
||||
|
@ -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.
|
||||
|
||||
- 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
|
||||
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.
|
||||
|
@ -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.
|
||||
- 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** 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,
|
||||
"\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,
|
||||
|
@ -2404,7 +2404,7 @@
|
|||
],
|
||||
[
|
||||
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,
|
||||
|
@ -2432,7 +2432,7 @@
|
|||
],
|
||||
[
|
||||
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,
|
||||
|
@ -3108,11 +3108,11 @@
|
|||
],
|
||||
[
|
||||
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,
|
||||
"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,
|
||||
|
@ -3508,7 +3508,7 @@
|
|||
],
|
||||
[
|
||||
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,
|
||||
|
@ -3795,4 +3795,4 @@
|
|||
"exit\r\n"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
|
@ -2,3 +2,11 @@ a.toctree-l3 {
|
|||
margin-left: 10px;
|
||||
/* 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).
|
||||
- **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),
|
||||
[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
|
||||
useful throughout the years.
|
||||
- **Easily configurable:** Gitlint has sane defaults, but [you can also easily customize it to your own liking](configuration.md).
|
||||
- **Community contributed rules**: Conventions that are common but not universal [can be selectively enabled](contrib_rules).
|
||||
- **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).
|
||||
- **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,
|
||||
|
@ -38,6 +38,10 @@ useful throughout the years.
|
|||
# Pip is recommended to install the latest version
|
||||
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:
|
||||
brew install gitlint # Homebrew (macOS)
|
||||
sudo port install gitlint # Macports (macOS)
|
||||
|
@ -81,6 +85,19 @@ $ cat examples/commit-message-2 | gitlint
|
|||
!!! note
|
||||
The returned exit code equals the number of errors found. [Some exit codes are special](index.md#exit-codes).
|
||||
|
||||
### 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
|
||||
|
||||
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
|
||||
ignore=body-is-missing,T3
|
||||
|
||||
# Ignore any data send to gitlint via stdin
|
||||
# Ignore any data sent to gitlint via stdin
|
||||
ignore-stdin=true
|
||||
|
||||
# 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
|
||||
used multiple times to set multiple config values.
|
||||
--commit TEXT Hash (SHA) of specific commit to lint.
|
||||
--commits TEXT The range of commits to lint. [default: HEAD]
|
||||
--commits TEXT The range of commits (refspec or comma-separated
|
||||
hashes) to lint. [default: HEAD]
|
||||
-e, --extra-path PATH Path to a directory or python module with extra
|
||||
user-defined rules
|
||||
--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.
|
||||
--ignore-stdin Ignore any stdin data. Useful for running in CI
|
||||
server.
|
||||
--staged Read staged commit meta-info from the local
|
||||
repository.
|
||||
--staged Attempt smart guesses about meta info (like
|
||||
author name, email, branch, changed files, etc)
|
||||
for staged commits.
|
||||
--fail-without-commits Hard fail when the target commit range is empty.
|
||||
-v, --verbose Verbosity, more v's for more verbose output
|
||||
(e.g.: -v, -vv, -vvv). [default: -vvv]
|
||||
|
@ -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:
|
||||
```yaml
|
||||
- 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:
|
||||
- id: gitlint
|
||||
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.
|
||||
|
||||
|
||||
### 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
|
||||
By default, when just running `gitlint` without additional parameters, gitlint lints the last commit in the current
|
||||
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),
|
||||
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
|
||||
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
|
||||
# Lint a specific commit range:
|
||||
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"
|
||||
|
||||
# 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
|
||||
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
|
||||
script to lint an arbitrary set of commits, like shown in the example below.
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
|
||||
for commit in $(git rev-list master); do
|
||||
for commit in $(git rev-list my-branch); do
|
||||
echo "Commit $commit"
|
||||
gitlint --commit $commit
|
||||
echo "--------"
|
||||
|
@ -283,14 +347,14 @@ done
|
|||
|
||||
!!! note
|
||||
One downside to this approach is that you invoke gitlint once per commit vs. once per set of commits.
|
||||
This means you'll incur the gitlint startup time once per commit, making this approach rather slow if you want to
|
||||
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.
|
||||
|
||||
|
||||
## 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
|
||||
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
|
||||
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
|
||||
short-lived and not intended to make it into the final commit history. In addition, by prepending *"fixup!"* or
|
||||
*"squash!"* to your commit message, certain gitlint rules might be violated
|
||||
short-lived and not intended to make it into the final commit history. In addition, by prepending *"fixup!"*,
|
||||
*"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.
|
||||
|
||||
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`
|
||||
[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
|
||||
a conflicting situation. Gitlint does not do any resolution of such conflicts, it's up to you to make sure
|
||||
any configuration is non-conflicting. So caution advised!
|
||||
|
||||
|
||||
Defining a named rule is easy, for example using your `.gitlint` file:
|
||||
|
||||
```ini
|
||||
|
@ -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.
|
||||
|
||||
```sh
|
||||
$ gitlint
|
||||
$ gitlint
|
||||
1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: foo wonderwoman hur bar"
|
||||
1: T5:This-Can_Be*Whatever$YouWant Title contains the word 'wonderwoman' (case-insensitive): "WIP: foo wonderwoman hur bar"
|
||||
1: T5:extra-words Title contains the word 'foo' (case-insensitive): "WIP: foo wonderwoman hur bar"
|
||||
|
@ -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
|
||||
to 252.
|
||||
|
||||
Exit Code | Description
|
||||
-----------|------------------------------------------------------------
|
||||
253 | Wrong invocation of the `gitlint` command.
|
||||
254 | Something went wrong when invoking git.
|
||||
255 | Invalid gitlint configuration
|
||||
| Exit Code | Description |
|
||||
| --------- | ------------------------------------------ |
|
||||
| 253 | Wrong invocation of the `gitlint` command. |
|
||||
| 254 | Something went wrong when invoking git. |
|
||||
| 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.
|
||||
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-------------------|-------------------------------------------
|
||||
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)
|
||||
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)
|
||||
T5 | title-must-not-contain-word | >= 0.1.0 | Title cannot contain certain words (default: "WIP")
|
||||
T6 | title-leading-whitespace | >= 0.4.0 | Title cannot have leading whitespace (space or tab)
|
||||
T7 | title-match-regex | >= 0.5.0 | Title must match a given regex (default: None)
|
||||
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
|
||||
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)
|
||||
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
|
||||
B6 | body-is-missing | >= 0.4.0 | Body message must be specified
|
||||
B7 | body-changed-file-mention | >= 0.4.0 | Body must contain references to certain files if those files are changed in the last commit
|
||||
B8 | body-match-regex | >= 0.14.0 | Title must match a given regex (default: None)
|
||||
M1 | author-valid-email | >= 0.9.0 | Author email address must be a valid email address
|
||||
I1 | ignore-by-title | >= 0.10.0 | Ignore a commit based on matching its title
|
||||
I2 | ignore-by-body | >= 0.10.0 | Ignore a commit based on matching its body
|
||||
I3 | ignore-body-lines | >= 0.14.0 | Ignore certain lines in a commit body that match a regex
|
||||
I4 | ignore-by-author-name | >= 0.16.0 | Ignore a commit based on matching its author name
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | --------------------------- | --------------- | ------------------------------------------------------------------------------------------- |
|
||||
| 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) |
|
||||
| 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) |
|
||||
| T5 | title-must-not-contain-word | >= 0.1.0 | Title cannot contain certain words (default: "WIP") |
|
||||
| T6 | title-leading-whitespace | >= 0.4.0 | Title cannot have leading whitespace (space or tab) |
|
||||
| T7 | title-match-regex | >= 0.5.0 | Title must match a given regex (default: None) |
|
||||
| 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 |
|
||||
| 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) |
|
||||
| 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 |
|
||||
| B6 | body-is-missing | >= 0.4.0 | Body message must be specified |
|
||||
| B7 | body-changed-file-mention | >= 0.4.0 | Body must contain references to certain files if those files are changed in the last commit |
|
||||
| B8 | body-match-regex | >= 0.14.0 | Body must match a given regex (default: None) |
|
||||
| M1 | author-valid-email | >= 0.9.0 | Author email address must be a valid email address |
|
||||
| I1 | ignore-by-title | >= 0.10.0 | Ignore a commit based on matching its title |
|
||||
| I2 | ignore-by-body | >= 0.10.0 | Ignore a commit based on matching its body |
|
||||
| I3 | ignore-body-lines | >= 0.14.0 | Ignore certain lines in a commit body that match a regex |
|
||||
| I4 | ignore-by-author-name | >= 0.16.0 | Ignore a commit based on matching its author name |
|
||||
|
||||
|
||||
|
||||
## T1: title-max-length
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
T1 | title-max-length | >= 0.1 | Title length must be < 72 chars.
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | ---------------- | --------------- | ------------------------------------ |
|
||||
| T1 | title-max-length | >= 0.1 | Title length must be <= 72 chars. |
|
||||
|
||||
### Options
|
||||
|
||||
Name | gitlint version | Default | Description
|
||||
---------------|-----------------|---------|----------------------------------
|
||||
line-length | >= 0.2 | 72 | Maximum allowed title length
|
||||
| Name | gitlint version | Default | Description |
|
||||
| ----------- | --------------- | ------- | ---------------------------- |
|
||||
| line-length | >= 0.2 | 72 | Maximum allowed title length |
|
||||
|
||||
### Examples
|
||||
|
||||
|
@ -59,39 +59,43 @@ line-length=72
|
|||
[title-max-length]
|
||||
line-length=120
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## T2: title-trailing-whitespace
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
T2 | title-trailing-whitespace | >= 0.1 | Title cannot have trailing whitespace (space or tab)
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | ------------------------- | --------------- | ---------------------------------------------------- |
|
||||
| T2 | title-trailing-whitespace | >= 0.1 | Title cannot have trailing whitespace (space or tab) |
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## T3: title-trailing-punctuation
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
T3 | title-trailing-punctuation | >= 0.1 | Title cannot have trailing punctuation (?:!.,;)
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | -------------------------- | --------------- | ----------------------------------------------- |
|
||||
| T3 | title-trailing-punctuation | >= 0.1 | Title cannot have trailing punctuation (?:!.,;) |
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## T4: title-hard-tab
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
T4 | title-hard-tab | >= 0.1 | Title cannot contain hard tab characters (\t)
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | -------------- | --------------- | --------------------------------------------- |
|
||||
| T4 | title-hard-tab | >= 0.1 | Title cannot contain hard tab characters (\t) |
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## T5: title-must-not-contain-word
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
T5 | title-must-not-contain-word | >= 0.1 | Title cannot contain certain words (default: "WIP")
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | --------------------------- | --------------- | --------------------------------------------------- |
|
||||
| T5 | title-must-not-contain-word | >= 0.1 | Title cannot contain certain words (default: "WIP") |
|
||||
|
||||
### Options
|
||||
|
||||
Name | gitlint version | Default | Description
|
||||
---------------|-----------------|---------|----------------------------------
|
||||
words | >= 0.3 | WIP | Comma-separated list of words that should not be used in the title. Matching is case insensitive
|
||||
| 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 |
|
||||
|
||||
### Examples
|
||||
|
||||
|
@ -102,25 +106,28 @@ words | >= 0.3 | WIP | Comma-separated list of words that
|
|||
[title-must-not-contain-word]
|
||||
words=crap,darn,damn
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## T6: title-leading-whitespace
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
T6 | title-leading-whitespace | >= 0.4 | Title cannot have leading whitespace (space or tab)
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | ------------------------ | --------------- | --------------------------------------------------- |
|
||||
| T6 | title-leading-whitespace | >= 0.4 | Title cannot have leading whitespace (space or tab) |
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## T7: title-match-regex
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
T7 | title-match-regex | >= 0.5 | Title must match a given regex (default: .*)
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | ----------------- | --------------- | -------------------------------------------- |
|
||||
| T7 | title-match-regex | >= 0.5 | Title must match a given regex (default: .*) |
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
Name | gitlint version | Default | Description
|
||||
---------------|-----------------|---------|----------------------------------
|
||||
regex | >= 0.5 | .* | [Python regex](https://docs.python.org/library/re.html) that the title should match.
|
||||
| Name | gitlint version | Default | Description |
|
||||
| ----- | --------------- | ------- | ------------------------------------------------------------------------------------ |
|
||||
| regex | >= 0.5 | .* | [Python regex](https://docs.python.org/library/re.html) that the title should match. |
|
||||
|
||||
### Examples
|
||||
|
||||
|
@ -131,19 +138,20 @@ regex | >= 0.5 | .* | [Python regex](https://docs.python.
|
|||
[title-match-regex]
|
||||
regex=^US[1-9][0-9]*
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## T8: title-min-length ##
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
T1 | title-min-length | >= | Title length must be > 5 chars.
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | ---------------- | --------------- | ----------------------------------- |
|
||||
| T8 | title-min-length | >= 0.14.0 | Title length must be >= 5 chars. |
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
Name | gitlint version | Default | Description
|
||||
---------------|-----------------|---------|----------------------------------
|
||||
min-length | >= 0.14.0 | 5 | Minimum required title length
|
||||
| Name | gitlint version | Default | Description |
|
||||
| ---------- | --------------- | ------- | ----------------------------- |
|
||||
| min-length | >= 0.14.0 | 5 | Minimum required title length |
|
||||
|
||||
### Examples
|
||||
|
||||
|
@ -154,18 +162,19 @@ min-length | >= 0.14.0 | 5 | Minimum required title length
|
|||
[title-min-length]
|
||||
min-length=3
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## B1: body-max-line-length
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
B1 | body-max-line-length | >= 0.1 | Lines in the body must be < 80 chars
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | -------------------- | --------------- | ---------------------------------------- |
|
||||
| B1 | body-max-line-length | >= 0.1 | Lines in the body must be <= 80 chars |
|
||||
|
||||
### Options
|
||||
|
||||
Name | gitlint version | Default | Description
|
||||
---------------|-----------------|---------|----------------------------------
|
||||
line-length | >= 0.2 | 80 | Maximum allowed line length in the commit message body
|
||||
| Name | gitlint version | Default | Description |
|
||||
| ----------- | --------------- | ------- | ------------------------------------------------------ |
|
||||
| line-length | >= 0.2 | 80 | Maximum allowed line length in the commit message body |
|
||||
|
||||
### Examples
|
||||
|
||||
|
@ -180,38 +189,43 @@ line-length=120
|
|||
[body-max-line-length]
|
||||
line-length=72
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## B2: body-trailing-whitespace
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
B2 | body-trailing-whitespace | >= 0.1 | Body cannot have trailing whitespace (space or tab)
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | ------------------------ | --------------- | --------------------------------------------------- |
|
||||
| B2 | body-trailing-whitespace | >= 0.1 | Body cannot have trailing whitespace (space or tab) |
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## B3: body-hard-tab
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
B3 | body-hard-tab | >= 0.1 | Body cannot contain hard tab characters (\t)
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | ------------- | --------------- | -------------------------------------------- |
|
||||
| B3 | body-hard-tab | >= 0.1 | Body cannot contain hard tab characters (\t) |
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## B4: body-first-line-empty
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
B4 | body-first-line-empty | >= 0.1 | First line of the body (second line of commit message) must be empty
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | --------------------- | --------------- | -------------------------------------------------------------------- |
|
||||
| B4 | body-first-line-empty | >= 0.1 | First line of the body (second line of commit message) must be empty |
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## B5: body-min-length
|
||||
|
||||
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.
|
||||
| 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. |
|
||||
|
||||
### Options ###
|
||||
|
||||
Name | gitlint version | Default | Description
|
||||
---------------|-----------------|---------|----------------------------------
|
||||
min-length | >= 0.4 | 20 | Minimum number of required characters in body
|
||||
| Name | gitlint version | Default | Description |
|
||||
| ---------- | --------------- | ------- | --------------------------------------------- |
|
||||
| min-length | >= 0.4 | 20 | Minimum number of required characters in body |
|
||||
|
||||
### Examples
|
||||
|
||||
|
@ -226,31 +240,34 @@ min-length=5
|
|||
[body-min-length]
|
||||
min-length=100
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## B6: body-is-missing
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
B6 | body-is-missing | >= 0.4 | Body message must be specified
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | --------------- | --------------- | ------------------------------ |
|
||||
| B6 | body-is-missing | >= 0.4 | Body message must be specified |
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
Name | gitlint version | Default | Description
|
||||
----------------------|-----------------|-----------|----------------------------------
|
||||
ignore-merge-commits | >= 0.4 | true | Whether this rule should be ignored during merge commits. Allowed values: true,false.
|
||||
| Name | gitlint version | Default | Description |
|
||||
| -------------------- | --------------- | ------- | ------------------------------------------------------------------------------------- |
|
||||
| ignore-merge-commits | >= 0.4 | true | Whether this rule should be ignored during merge commits. Allowed values: true,false. |
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## B7: body-changed-file-mention
|
||||
|
||||
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
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | ------------------------- | --------------- | ------------------------------------------------------------------------------------------- |
|
||||
| B7 | body-changed-file-mention | >= 0.4 | Body must contain references to certain files if those files are changed in the last commit |
|
||||
|
||||
### Options
|
||||
|
||||
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.
|
||||
| Name | gitlint version | Default | Description |
|
||||
| ----- | --------------- | ------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| files | >= 0.4 | (empty) | Comma-separated list of files that need to an explicit mention in the commit message in case they are changed. |
|
||||
|
||||
### Examples
|
||||
|
||||
|
@ -262,18 +279,19 @@ files | >= 0.4 | (empty) | Comma-separated list o
|
|||
[body-changed-file-mention]
|
||||
files=generated.xml,secrets.txt,private-key.pem
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## B8: body-match-regex
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
B8 | body-match-regex | >= 0.14 | Body must match a given regex
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | ---------------- | --------------- | ----------------------------- |
|
||||
| B8 | body-match-regex | >= 0.14 | Body must match a given regex |
|
||||
|
||||
### Options
|
||||
|
||||
Name | gitlint version | Default | Description
|
||||
----------------------|-----------------|--------------|----------------------------------
|
||||
regex | >= 0.14 | None | [Python regex](https://docs.python.org/library/re.html) that the title should match.
|
||||
| Name | gitlint version | Default | Description |
|
||||
| ----- | --------------- | ------- | ----------------------------------------------------------------------------------- |
|
||||
| regex | >= 0.14 | None | [Python regex](https://docs.python.org/library/re.html) that the body should match. |
|
||||
|
||||
### Examples
|
||||
|
||||
|
@ -288,12 +306,13 @@ regex=Reviewed-By:(.*)$
|
|||
[body-match-regex]
|
||||
regex=(*.)Foo(.*)
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## M1: author-valid-email
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
M1 | author-valid-email | >= 0.8.3 | Author email address must be a valid email address
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | ------------------ | --------------- | -------------------------------------------------- |
|
||||
| M1 | author-valid-email | >= 0.8.3 | Author email address must be a valid email address |
|
||||
|
||||
!!! 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).
|
||||
|
@ -303,9 +322,9 @@ M1 | author-valid-email | >= 0.8.3 | Author email address mus
|
|||
|
||||
### Options
|
||||
|
||||
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
|
||||
| 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 |
|
||||
|
||||
|
||||
### Examples
|
||||
|
@ -317,20 +336,21 @@ regex | >= 0.9.0 | `[^@ ]+@[^@ ]+\.[^@ ]+` | [Python
|
|||
[author-valid-email]
|
||||
regex=[^@]+@foo.com
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## I1: ignore-by-title
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
I1 | ignore-by-title | >= 0.10.0 | Ignore a commit based on matching its title.
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | --------------- | --------------- | -------------------------------------------- |
|
||||
| I1 | ignore-by-title | >= 0.10.0 | Ignore a commit based on matching its title. |
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
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.
|
||||
ignore | >= 0.10.0 | all | Comma-separated list of rule names or ids to ignore when this rule is matched.
|
||||
| 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. |
|
||||
| ignore | >= 0.10.0 | all | Comma-separated list of rule names or ids to ignore when this rule is matched. |
|
||||
|
||||
### Examples
|
||||
|
||||
|
@ -345,20 +365,21 @@ ignore=title-max-length,body-min-length
|
|||
# ignore all rules by setting ignore to 'all'
|
||||
# ignore=all
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## I2: ignore-by-body
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
I2 | ignore-by-body | >= 0.10.0 | Ignore a commit based on matching its body.
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | -------------- | --------------- | ------------------------------------------- |
|
||||
| I2 | ignore-by-body | >= 0.10.0 | Ignore a commit based on matching its body. |
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
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.
|
||||
ignore | >= 0.10.0 | all | Comma-separated list of rule names or ids to ignore when this rule is matched.
|
||||
| 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. |
|
||||
| ignore | >= 0.10.0 | all | Comma-separated list of rule names or ids to ignore when this rule is matched. |
|
||||
|
||||
### Examples
|
||||
|
||||
|
@ -376,19 +397,20 @@ ignore=all
|
|||
regex=(.*)release(.*)
|
||||
ignore=T1,body-min-length,B6
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## I3: ignore-body-lines
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|-----------------------------|-----------------|-------------------------------------------
|
||||
I3 | ignore-body-lines | >= 0.14.0 | Ignore certain lines in a commit body that match a regex.
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | ----------------- | --------------- | --------------------------------------------------------- |
|
||||
| I3 | ignore-body-lines | >= 0.14.0 | Ignore certain lines in a commit body that match a regex. |
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
Name | gitlint version | Default | Description
|
||||
----------------------|-------------------|------------------------------|----------------------------------
|
||||
regex | >= 0.14.0 | None | [Python regex](https://docs.python.org/library/re.html) to match against each line of the body. On match, that line will be ignored by gitlint (the rest of the body will still be linted).
|
||||
| Name | gitlint version | Default | Description |
|
||||
| ----- | --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| regex | >= 0.14.0 | None | [Python regex](https://docs.python.org/library/re.html) to match against each line of the body. On match, that line will be ignored by gitlint (the rest of the body will still be linted). |
|
||||
|
||||
### Examples
|
||||
|
||||
|
@ -407,19 +429,20 @@ regex=(^Co-Authored-By)|(^Signed-off-by)
|
|||
[ignore-body-lines]
|
||||
regex=(.*)foobar(.*)
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## I4: ignore-by-author-name
|
||||
|
||||
ID | Name | gitlint version | Description
|
||||
------|---------------------------|-----------------|-------------------------------------------
|
||||
I4 | ignore-by-author-name | >= 0.16.0 | Ignore a commit based on matching its author name.
|
||||
| ID | Name | gitlint version | Description |
|
||||
| --- | --------------------- | --------------- | -------------------------------------------------- |
|
||||
| I4 | ignore-by-author-name | >= 0.16.0 | Ignore a commit based on matching its author name. |
|
||||
|
||||
### Options
|
||||
|
||||
Name | gitlint version | Default | Description
|
||||
----------------------|-------------------|------------------------------|----------------------------------
|
||||
regex | >= 0.16.0 | None | [Python regex](https://docs.python.org/library/re.html) to match against the commit author name. On match, the commit will be ignored.
|
||||
ignore | >= 0.16.0 | all | Comma-separated list of rule names or ids to ignore when this rule is matched.
|
||||
| Name | gitlint version | Default | Description |
|
||||
| ------ | --------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| regex | >= 0.16.0 | None | [Python regex](https://docs.python.org/library/re.html) to match against the commit author name. On match, the commit will be ignored. |
|
||||
| ignore | >= 0.16.0 | all | Comma-separated list of rule names or ids to ignore when this rule is matched. |
|
||||
|
||||
### Examples
|
||||
|
||||
|
@ -435,4 +458,4 @@ regex=dependabot
|
|||
[ignore-by-author-name]
|
||||
regex=(.*)\[bot\](.*)
|
||||
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.
|
||||
|
||||
|
||||
Property | Type | Description
|
||||
-------------------------------| ---------------|-------------------
|
||||
commit.message | object | Python object representing the commit message
|
||||
commit.message.original | string | Original commit message as returned by git
|
||||
commit.message.full | string | Full commit message, with comments (lines starting with #) removed.
|
||||
commit.message.title | string | Title/subject of the commit message: the first line
|
||||
commit.message.body | string[] | List of lines in the body of the commit message (i.e. starting from the second line)
|
||||
commit.author_name | string | Name of the author, result of `git log --pretty=%aN`
|
||||
commit.author_email | string | Email of the author, result of `git log --pretty=%aE`
|
||||
commit.date | datetime | Python `datetime` object representing the time of commit
|
||||
commit.is_merge_commit | boolean | Boolean indicating whether the commit is a merge commit or not.
|
||||
commit.is_revert_commit | boolean | Boolean indicating whether the commit is a revert commit or not.
|
||||
commit.is_fixup_commit | boolean | Boolean indicating whether the commit is a fixup commit or not.
|
||||
commit.is_squash_commit | boolean | Boolean indicating whether the commit is a squash commit or not.
|
||||
commit.parents | string[] | List of parent commit `sha`s (only for merge commits).
|
||||
commit.changed_files | string[] | List of files changed in the commit (relative paths).
|
||||
commit.branches | string[] | List of branch names the commit is part of
|
||||
commit.context | object | Object pointing to the bigger git context that the commit is part of
|
||||
commit.context.current_branch | string | Name of the currently active branch (of local repo)
|
||||
commit.context.repository_path | string | Absolute path pointing to the git repository being linted
|
||||
commit.context.commits | object[] | List of commits gitlint is acting on, NOT all commits in the repo.
|
||||
| Property | Type | Description |
|
||||
| -------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| commit | `GitCommit` | Python object representing the commit |
|
||||
| commit.message | `GitCommitMessage` | Python object representing the commit message |
|
||||
| commit.message.original | `str` | Original commit message as returned by git |
|
||||
| commit.message.full | `str` | Full commit message, with comments (lines starting with #) removed. |
|
||||
| commit.message.title | `str` | Title/subject of the commit message: the first line |
|
||||
| commit.message.body | `str[]` | List of lines in the body of the commit message (i.e. starting from the second line) |
|
||||
| commit.author_name | `str` | Name of the author, result of `git log --pretty=%aN` |
|
||||
| commit.author_email | `str` | Email of the author, result of `git log --pretty=%aE` |
|
||||
| commit.date | `datetime.datetime` | Python `datetime` object representing the time of commit |
|
||||
| commit.is_merge_commit | `bool` | Boolean indicating whether the commit is a merge commit or not. |
|
||||
| commit.is_revert_commit | `bool` | Boolean indicating whether the commit is a revert commit or not. |
|
||||
| commit.is_fixup_commit | `bool` | Boolean indicating whether the commit is a fixup commit or not. |
|
||||
| commit.is_fixup_amend_commit | `bool` | Boolean indicating whether the commit is a (fixup) amend commit or not. |
|
||||
| commit.is_squash_commit | `bool` | Boolean indicating whether the commit is a squash commit or not. |
|
||||
| commit.parents | `str[]` | List of parent commit `sha`s (only for merge commits). |
|
||||
| commit.changed_files | `str[]` | List of files changed in the commit (relative paths). |
|
||||
| commit.changed_files_stats | `dict[str, GitChangedFilesStats]` | Dictionary mapping the changed files to a `GitChangedFilesStats` objects |
|
||||
| commit.changed_files_stats["path"].filepath | `pathlib.Path` | Relative path (compared to repo root) of the file that was changed. |
|
||||
| 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
|
||||
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:
|
||||
|
||||
Parameter | Type | Description
|
||||
--------------|---------|--------------------------------
|
||||
rule_id | string | Rule's unique string id
|
||||
message | string | Short description of the violation
|
||||
content | string | (optional) the violating part of commit or line
|
||||
line_nr | int | (optional) line number in the commit message where the violation occurs. **Automatically set to the correct line number for `LineRule`s if not set explicitly.**
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| rule_id | `str` | Rule's unique string id |
|
||||
| message | `str` | Short description of the violation |
|
||||
| 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.** |
|
||||
|
||||
A typical `validate(...)` implementation for a `CommitRule` would then be as follows:
|
||||
```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`:
|
||||
|
||||
Option Class | Use for
|
||||
------------------|--------------
|
||||
`StrOption ` | Strings
|
||||
`IntOption` | Integers. `IntOption` takes an optional `allow_negative` parameter if you want to allow negative integers.
|
||||
`BoolOption` | Booleans. Valid values: `true`, `false`. Case-insensitive.
|
||||
`ListOption` | List of strings. Comma separated.
|
||||
`PathOption` | Directory or file path. Takes an optional `type` parameter for specifying path type (`file`, `dir` (=default) or `both`).
|
||||
`RegexOption` | String representing a [Python-style regex](https://docs.python.org/library/re.html) - compiled and validated before rules are applied.
|
||||
| Option Class | Use for |
|
||||
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `StrOption ` | Strings |
|
||||
| `IntOption` | Integers. `IntOption` takes an optional `allow_negative` parameter if you want to allow negative integers. |
|
||||
| `BoolOption` | Booleans. Valid values: `true`, `false`. Case-insensitive. |
|
||||
| `ListOption` | List of strings. Comma separated. |
|
||||
| `PathOption` | Directory or file path. Takes an optional `type` parameter for specifying path type (`file`, `dir` (=default) or `both`). |
|
||||
| `RegexOption` | String representing a [Python-style regex](https://docs.python.org/library/re.html) - compiled and validated before rules are applied. |
|
||||
|
||||
!!! note
|
||||
Gitlint currently does not support options for all possible types (e.g. float, list of int, etc).
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from gitlint.rules import CommitRule, RuleViolation
|
||||
from gitlint.options import IntOption, ListOption
|
||||
|
||||
|
@ -27,20 +25,20 @@ class BodyMaxLineCount(CommitRule):
|
|||
id = "UC1"
|
||||
|
||||
# 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):
|
||||
self.log.debug("BodyMaxLineCount: This will be visible when running `gitlint --debug`")
|
||||
|
||||
line_count = len(commit.message.body)
|
||||
max_line_count = self.options['max-line-count'].value
|
||||
max_line_count = self.options["max-line-count"].value
|
||||
if 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)]
|
||||
|
||||
|
||||
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".
|
||||
"""
|
||||
|
||||
|
@ -61,8 +59,8 @@ class SignedOffBy(CommitRule):
|
|||
|
||||
|
||||
class BranchNamingConventions(CommitRule):
|
||||
""" 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/
|
||||
"""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/
|
||||
"""
|
||||
|
||||
# A rule MUST have a human friendly name
|
||||
|
@ -72,13 +70,13 @@ class BranchNamingConventions(CommitRule):
|
|||
id = "UC3"
|
||||
|
||||
# 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):
|
||||
self.log.debug("BranchNamingConventions: This line will be visible when running `gitlint --debug`")
|
||||
|
||||
violations = []
|
||||
allowed_branch_prefixes = self.options['branch-prefixes'].value
|
||||
allowed_branch_prefixes = self.options["branch-prefixes"].value
|
||||
for branch in commit.branches:
|
||||
valid_branch_name = False
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from gitlint.rules import ConfigurationRule
|
||||
from gitlint.options import IntOption
|
||||
|
||||
|
@ -36,7 +34,7 @@ class ReleaseConfigurationRule(ConfigurationRule):
|
|||
id = "UCR1"
|
||||
|
||||
# A rule MAY have an option_spec if its behavior should be configurable.
|
||||
options_spec = [IntOption('custom-verbosity', 2, "Gitlint verbosity for release commits")]
|
||||
options_spec = [IntOption("custom-verbosity", 2, "Gitlint verbosity for release commits")]
|
||||
|
||||
def apply(self, config, commit):
|
||||
self.log.debug("ReleaseConfigurationRule: This will be visible when running `gitlint --debug`")
|
||||
|
@ -44,7 +42,6 @@ class ReleaseConfigurationRule(ConfigurationRule):
|
|||
# If the commit title starts with 'Release', we want to modify
|
||||
# how all subsequent rules interpret that commit
|
||||
if commit.message.title.startswith("Release"):
|
||||
|
||||
# If your Release commit messages are auto-generated, the
|
||||
# body might contain trailing whitespace. Let's ignore that
|
||||
config.ignore.append("body-trailing-whitespace")
|
||||
|
@ -60,7 +57,7 @@ class ReleaseConfigurationRule(ConfigurationRule):
|
|||
# config.set_general_option(<general-option>, <value>)
|
||||
config.set_general_option("verbosity", 2)
|
||||
# Wwe can also use custom options to make this configurable
|
||||
config.set_general_option("verbosity", self.options['custom-verbosity'].value)
|
||||
config.set_general_option("verbosity", self.options["custom-verbosity"].value)
|
||||
|
||||
# Strip any lines starting with $ from the commit message
|
||||
# (this only affects how gitlint sees your commit message, it does
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from gitlint.rules import LineRule, RuleViolation, CommitMessageTitle
|
||||
from gitlint.options import ListOption
|
||||
|
||||
|
@ -21,8 +19,8 @@ that fits your needs.
|
|||
|
||||
|
||||
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
|
||||
name = "title-no-special-chars"
|
||||
|
@ -35,15 +33,20 @@ class SpecialChars(LineRule):
|
|||
target = CommitMessageTitle
|
||||
|
||||
# A rule MAY have an option_spec if its behavior should be configurable.
|
||||
options_spec = [ListOption('special-chars', ['$', '^', '%', '@', '!', '*', '(', ')'],
|
||||
"Comma separated list of characters that should not occur in the title")]
|
||||
options_spec = [
|
||||
ListOption(
|
||||
"special-chars",
|
||||
["$", "^", "%", "@", "!", "*", "(", ")"],
|
||||
"Comma separated list of characters that should not occur in the title",
|
||||
)
|
||||
]
|
||||
|
||||
def validate(self, line, _commit):
|
||||
self.log.debug("SpecialChars: This will be visible when running `gitlint --debug`")
|
||||
|
||||
violations = []
|
||||
# options can be accessed by looking them up by their name in self.options
|
||||
for char in self.options['special-chars'].value:
|
||||
for char in self.options["special-chars"].value:
|
||||
if char in line:
|
||||
msg = f"Title contains the special character '{char}'"
|
||||
violation = RuleViolation(self.id, msg, line)
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = "0.17.0"
|
||||
__version__ = "0.18.0"
|
||||
|
|
|
@ -1,31 +1,31 @@
|
|||
class PropertyCache:
|
||||
""" Mixin class providing a simple cache. """
|
||||
"""Mixin class providing a simple cache."""
|
||||
|
||||
def __init__(self):
|
||||
self._cache = {}
|
||||
|
||||
def _try_cache(self, cache_key, cache_populate_func):
|
||||
""" 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
|
||||
and then return the value from the cache. """
|
||||
"""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
|
||||
and then return the value from the cache."""
|
||||
if cache_key not in self._cache:
|
||||
cache_populate_func()
|
||||
return self._cache[cache_key]
|
||||
|
||||
|
||||
def cache(original_func=None, cachekey=None): # pylint: disable=unused-argument
|
||||
""" Cache decorator. Caches function return values.
|
||||
Requires the parent class to extend and initialize PropertyCache.
|
||||
Usage:
|
||||
# Use function name as cache key
|
||||
@cache
|
||||
def myfunc(args):
|
||||
...
|
||||
"""Cache decorator. Caches function return values.
|
||||
Requires the parent class to extend and initialize PropertyCache.
|
||||
Usage:
|
||||
# Use function name as cache key
|
||||
@cache
|
||||
def myfunc(args):
|
||||
...
|
||||
|
||||
# Specify cache key
|
||||
@cache(cachekey="foobar")
|
||||
def myfunc(args):
|
||||
...
|
||||
# Specify cache key
|
||||
@cache(cachekey="foobar")
|
||||
def myfunc(args):
|
||||
...
|
||||
"""
|
||||
|
||||
# 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():
|
||||
# Call decorated function and store its result in the cache
|
||||
args[0]._cache[cachekey] = func(*args)
|
||||
|
||||
return args[0]._try_cache(cachekey, cache_func_result)
|
||||
|
||||
return wrapped
|
||||
|
|
|
@ -11,6 +11,7 @@ import click
|
|||
import gitlint
|
||||
from gitlint.lint import GitLinter
|
||||
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 import hooks
|
||||
from gitlint.shell import shell
|
||||
|
@ -37,19 +38,29 @@ LOG = logging.getLogger("gitlint.cli")
|
|||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def setup_logging():
|
||||
""" Setup gitlint logging """
|
||||
"""Setup gitlint logging"""
|
||||
|
||||
# Root log, mostly used for debug
|
||||
root_log = logging.getLogger("gitlint")
|
||||
root_log.propagate = False # Don't propagate to child loggers, the gitlint root logger handles everything
|
||||
root_log.setLevel(logging.ERROR)
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter(LOG_FORMAT)
|
||||
handler.setFormatter(formatter)
|
||||
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():
|
||||
|
@ -62,10 +73,20 @@ def log_system_info():
|
|||
|
||||
|
||||
def build_config( # pylint: disable=too-many-arguments
|
||||
target, config_path, c, extra_path, ignore, contrib, ignore_stdin, staged, fail_without_commits, verbose,
|
||||
silent, debug
|
||||
target,
|
||||
config_path,
|
||||
c,
|
||||
extra_path,
|
||||
ignore,
|
||||
contrib,
|
||||
ignore_stdin,
|
||||
staged,
|
||||
fail_without_commits,
|
||||
verbose,
|
||||
silent,
|
||||
debug,
|
||||
):
|
||||
""" Creates a LintConfig object based on a set of commandline parameters. """
|
||||
"""Creates a LintConfig object based on a set of commandline parameters."""
|
||||
config_builder = LintConfigBuilder()
|
||||
# Config precedence:
|
||||
# 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
|
||||
if ignore:
|
||||
config_builder.set_option('general', 'ignore', ignore)
|
||||
config_builder.set_option("general", "ignore", ignore)
|
||||
|
||||
if contrib:
|
||||
config_builder.set_option('general', 'contrib', contrib)
|
||||
config_builder.set_option("general", "contrib", contrib)
|
||||
|
||||
if ignore_stdin:
|
||||
config_builder.set_option('general', 'ignore-stdin', ignore_stdin)
|
||||
config_builder.set_option("general", "ignore-stdin", ignore_stdin)
|
||||
|
||||
if silent:
|
||||
config_builder.set_option('general', 'verbosity', 0)
|
||||
config_builder.set_option("general", "verbosity", 0)
|
||||
elif verbose > 0:
|
||||
config_builder.set_option('general', 'verbosity', verbose)
|
||||
config_builder.set_option("general", "verbosity", verbose)
|
||||
|
||||
if extra_path:
|
||||
config_builder.set_option('general', 'extra-path', extra_path)
|
||||
config_builder.set_option("general", "extra-path", extra_path)
|
||||
|
||||
if target:
|
||||
config_builder.set_option('general', 'target', target)
|
||||
config_builder.set_option("general", "target", target)
|
||||
|
||||
if debug:
|
||||
config_builder.set_option('general', 'debug', debug)
|
||||
config_builder.set_option("general", "debug", debug)
|
||||
|
||||
if staged:
|
||||
config_builder.set_option('general', 'staged', staged)
|
||||
config_builder.set_option("general", "staged", staged)
|
||||
|
||||
if fail_without_commits:
|
||||
config_builder.set_option('general', 'fail-without-commits', fail_without_commits)
|
||||
config_builder.set_option("general", "fail-without-commits", fail_without_commits)
|
||||
|
||||
config = config_builder.build()
|
||||
|
||||
|
@ -113,7 +134,7 @@ def build_config( # pylint: disable=too-many-arguments
|
|||
|
||||
|
||||
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")
|
||||
# 1. An interactive terminal device (i.e. a TTY -> sys.stdin.isatty() or stat.S_ISCHR)
|
||||
# 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):
|
||||
""" 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
|
||||
from_commit_msg = GitContext.from_commit_msg
|
||||
if lint_config.staged:
|
||||
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:
|
||||
# 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)
|
||||
|
||||
if lint_config.staged:
|
||||
raise GitLintUsageError("The 'staged' option (--staged) can only be used when using '--msg-filename' or "
|
||||
"when piping data to gitlint via stdin.")
|
||||
raise GitLintUsageError(
|
||||
"The 'staged' option (--staged) can only be used when using '--msg-filename' or "
|
||||
"when piping data to gitlint via stdin."
|
||||
)
|
||||
|
||||
# 3. Fallback to reading from local repository
|
||||
LOG.debug("No --msg-filename flag, no or empty data passed to stdin. Using the local repo.")
|
||||
|
@ -177,11 +204,25 @@ def build_git_context(lint_config, msg_filename, commit_hash, refspec):
|
|||
if commit_hash and refspec:
|
||||
raise GitLintUsageError("--commit and --commits are mutually exclusive, use one or the other.")
|
||||
|
||||
return GitContext.from_local_repository(lint_config.target, refspec=refspec, commit_hash=commit_hash)
|
||||
# 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):
|
||||
""" Helper function to handle exceptions """
|
||||
"""Helper function to handle exceptions"""
|
||||
if isinstance(exc, GitContextError):
|
||||
click.echo(exc)
|
||||
ctx.exit(GIT_CONTEXT_ERROR_CODE)
|
||||
|
@ -194,7 +235,7 @@ def handle_gitlint_error(ctx, exc):
|
|||
|
||||
|
||||
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):
|
||||
self.config = config
|
||||
|
@ -205,29 +246,34 @@ class ContextObj:
|
|||
self.gitcontext = gitcontext
|
||||
|
||||
|
||||
# fmt: off
|
||||
@click.group(invoke_without_command=True, context_settings={'max_content_width': 120},
|
||||
epilog="When no COMMAND is specified, gitlint defaults to 'gitlint lint'.")
|
||||
@click.option('--target', envvar='GITLINT_TARGET',
|
||||
type=click.Path(exists=True, resolve_path=True, file_okay=False, readable=True),
|
||||
help="Path of the target git repository. [default: current working directory]")
|
||||
@click.option('-C', '--config', type=click.Path(exists=True, dir_okay=False, readable=True, resolve_path=True),
|
||||
@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}]")
|
||||
@click.option('-c', multiple=True,
|
||||
help="Config flags in format <rule>.<option>=<value> (e.g.: -c T1.line-length=80). " +
|
||||
"Flag can be used multiple times to set multiple config values.") # pylint: disable=bad-continuation
|
||||
@click.option('--commit', envvar='GITLINT_COMMIT', default=None, help="Hash (SHA) of specific commit to lint.")
|
||||
@click.option('--commits', envvar='GITLINT_COMMITS', default=None, help="The range of commits to lint. [default: HEAD]")
|
||||
@click.option('--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',
|
||||
help="Path to a directory or python module with extra user-defined rules",
|
||||
type=click.Path(exists=True, resolve_path=True, readable=True))
|
||||
@click.option('--ignore', envvar='GITLINT_IGNORE', default="", help="Ignore rules (comma-separated by id or name).")
|
||||
@click.option('--contrib', envvar='GITLINT_CONTRIB', default="",
|
||||
help="Contrib rules to enable (comma-separated by id or name).")
|
||||
@click.option('--msg-filename', type=click.File(), help="Path to a file containing a commit-msg.")
|
||||
@click.option('--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,
|
||||
help="Ignore any stdin data. Useful for running in CI server.")
|
||||
@click.option('--staged', envvar='GITLINT_STAGED', is_flag=True,
|
||||
help="Read staged commit meta-info from the local repository.")
|
||||
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,
|
||||
help="Hard fail when the target commit range is empty.")
|
||||
@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
|
||||
"""
|
||||
|
||||
try:
|
||||
if 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_system_info()
|
||||
|
||||
# Get the lint config from the commandline parameters and
|
||||
# store it in the context (click allows storing an arbitrary object in ctx.obj).
|
||||
config, config_builder = build_config(target, config, c, extra_path, ignore, contrib, ignore_stdin, staged,
|
||||
fail_without_commits, verbose, silent, debug)
|
||||
config, config_builder = build_config(target, config, c, extra_path, ignore, contrib, ignore_stdin,
|
||||
staged, fail_without_commits, verbose, silent, debug)
|
||||
LOG.debug("Configuration\n%s", config)
|
||||
|
||||
ctx.obj = ContextObj(config, config_builder, commit, commits, msg_filename)
|
||||
|
@ -268,12 +314,13 @@ def cli( # pylint: disable=too-many-arguments
|
|||
|
||||
except GitlintError as e:
|
||||
handle_gitlint_error(ctx, e)
|
||||
# fmt: on
|
||||
|
||||
|
||||
@cli.command("lint")
|
||||
@click.pass_context
|
||||
def lint(ctx):
|
||||
""" Lints a git repository [default command] """
|
||||
"""Lints a git repository [default command]"""
|
||||
lint_config = ctx.obj.config
|
||||
refspec = ctx.obj.refspec
|
||||
commit_hash = ctx.obj.commit_hash
|
||||
|
@ -295,7 +342,7 @@ def lint(ctx):
|
|||
raise GitLintUsageError(f'No commits in range "{refspec}"')
|
||||
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
|
||||
last_commit = gitcontext.commits[-1]
|
||||
|
||||
|
@ -334,7 +381,7 @@ def lint(ctx):
|
|||
@cli.command("install-hook")
|
||||
@click.pass_context
|
||||
def install_hook(ctx):
|
||||
""" Install gitlint as a git commit-msg hook. """
|
||||
"""Install gitlint as a git commit-msg hook."""
|
||||
try:
|
||||
hooks.GitHookInstaller.install_commit_msg_hook(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")
|
||||
@click.pass_context
|
||||
def uninstall_hook(ctx):
|
||||
""" Uninstall gitlint commit-msg hook. """
|
||||
"""Uninstall gitlint commit-msg hook."""
|
||||
try:
|
||||
hooks.GitHookInstaller.uninstall_commit_msg_hook(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")
|
||||
@click.pass_context
|
||||
def run_hook(ctx):
|
||||
""" Runs the gitlint commit-msg hook. """
|
||||
"""Runs the gitlint commit-msg hook."""
|
||||
|
||||
exit_code = 1
|
||||
while exit_code > 0:
|
||||
|
@ -378,16 +425,18 @@ def run_hook(ctx):
|
|||
|
||||
exit_code = e.exit_code
|
||||
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
|
||||
|
||||
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
|
||||
while value not in ["y", "n", "e"]:
|
||||
click.echo("Continue with commit anyways (this keeps the current commit message)? "
|
||||
"[y(es)/n(no)/e(dit)] ", nl=False)
|
||||
click.echo(
|
||||
"Continue with commit anyways (this keeps the current commit message)? [y(es)/n(no)/e(dit)] ",
|
||||
nl=False,
|
||||
)
|
||||
|
||||
# Ideally, we'd want to use click.getchar() or click.prompt() to get user's input here instead of
|
||||
# input(). However, those functions currently don't support getting answers from stdin.
|
||||
|
@ -431,15 +480,15 @@ def run_hook(ctx):
|
|||
@cli.command("generate-config")
|
||||
@click.pass_context
|
||||
def generate_config(ctx):
|
||||
""" Generates a sample gitlint config file. """
|
||||
path = click.prompt('Please specify a location for the sample gitlint config file', default=DEFAULT_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 = os.path.realpath(path)
|
||||
dir_name = os.path.dirname(path)
|
||||
if not os.path.exists(dir_name):
|
||||
click.echo(f"Error: Directory '{dir_name}' does not exist.", err=True)
|
||||
ctx.exit(USAGE_ERROR_CODE)
|
||||
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)
|
||||
|
||||
LintConfigGenerator.generate_config(path)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from configparser import ConfigParser, Error as ConfigParserError
|
||||
|
||||
import copy
|
||||
import io
|
||||
import re
|
||||
import os
|
||||
import shutil
|
||||
|
@ -16,8 +15,8 @@ from gitlint.exception import GitlintError
|
|||
|
||||
|
||||
def handle_option_error(func):
|
||||
""" Decorator that calls given method/function and handles any RuleOptionError gracefully by converting it to a
|
||||
LintConfigError. """
|
||||
"""Decorator that calls given method/function and handles any RuleOptionError gracefully by converting it to a
|
||||
LintConfigError."""
|
||||
|
||||
def wrapped(*args):
|
||||
try:
|
||||
|
@ -32,53 +31,62 @@ class LintConfigError(GitlintError):
|
|||
pass
|
||||
|
||||
|
||||
class LintConfig:
|
||||
""" Class representing gitlint configuration.
|
||||
Contains active config as well as number of methods to easily get/set the config.
|
||||
class LintConfig: # pylint: disable=too-many-instance-attributes
|
||||
"""Class representing gitlint configuration.
|
||||
Contains active config as well as number of methods to easily get/set the config.
|
||||
"""
|
||||
|
||||
# Default tuple of rule classes (tuple because immutable).
|
||||
default_rule_classes = (rules.IgnoreByTitle,
|
||||
rules.IgnoreByBody,
|
||||
rules.IgnoreBodyLines,
|
||||
rules.IgnoreByAuthorName,
|
||||
rules.TitleMaxLength,
|
||||
rules.TitleTrailingWhitespace,
|
||||
rules.TitleLeadingWhitespace,
|
||||
rules.TitleTrailingPunctuation,
|
||||
rules.TitleHardTab,
|
||||
rules.TitleMustNotContainWord,
|
||||
rules.TitleRegexMatches,
|
||||
rules.TitleMinLength,
|
||||
rules.BodyMaxLineLength,
|
||||
rules.BodyMinLength,
|
||||
rules.BodyMissing,
|
||||
rules.BodyTrailingWhitespace,
|
||||
rules.BodyHardTab,
|
||||
rules.BodyFirstLineEmpty,
|
||||
rules.BodyChangedFileMention,
|
||||
rules.BodyRegexMatches,
|
||||
rules.AuthorValidEmail)
|
||||
default_rule_classes = (
|
||||
rules.IgnoreByTitle,
|
||||
rules.IgnoreByBody,
|
||||
rules.IgnoreBodyLines,
|
||||
rules.IgnoreByAuthorName,
|
||||
rules.TitleMaxLength,
|
||||
rules.TitleTrailingWhitespace,
|
||||
rules.TitleLeadingWhitespace,
|
||||
rules.TitleTrailingPunctuation,
|
||||
rules.TitleHardTab,
|
||||
rules.TitleMustNotContainWord,
|
||||
rules.TitleRegexMatches,
|
||||
rules.TitleMinLength,
|
||||
rules.BodyMaxLineLength,
|
||||
rules.BodyMinLength,
|
||||
rules.BodyMissing,
|
||||
rules.BodyTrailingWhitespace,
|
||||
rules.BodyHardTab,
|
||||
rules.BodyFirstLineEmpty,
|
||||
rules.BodyChangedFileMention,
|
||||
rules.BodyRegexMatches,
|
||||
rules.AuthorValidEmail,
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.rules = RuleCollection(self.default_rule_classes)
|
||||
self._verbosity = options.IntOption('verbosity', 3, "Verbosity")
|
||||
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_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._verbosity = options.IntOption("verbosity", 3, "Verbosity")
|
||||
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_amend_commits = options.BoolOption(
|
||||
"ignore-fixup-amend-commits", True, "Ignore fixup amend commits"
|
||||
)
|
||||
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
|
||||
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._ignore = options.ListOption('ignore', [], 'List of rule-ids to ignore')
|
||||
self._contrib = options.ListOption('contrib', [], 'List of contrib-rules to enable')
|
||||
self._target = options.PathOption("target", os.path.realpath(os.getcwd()), target_description)
|
||||
self._ignore = options.ListOption("ignore", [], "List of rule-ids to ignore")
|
||||
self._contrib = options.ListOption("contrib", [], "List of contrib-rules to enable")
|
||||
self._config_path = None
|
||||
ignore_stdin_description = "Ignore any stdin data. Useful for running in CI server."
|
||||
self._ignore_stdin = options.BoolOption('ignore-stdin', False, ignore_stdin_description)
|
||||
self._staged = options.BoolOption('staged', False, "Read staged commit meta-info from the local repository.")
|
||||
self._fail_without_commits = options.BoolOption('fail-without-commits', False,
|
||||
"Hard fail when the target commit range is empty")
|
||||
self._ignore_stdin = options.BoolOption("ignore-stdin", False, ignore_stdin_description)
|
||||
self._staged = options.BoolOption("staged", False, "Read staged commit meta-info from the local repository.")
|
||||
self._fail_without_commits = options.BoolOption(
|
||||
"fail-without-commits", False, "Hard fail when the target commit range is empty"
|
||||
)
|
||||
self._regex_style_search = options.BoolOption(
|
||||
"regex-style-search", False, "Use `search` instead of `match` semantics for regex rules"
|
||||
)
|
||||
|
||||
@property
|
||||
def target(self):
|
||||
|
@ -118,6 +126,15 @@ class LintConfig:
|
|||
def ignore_fixup_commits(self, 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
|
||||
def ignore_squash_commits(self):
|
||||
return self._ignore_squash_commits.value
|
||||
|
@ -182,6 +199,15 @@ class LintConfig:
|
|||
def fail_without_commits(self, 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
|
||||
def extra_path(self):
|
||||
return self._extra_path.value if self._extra_path else None
|
||||
|
@ -193,9 +219,7 @@ class LintConfig:
|
|||
self._extra_path.set(value)
|
||||
else:
|
||||
self._extra_path = options.PathOption(
|
||||
'extra-path', value,
|
||||
"Path to a directory or module with extra user-defined rules",
|
||||
type='both'
|
||||
"extra-path", value, "Path to a directory or module with extra user-defined rules", type="both"
|
||||
)
|
||||
|
||||
# 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
|
||||
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:
|
||||
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 rule_id_or_name in self.contrib:
|
||||
rule_class = next((rc for rc in rule_classes if
|
||||
rule_id_or_name in (rc.id, rc.name)), False)
|
||||
rule_class = next((rc for rc in rule_classes if rule_id_or_name in (rc.id, rc.name)), False)
|
||||
|
||||
# If contrib rule exists, instantiate it and add it to the rules list
|
||||
if rule_class:
|
||||
self.rules.add_rule(rule_class, rule_class.id, {'is_contrib': True})
|
||||
self.rules.add_rule(rule_class, rule_class.id, {"is_contrib": True})
|
||||
else:
|
||||
raise LintConfigError(f"No contrib rule with id or name '{rule_id_or_name}' found.")
|
||||
|
||||
|
@ -250,14 +273,14 @@ class LintConfig:
|
|||
return option
|
||||
|
||||
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
|
||||
rule or option don't exist. """
|
||||
"""Returns the value of a given option for a given rule. LintConfigErrors will be raised if the
|
||||
rule or option don't exist."""
|
||||
option = self._get_option(rule_name_or_id, option_name)
|
||||
return 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.
|
||||
LintConfigErrors will be raised if the rule or option don't exist or if the value is invalid. """
|
||||
"""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."""
|
||||
option = self._get_option(rule_name_or_id, option_name)
|
||||
try:
|
||||
option.set(option_value)
|
||||
|
@ -275,45 +298,53 @@ class LintConfig:
|
|||
setattr(self, attr_name, option_value)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, LintConfig) and \
|
||||
self.rules == other.rules and \
|
||||
self.verbosity == other.verbosity and \
|
||||
self.target == other.target and \
|
||||
self.extra_path == other.extra_path and \
|
||||
self.contrib == other.contrib and \
|
||||
self.ignore_merge_commits == other.ignore_merge_commits and \
|
||||
self.ignore_fixup_commits == other.ignore_fixup_commits and \
|
||||
self.ignore_squash_commits == other.ignore_squash_commits and \
|
||||
self.ignore_revert_commits == other.ignore_revert_commits and \
|
||||
self.ignore_stdin == other.ignore_stdin and \
|
||||
self.staged == other.staged and \
|
||||
self.fail_without_commits == other.fail_without_commits and \
|
||||
self.debug == other.debug and \
|
||||
self.ignore == other.ignore and \
|
||||
self._config_path == other._config_path # noqa
|
||||
return (
|
||||
isinstance(other, LintConfig)
|
||||
and self.rules == other.rules
|
||||
and self.verbosity == other.verbosity
|
||||
and self.target == other.target
|
||||
and self.extra_path == other.extra_path
|
||||
and self.contrib == other.contrib
|
||||
and self.ignore_merge_commits == other.ignore_merge_commits
|
||||
and self.ignore_fixup_commits == other.ignore_fixup_commits
|
||||
and self.ignore_fixup_amend_commits == other.ignore_fixup_amend_commits
|
||||
and self.ignore_squash_commits == other.ignore_squash_commits
|
||||
and self.ignore_revert_commits == other.ignore_revert_commits
|
||||
and self.ignore_stdin == other.ignore_stdin
|
||||
and self.staged == other.staged
|
||||
and self.fail_without_commits == other.fail_without_commits
|
||||
and self.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):
|
||||
# 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"
|
||||
f"[GENERAL]\n"
|
||||
f"extra-path: {self.extra_path}\n"
|
||||
f"contrib: {self.contrib}\n"
|
||||
f"ignore: {','.join(self.ignore)}\n"
|
||||
f"ignore-merge-commits: {self.ignore_merge_commits}\n"
|
||||
f"ignore-fixup-commits: {self.ignore_fixup_commits}\n"
|
||||
f"ignore-squash-commits: {self.ignore_squash_commits}\n"
|
||||
f"ignore-revert-commits: {self.ignore_revert_commits}\n"
|
||||
f"ignore-stdin: {self.ignore_stdin}\n"
|
||||
f"staged: {self.staged}\n"
|
||||
f"fail-without-commits: {self.fail_without_commits}\n"
|
||||
f"verbosity: {self.verbosity}\n"
|
||||
f"debug: {self.debug}\n"
|
||||
f"target: {self.target}\n"
|
||||
f"[RULES]\n{self.rules}")
|
||||
return (
|
||||
f"config-path: {self._config_path}\n"
|
||||
"[GENERAL]\n"
|
||||
f"extra-path: {self.extra_path}\n"
|
||||
f"contrib: {self.contrib}\n"
|
||||
f"ignore: {','.join(self.ignore)}\n"
|
||||
f"ignore-merge-commits: {self.ignore_merge_commits}\n"
|
||||
f"ignore-fixup-commits: {self.ignore_fixup_commits}\n"
|
||||
f"ignore-fixup-amend-commits: {self.ignore_fixup_amend_commits}\n"
|
||||
f"ignore-squash-commits: {self.ignore_squash_commits}\n"
|
||||
f"ignore-revert-commits: {self.ignore_revert_commits}\n"
|
||||
f"ignore-stdin: {self.ignore_stdin}\n"
|
||||
f"staged: {self.staged}\n"
|
||||
f"fail-without-commits: {self.fail_without_commits}\n"
|
||||
f"regex-style-search: {self.regex_style_search}\n"
|
||||
f"verbosity: {self.verbosity}\n"
|
||||
f"debug: {self.debug}\n"
|
||||
f"target: {self.target}\n"
|
||||
f"[RULES]\n{self.rules}"
|
||||
)
|
||||
|
||||
|
||||
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):
|
||||
# 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
|
||||
|
||||
def add_rule(self, rule_class, rule_id, rule_attrs=None):
|
||||
""" 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
|
||||
rule_id is unique.
|
||||
:param rule_class python class representing the rule
|
||||
:param rule_id unique identifier for the rule. If not unique, it will
|
||||
overwrite the existing rule with that id
|
||||
:param rule_attrs dictionary of attributes to set on the instantiated rule obj
|
||||
"""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
|
||||
rule_id is unique.
|
||||
:param rule_class python class representing the rule
|
||||
:param rule_id unique identifier for the rule. If not unique, it will
|
||||
overwrite the existing rule with that id
|
||||
:param rule_attrs dictionary of attributes to set on the instantiated rule obj
|
||||
"""
|
||||
rule_obj = rule_class()
|
||||
rule_obj.id = rule_id
|
||||
|
@ -345,12 +376,12 @@ class RuleCollection:
|
|||
self._rules[rule_obj.id] = rule_obj
|
||||
|
||||
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:
|
||||
self.add_rule(rule_class, rule_class.id, rule_attrs)
|
||||
|
||||
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
|
||||
# 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
|
||||
|
@ -358,8 +389,7 @@ class RuleCollection:
|
|||
del self._rules[rule.id]
|
||||
|
||||
def __iter__(self):
|
||||
for rule in self._rules.values():
|
||||
yield rule
|
||||
yield from self._rules.values()
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, RuleCollection) and self._rules == other._rules
|
||||
|
@ -385,7 +415,7 @@ class RuleCollection:
|
|||
|
||||
|
||||
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
|
||||
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.
|
||||
|
@ -403,19 +433,19 @@ class LintConfigBuilder:
|
|||
self._config_blueprint[section][option_name] = option_value
|
||||
|
||||
def set_config_from_commit(self, commit):
|
||||
""" Given a git commit, applies config specified in the commit message.
|
||||
Supported:
|
||||
- gitlint-ignore: all
|
||||
"""Given a git commit, applies config specified in the commit message.
|
||||
Supported:
|
||||
- gitlint-ignore: all
|
||||
"""
|
||||
for line in commit.message.body:
|
||||
pattern = re.compile(r"^gitlint-ignore:\s*(.*)")
|
||||
matches = pattern.match(line)
|
||||
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):
|
||||
""" 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. """
|
||||
"""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."""
|
||||
for config_option in config_options:
|
||||
try:
|
||||
config_name, option_value = config_option.split("=", 1)
|
||||
|
@ -425,17 +455,18 @@ class LintConfigBuilder:
|
|||
self.set_option(rule_name, option_name, option_value)
|
||||
except ValueError as e: # raised if the config string is invalid
|
||||
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):
|
||||
""" Loads lint config from an ini-style config file """
|
||||
"""Loads lint config from an ini-style config file"""
|
||||
if not os.path.exists(filename):
|
||||
raise LintConfigError(f"Invalid file path: {filename}")
|
||||
self._config_path = os.path.realpath(filename)
|
||||
try:
|
||||
parser = ConfigParser()
|
||||
|
||||
with io.open(filename, encoding=DEFAULT_ENCODING) as config_file:
|
||||
with open(filename, encoding=DEFAULT_ENCODING) as config_file:
|
||||
parser.read_file(config_file, filename)
|
||||
|
||||
for section_name in parser.sections():
|
||||
|
@ -446,8 +477,8 @@ class LintConfigBuilder:
|
|||
raise LintConfigError(str(e)) from e
|
||||
|
||||
def _add_named_rule(self, config, qualified_rule_name):
|
||||
""" Adds a Named Rule to a given LintConfig object.
|
||||
IMPORTANT: This method does *NOT* overwrite existing Named Rules with the same canonical id.
|
||||
"""Adds a Named Rule to a given LintConfig object.
|
||||
IMPORTANT: This method does *NOT* overwrite existing Named Rules with the same canonical id.
|
||||
"""
|
||||
|
||||
# Split up named rule in its parts: the name/id that specifies the parent rule,
|
||||
|
@ -475,13 +506,13 @@ class LintConfigBuilder:
|
|||
|
||||
# Add the rule to the collection of rules if it's not there already
|
||||
if not config.rules.find_rule(canonical_id):
|
||||
config.rules.add_rule(parent_rule.__class__, canonical_id, {'is_named': True, 'name': canonical_name})
|
||||
config.rules.add_rule(parent_rule.__class__, canonical_id, {"is_named": True, "name": canonical_name})
|
||||
|
||||
return canonical_id
|
||||
|
||||
def build(self, config=None):
|
||||
""" Build a real LintConfig object by normalizing and validating the options that were previously set on this
|
||||
factory. """
|
||||
"""Build a real LintConfig object by normalizing and validating the options that were previously set on this
|
||||
factory."""
|
||||
# If we are passed a config object, then rebuild that object instead of building a new lintconfig object from
|
||||
# scratch
|
||||
if not config:
|
||||
|
@ -490,7 +521,7 @@ class LintConfigBuilder:
|
|||
config._config_path = self._config_path
|
||||
|
||||
# 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:
|
||||
for option_name, option_value in general_section.items():
|
||||
config.set_general_option(option_name, option_value)
|
||||
|
@ -499,7 +530,6 @@ class LintConfigBuilder:
|
|||
for option_name, option_value in section_dict.items():
|
||||
# Skip over the general section, as we've already done that above
|
||||
if section_name != "general":
|
||||
|
||||
# If the section name contains a colon (:), then this section is defining a Named Rule
|
||||
# Which means we need to instantiate that Named Rule in the config.
|
||||
if self.RULE_QUALIFIER_SYMBOL in section_name:
|
||||
|
@ -510,7 +540,7 @@ class LintConfigBuilder:
|
|||
return config
|
||||
|
||||
def clone(self):
|
||||
""" Creates an exact copy of a LintConfigBuilder. """
|
||||
"""Creates an exact copy of a LintConfigBuilder."""
|
||||
builder = LintConfigBuilder()
|
||||
builder._config_blueprint = copy.deepcopy(self._config_blueprint)
|
||||
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:
|
||||
@staticmethod
|
||||
def generate_config(dest):
|
||||
""" Generates a gitlint config file at the given destination location.
|
||||
Expects that the given ```dest``` points to a valid destination. """
|
||||
"""Generates a gitlint config file at the given destination location.
|
||||
Expects that the given ```dest``` points to a valid destination."""
|
||||
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):
|
||||
""" 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"
|
||||
id = "CT1"
|
||||
|
@ -31,7 +31,7 @@ class ConventionalCommit(LineRule):
|
|||
else:
|
||||
line_commit_type = match.group(1)
|
||||
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))
|
||||
|
||||
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
|
||||
|
||||
|
||||
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".
|
||||
"""
|
||||
|
||||
|
|
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:
|
||||
""" 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):
|
||||
self.config = lint_config
|
||||
|
||||
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
|
||||
will only be outputted if the given verbosity exactly matches the config's verbosity. """
|
||||
"""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."""
|
||||
if exact:
|
||||
if self.config.verbosity == verbosity:
|
||||
stream.write(message + "\n")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
|
||||
class GitlintError(Exception):
|
||||
""" Based Exception class for all gitlint exceptions """
|
||||
"""Based Exception class for all gitlint exceptions"""
|
||||
|
||||
pass
|
||||
|
|
|
@ -14,13 +14,14 @@
|
|||
# verbosity should be a value between 1 and 3, the commandline -v flags take precedence over this
|
||||
# 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-revert-commits=true
|
||||
# ignore-fixup-commits=true
|
||||
# ignore-fixup-amend-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
|
||||
|
||||
# Fetch additional meta-data from the local repository when manually passing a
|
||||
|
@ -33,6 +34,11 @@
|
|||
# Disabled by default.
|
||||
# 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.
|
||||
# debug=true
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import arrow
|
||||
|
||||
from gitlint import shell as sh
|
||||
|
||||
# import exceptions separately, this makes it a little easier to mock them out in the unit tests
|
||||
from gitlint.shell import CommandNotFound, ErrorReturnCode
|
||||
|
||||
|
@ -18,15 +20,17 @@ LOG = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class GitContextError(GitlintError):
|
||||
""" Exception indicating there is an issue with the git context """
|
||||
"""Exception indicating there is an issue with the git context"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class GitNotInstalledError(GitContextError):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"'git' command not found. You need to install git to use gitlint on a local repository. " +
|
||||
"See https://git-scm.com/book/en/v2/Getting-Started-Installing-Git on how to install git.")
|
||||
"'git' command not found. You need to install git to use gitlint on a local repository. "
|
||||
"See https://git-scm.com/book/en/v2/Getting-Started-Installing-Git on how to install git."
|
||||
)
|
||||
|
||||
|
||||
class GitExitCodeError(GitContextError):
|
||||
|
@ -37,8 +41,8 @@ class GitExitCodeError(GitContextError):
|
|||
|
||||
|
||||
def _git(*command_parts, **kwargs):
|
||||
""" Convenience function for running git commands. Automatically deals with exceptions and unicode. """
|
||||
git_kwargs = {'_tty_out': False}
|
||||
"""Convenience function for running git commands. Automatically deals with exceptions and unicode."""
|
||||
git_kwargs = {"_tty_out": False}
|
||||
git_kwargs.update(kwargs)
|
||||
try:
|
||||
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
|
||||
# get an exception (which is the default sh behavior for non-zero exit codes) and so the user is expecting
|
||||
# a non-zero exit code -> just return the entire result
|
||||
if hasattr(result, 'exit_code') and result.exit_code > 0:
|
||||
if hasattr(result, "exit_code") and result.exit_code > 0:
|
||||
return result
|
||||
return str(result)
|
||||
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
|
||||
error_msg = e.stderr.strip()
|
||||
error_msg_lower = error_msg.lower()
|
||||
if '_cwd' in git_kwargs and b"not a git repository" in error_msg_lower:
|
||||
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
|
||||
|
||||
if (b"does not have any commits yet" in error_msg_lower or
|
||||
b"ambiguous argument 'head': unknown revision" in error_msg_lower):
|
||||
if (
|
||||
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."
|
||||
raise GitContextError(msg) from e
|
||||
|
||||
|
@ -66,34 +72,54 @@ def _git(*command_parts, **kwargs):
|
|||
|
||||
|
||||
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", "")
|
||||
|
||||
|
||||
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])
|
||||
# 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 = "#"
|
||||
return commentchar.replace("\n", "")
|
||||
|
||||
|
||||
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 = hooks_dir.replace("\n", "")
|
||||
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 representing a git commit message. A commit message consists of the following:
|
||||
- context: The `GitContext` this commit message is part of
|
||||
- original: The actual commit message as returned by `git log`
|
||||
- full: original, but stripped of any comments
|
||||
- title: the first line of full
|
||||
- body: all lines following the title
|
||||
"""Class representing a git commit message. A commit message consists of the following:
|
||||
- context: The `GitContext` this commit message is part of
|
||||
- original: The actual commit message as returned by `git log`
|
||||
- full: original, but stripped of any comments
|
||||
- title: the first line of full
|
||||
- body: all lines following the title
|
||||
"""
|
||||
|
||||
def __init__(self, context, original=None, full=None, title=None, body=None):
|
||||
self.context = context
|
||||
self.original = original
|
||||
|
@ -103,7 +129,7 @@ class GitCommitMessage:
|
|||
|
||||
@staticmethod
|
||||
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()
|
||||
cutline = f"{context.commentchar} ------------------------ >8 ------------------------"
|
||||
try:
|
||||
|
@ -120,19 +146,59 @@ class GitCommitMessage:
|
|||
return self.full
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, GitCommitMessage) and self.original == other.original
|
||||
and self.full == other.full and self.title == other.title and self.body == other.body) # noqa
|
||||
return (
|
||||
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 representing a git commit.
|
||||
A commit consists of: context, message, author name, author email, date, list of parent commit shas,
|
||||
list of changed files, list of branch names.
|
||||
In the context of gitlint, only the git context and commit message are required.
|
||||
"""Class representing a git commit.
|
||||
A commit consists of: context, message, author name, author email, date, list of parent commit shas,
|
||||
list of changed files, list of branch names.
|
||||
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
|
||||
author_email=None, parents=None, changed_files=None, branches=None):
|
||||
def __init__(
|
||||
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.message = message
|
||||
self.sha = sha
|
||||
|
@ -140,7 +206,7 @@ class GitCommit:
|
|||
self.author_name = author_name
|
||||
self.author_email = author_email
|
||||
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 []
|
||||
|
||||
@property
|
||||
|
@ -155,57 +221,87 @@ class GitCommit:
|
|||
def is_squash_commit(self):
|
||||
return self.message.title.startswith("squash!")
|
||||
|
||||
@property
|
||||
def is_fixup_amend_commit(self):
|
||||
return self.message.title.startswith("amend!")
|
||||
|
||||
@property
|
||||
def is_revert_commit(self):
|
||||
return self.message.title.startswith("Revert")
|
||||
|
||||
@property
|
||||
def changed_files(self):
|
||||
return list(self.changed_files_stats.keys())
|
||||
|
||||
def __str__(self):
|
||||
date_str = arrow.get(self.date).format(GIT_TIMEFORMAT) if self.date else None
|
||||
return (f"--- Commit Message ----\n{self.message}\n"
|
||||
"--- Meta info ---------\n"
|
||||
f"Author: {self.author_name} <{self.author_email}>\n"
|
||||
f"Date: {date_str}\n"
|
||||
f"is-merge-commit: {self.is_merge_commit}\n"
|
||||
f"is-fixup-commit: {self.is_fixup_commit}\n"
|
||||
f"is-squash-commit: {self.is_squash_commit}\n"
|
||||
f"is-revert-commit: {self.is_revert_commit}\n"
|
||||
f"Branches: {self.branches}\n"
|
||||
f"Changed Files: {self.changed_files}\n"
|
||||
"-----------------------")
|
||||
|
||||
if len(self.changed_files_stats) > 0:
|
||||
changed_files_stats_str = "\n " + "\n ".join([str(stats) for stats in self.changed_files_stats.values()])
|
||||
else:
|
||||
changed_files_stats_str = " {}"
|
||||
|
||||
return (
|
||||
f"--- Commit Message ----\n{self.message}\n"
|
||||
"--- Meta info ---------\n"
|
||||
f"Author: {self.author_name} <{self.author_email}>\n"
|
||||
f"Date: {date_str}\n"
|
||||
f"is-merge-commit: {self.is_merge_commit}\n"
|
||||
f"is-fixup-commit: {self.is_fixup_commit}\n"
|
||||
f"is-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):
|
||||
# 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
|
||||
and self.sha == other.sha and self.author_name == other.author_name
|
||||
and self.author_email == other.author_email
|
||||
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_squash_commit == other.is_squash_commit and self.is_revert_commit == other.is_revert_commit
|
||||
and self.changed_files == other.changed_files and self.branches == other.branches) # noqa
|
||||
return (
|
||||
isinstance(other, GitCommit)
|
||||
and self.message == other.message
|
||||
and self.sha == other.sha
|
||||
and self.author_name == other.author_name
|
||||
and self.author_email == other.author_email
|
||||
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 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
|
||||
property is accessed for the first time. Properties are then cached for subsequent access.
|
||||
"""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
|
||||
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
|
||||
PropertyCache.__init__(self)
|
||||
self.context = context
|
||||
self.sha = sha
|
||||
|
||||
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"
|
||||
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
|
||||
|
||||
# "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
|
||||
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,
|
||||
'parents': commit_parents, 'is_merge_commit': commit_is_merge_commit})
|
||||
self._cache.update(
|
||||
{
|
||||
"message": commit_msg_obj,
|
||||
"author_name": name,
|
||||
"author_email": email,
|
||||
"date": commit_date,
|
||||
"parents": commit_parents,
|
||||
"is_merge_commit": commit_is_merge_commit,
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
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
|
||||
# from the git CLI and not part of the branch name. See https://git-scm.com/docs/git-check-ref-format
|
||||
# We also drop the last empty line from the output.
|
||||
self._cache['branches'] = [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)
|
||||
|
||||
|
@ -260,20 +364,22 @@ class LocalGitCommit(GitCommit, PropertyCache):
|
|||
return self._try_cache("is_merge_commit", self._log)
|
||||
|
||||
@property
|
||||
def changed_files(self):
|
||||
def cache_changed_files():
|
||||
self._cache['changed_files'] = _git("diff-tree", "--no-commit-id", "--name-only", "-r", "--root",
|
||||
self.sha, _cwd=self.context.repository_path).split()
|
||||
def changed_files_stats(self):
|
||||
def cache_changed_files_stats():
|
||||
changed_files_stats_raw = _git(
|
||||
"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 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
|
||||
time, since the commit hasn't happened yet. However, we can make educated guesses based on existing repository
|
||||
information.
|
||||
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
|
||||
information.
|
||||
"""
|
||||
|
||||
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]
|
||||
|
||||
@property
|
||||
def changed_files(self):
|
||||
return _git("diff", "--staged", "--name-only", "-r", _cwd=self.context.repository_path).split()
|
||||
def changed_files_stats(self):
|
||||
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 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.
|
||||
"""
|
||||
|
||||
|
@ -337,12 +447,16 @@ class GitContext(PropertyCache):
|
|||
@property
|
||||
@cache
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
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.
|
||||
"""
|
||||
context = GitContext()
|
||||
|
@ -353,7 +467,7 @@ class GitContext(PropertyCache):
|
|||
|
||||
@staticmethod
|
||||
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 repository_path: Path to the git repository to retrieve the context from
|
||||
"""
|
||||
|
@ -364,8 +478,8 @@ class GitContext(PropertyCache):
|
|||
return context
|
||||
|
||||
@staticmethod
|
||||
def from_local_repository(repository_path, refspec=None, commit_hash=None):
|
||||
""" Retrieves the git context from a local git repository.
|
||||
def from_local_repository(repository_path, refspec=None, commit_hashes=None):
|
||||
"""Retrieves the git context from a local git repository.
|
||||
:param repository_path: Path to the git repository to retrieve the context from
|
||||
:param refspec: The commit(s) to retrieve (mutually exclusive with `commit_hash`)
|
||||
:param commit_hash: Hash of the commit to retrieve (mutually exclusive with `refspec`)
|
||||
|
@ -375,11 +489,13 @@ class GitContext(PropertyCache):
|
|||
|
||||
if refspec:
|
||||
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
|
||||
# return it to us. This way we verify that the passed hash is a valid hash for the target repo and we
|
||||
# also convert it to the full hash format (we might have been passed a short hash).
|
||||
sha_list = [_git("log", "-1", commit_hash, "--pretty=%H", _cwd=repository_path).replace("\n", "")]
|
||||
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
|
||||
# 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
|
||||
|
@ -393,6 +509,10 @@ class GitContext(PropertyCache):
|
|||
return context
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, GitContext) and self.commits == other.commits
|
||||
and self.repository_path == other.repository_path
|
||||
and self.commentchar == other.commentchar and self.current_branch == other.current_branch) # noqa
|
||||
return (
|
||||
isinstance(other, GitContext)
|
||||
and self.commits == other.commits
|
||||
and self.repository_path == other.repository_path
|
||||
and self.commentchar == other.commentchar
|
||||
and self.current_branch == other.current_branch
|
||||
)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import io
|
||||
import shutil
|
||||
import os
|
||||
import stat
|
||||
|
@ -17,7 +16,7 @@ class GitHookInstallerError(GitlintError):
|
|||
|
||||
|
||||
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
|
||||
def commit_msg_hook_path(lint_config):
|
||||
|
@ -25,7 +24,7 @@ class GitHookInstaller:
|
|||
|
||||
@staticmethod
|
||||
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)
|
||||
if not os.path.isdir(hooks_dir):
|
||||
raise GitHookInstallerError(f"{target} is not a git repository.")
|
||||
|
@ -36,8 +35,9 @@ class GitHookInstaller:
|
|||
dest_path = GitHookInstaller.commit_msg_hook_path(lint_config)
|
||||
if os.path.exists(dest_path):
|
||||
raise GitHookInstallerError(
|
||||
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.")
|
||||
f"There is already a commit-msg hook file present in {dest_path}.\n"
|
||||
"gitlint currently does not support appending to an existing commit-msg file."
|
||||
)
|
||||
|
||||
# copy hook file
|
||||
shutil.copy(COMMIT_MSG_HOOK_SRC_PATH, dest_path)
|
||||
|
@ -52,11 +52,13 @@ class GitHookInstaller:
|
|||
if not os.path.exists(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()
|
||||
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" + \
|
||||
"Uninstallation of 3th party or modified gitlint hooks is not supported."
|
||||
msg = (
|
||||
f"The commit-msg hook in {dest_path} was not installed by gitlint (or it was modified).\n"
|
||||
"Uninstallation of 3th party or modified gitlint hooks is not supported."
|
||||
)
|
||||
raise GitHookInstallerError(msg)
|
||||
|
||||
# If we are sure it's a gitlint hook, go ahead and remove it
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
import logging
|
||||
from gitlint import rules as gitlint_rules
|
||||
from gitlint import display
|
||||
from gitlint.deprecation import Deprecation
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
|
||||
|
||||
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):
|
||||
self.config = config
|
||||
|
@ -16,34 +17,48 @@ class GitLinter:
|
|||
self.display = display.Display(config)
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def configuration_rules(self):
|
||||
return [rule for rule in self.config.rules if
|
||||
isinstance(rule, gitlint_rules.ConfigurationRule) and not self.should_ignore_rule(rule)]
|
||||
return [
|
||||
rule
|
||||
for rule in self.config.rules
|
||||
if isinstance(rule, gitlint_rules.ConfigurationRule) and not self.should_ignore_rule(rule)
|
||||
]
|
||||
|
||||
@property
|
||||
def title_line_rules(self):
|
||||
return [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)]
|
||||
return [
|
||||
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
|
||||
def body_line_rules(self):
|
||||
return [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)]
|
||||
return [
|
||||
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
|
||||
def commit_rules(self):
|
||||
return [rule for rule in self.config.rules if isinstance(rule, gitlint_rules.CommitRule) and
|
||||
not self.should_ignore_rule(rule)]
|
||||
return [
|
||||
rule
|
||||
for rule in self.config.rules
|
||||
if isinstance(rule, gitlint_rules.CommitRule) and not self.should_ignore_rule(rule)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
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 = []
|
||||
line_nr = line_nr_start
|
||||
for line in lines:
|
||||
|
@ -58,7 +73,7 @@ class GitLinter:
|
|||
|
||||
@staticmethod
|
||||
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 = []
|
||||
for rule in rules:
|
||||
violations = rule.validate(commit)
|
||||
|
@ -67,19 +82,21 @@ class GitLinter:
|
|||
return all_violations
|
||||
|
||||
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("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
|
||||
for rule in self.configuration_rules:
|
||||
rule.apply(self.config, commit)
|
||||
|
||||
# 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:
|
||||
if getattr(commit, f"is_{commit_type}_commit") and \
|
||||
getattr(self.config, f"ignore_{commit_type}_commits"):
|
||||
if getattr(commit, f"is_{commit_type}_commit") and getattr(self.config, f"ignore_{commit_type}_commits"):
|
||||
return []
|
||||
|
||||
violations = []
|
||||
|
@ -95,12 +112,12 @@ class GitLinter:
|
|||
return 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:
|
||||
line_nr = v.line_nr if v.line_nr else "-"
|
||||
self.display.e(f"{line_nr}: {v.rule_id}", exact=True)
|
||||
self.display.ee(f"{line_nr}: {v.rule_id} {v.message}", exact=True)
|
||||
if v.content:
|
||||
self.display.eee(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:
|
||||
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):
|
||||
""" 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):
|
||||
if value is None:
|
||||
|
@ -22,10 +22,10 @@ class RuleOptionError(GitlintError):
|
|||
|
||||
|
||||
class RuleOption:
|
||||
""" Base class representing a configurable part (i.e. option) of a rule (e.g. the max-length of the title-max-line
|
||||
rule).
|
||||
This class should not be used directly. Instead, use on the derived classes like StrOption, IntOption to set
|
||||
options of a particular type like int, str, etc.
|
||||
"""Base class representing a configurable part (i.e. option) of a rule (e.g. the max-length of the title-max-line
|
||||
rule).
|
||||
This class should not be used directly. Instead, use on the derived classes like StrOption, IntOption to set
|
||||
options of a particular type like int, str, etc.
|
||||
"""
|
||||
|
||||
def __init__(self, name, value, description):
|
||||
|
@ -36,7 +36,7 @@ class RuleOption:
|
|||
|
||||
@abstractmethod
|
||||
def set(self, value):
|
||||
""" Validates and sets the option's value """
|
||||
"""Validates and sets the option's value"""
|
||||
pass # pragma: no cover
|
||||
|
||||
def __str__(self):
|
||||
|
@ -76,18 +76,16 @@ class IntOption(RuleOption):
|
|||
|
||||
|
||||
class BoolOption(RuleOption):
|
||||
|
||||
# explicit choice to not annotate with @allow_none: Booleans must be False or True, they cannot be unset.
|
||||
def set(self, value):
|
||||
value = 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'")
|
||||
self.value = value == 'true'
|
||||
self.value = value == "true"
|
||||
|
||||
|
||||
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
|
||||
def set(self, value):
|
||||
|
@ -100,7 +98,7 @@ class ListOption(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"):
|
||||
self.type = type
|
||||
|
@ -112,16 +110,17 @@ class PathOption(RuleOption):
|
|||
|
||||
error_msg = ""
|
||||
|
||||
if self.type == 'dir':
|
||||
if self.type == "dir":
|
||||
if not os.path.isdir(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):
|
||||
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):
|
||||
error_msg = (f"Option {self.name} must be either an existing directory or file "
|
||||
f"(current value: '{value}')")
|
||||
error_msg = (
|
||||
f"Option {self.name} must be either an existing directory or file (current value: '{value}')"
|
||||
)
|
||||
else:
|
||||
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):
|
||||
|
||||
@allow_none
|
||||
def set(self, value):
|
||||
try:
|
||||
|
|
|
@ -31,7 +31,7 @@ def find_rule_classes(extra_path):
|
|||
|
||||
# Filter out files that are not python modules
|
||||
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
|
||||
# add their parent dir to the sys.path (this fixes import issues with pypy2).
|
||||
if filename == "__init__.py":
|
||||
|
@ -61,13 +61,19 @@ def find_rule_classes(extra_path):
|
|||
# 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
|
||||
# 3) is it a subclass of rule
|
||||
rule_classes.extend([clazz for _, clazz in inspect.getmembers(sys.modules[module])
|
||||
if
|
||||
inspect.isclass(clazz) and # check isclass to ensure clazz.__module__ exists
|
||||
clazz.__module__ == module and # ignore imported classes
|
||||
(issubclass(clazz, rules.LineRule) or
|
||||
issubclass(clazz, rules.CommitRule) or
|
||||
issubclass(clazz, rules.ConfigurationRule))])
|
||||
rule_classes.extend(
|
||||
[
|
||||
clazz
|
||||
for _, clazz in inspect.getmembers(sys.modules[module])
|
||||
if inspect.isclass(clazz) # check isclass to ensure clazz.__module__ exists
|
||||
and clazz.__module__ == module # ignore imported classes
|
||||
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
|
||||
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
|
||||
if not (issubclass(clazz, rules.LineRule) or issubclass(clazz, rules.CommitRule)
|
||||
or issubclass(clazz, rules.ConfigurationRule)):
|
||||
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__}"
|
||||
if not (
|
||||
issubclass(clazz, rules.LineRule)
|
||||
or issubclass(clazz, rules.CommitRule)
|
||||
or issubclass(clazz, rules.ConfigurationRule)
|
||||
):
|
||||
msg = (
|
||||
f"{rule_type} rule class '{clazz.__name__}' "
|
||||
f"must extend from {rules.CommitRule.__module__}.{rules.LineRule.__name__}, "
|
||||
f"{rules.CommitRule.__module__}.{rules.CommitRule.__name__} or "
|
||||
f"{rules.CommitRule.__module__}.{rules.ConfigurationRule.__name__}"
|
||||
)
|
||||
raise rules.UserRuleError(msg)
|
||||
|
||||
# Rules must have an id attribute
|
||||
if not hasattr(clazz, 'id') or clazz.id is None or not clazz.id:
|
||||
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")
|
||||
|
||||
# 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"
|
||||
raise rules.UserRuleError(msg)
|
||||
|
||||
# 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")
|
||||
|
||||
# if set, options_spec must be a list of RuleOption
|
||||
if not isinstance(clazz.options_spec, list):
|
||||
msg = f"The options_spec attribute of {rule_type.lower()} rule class '{clazz.__name__}' " + \
|
||||
f"must be a list of {options.RuleOption.__module__}.{options.RuleOption.__name__}"
|
||||
msg = (
|
||||
f"The options_spec attribute of {rule_type.lower()} rule class '{clazz.__name__}' "
|
||||
f"must be a list of {options.RuleOption.__module__}.{options.RuleOption.__name__}"
|
||||
)
|
||||
raise rules.UserRuleError(msg)
|
||||
|
||||
# check that all items in options_spec are actual gitlint options
|
||||
for option in clazz.options_spec:
|
||||
if not isinstance(option, options.RuleOption):
|
||||
msg = 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__}"
|
||||
msg = (
|
||||
f"The options_spec attribute of {rule_type.lower()} rule class '{clazz.__name__}' "
|
||||
f"must be a list of {options.RuleOption.__module__}.{options.RuleOption.__name__}"
|
||||
)
|
||||
raise rules.UserRuleError(msg)
|
||||
|
||||
# Line/Commit rules must have a `validate` method
|
||||
# We use isroutine() as it's both python 2 and 3 compatible. Details: http://stackoverflow.com/a/17019998/381010
|
||||
if (issubclass(clazz, rules.LineRule) or issubclass(clazz, rules.CommitRule)):
|
||||
if not hasattr(clazz, 'validate') or not inspect.isroutine(clazz.validate):
|
||||
if issubclass(clazz, rules.LineRule) or issubclass(clazz, rules.CommitRule):
|
||||
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")
|
||||
# Configuration rules must have an `apply` method
|
||||
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"
|
||||
raise rules.UserRuleError(msg)
|
||||
|
||||
# LineRules must have a valid target: rules.CommitMessageTitle or rules.CommitMessageBody
|
||||
if issubclass(clazz, rules.LineRule):
|
||||
if clazz.target not in [rules.CommitMessageTitle, rules.CommitMessageBody]:
|
||||
msg = f"The target attribute of the {rule_type.lower()} LineRule class '{clazz.__name__}' " + \
|
||||
f"must be either {rules.CommitMessageTitle.__module__}.{rules.CommitMessageTitle.__name__} " + \
|
||||
f"or {rules.CommitMessageTitle.__module__}.{rules.CommitMessageBody.__name__}"
|
||||
msg = (
|
||||
f"The target attribute of the {rule_type.lower()} LineRule class '{clazz.__name__}' "
|
||||
f"must be either {rules.CommitMessageTitle.__module__}.{rules.CommitMessageTitle.__name__} "
|
||||
f"or {rules.CommitMessageTitle.__module__}.{rules.CommitMessageBody.__name__}"
|
||||
)
|
||||
raise rules.UserRuleError(msg)
|
||||
|
|
|
@ -5,15 +5,18 @@ import re
|
|||
|
||||
from gitlint.options import IntOption, BoolOption, StrOption, ListOption, RegexOption
|
||||
from gitlint.exception import GitlintError
|
||||
from gitlint.deprecation import Deprecation
|
||||
|
||||
|
||||
class Rule:
|
||||
""" Class representing gitlint rules. """
|
||||
"""Class representing gitlint rules."""
|
||||
|
||||
options_spec = []
|
||||
id = None
|
||||
name = None
|
||||
target = None
|
||||
_log = None
|
||||
_log_deprecated_regex_style_search = None
|
||||
|
||||
def __init__(self, opts=None):
|
||||
if not opts:
|
||||
|
@ -33,48 +36,58 @@ class Rule:
|
|||
return self._log
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.id == other.id and self.name == other.name and \
|
||||
self.options == other.options and self.target == other.target # noqa
|
||||
return (
|
||||
self.id == other.id
|
||||
and self.name == other.name
|
||||
and self.options == other.options
|
||||
and self.target == other.target
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.id} {self.name}" # pragma: no cover
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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).
|
||||
Each LineRule MUST have a target specified. """
|
||||
Each LineRule MUST have a target specified."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class RuleViolation:
|
||||
""" Class representing a violation of a rule. I.e.: When a rule is broken, the rule will instantiate this class
|
||||
to indicate how and where the rule was broken. """
|
||||
"""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."""
|
||||
|
||||
def __init__(self, rule_id, message, content=None, line_nr=None):
|
||||
self.rule_id = rule_id
|
||||
|
@ -88,22 +101,23 @@ class RuleViolation:
|
|||
return equal
|
||||
|
||||
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):
|
||||
""" 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
|
||||
|
||||
|
||||
class MaxLineLength(LineRule):
|
||||
name = "max-line-length"
|
||||
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})"
|
||||
|
||||
def validate(self, line, _commit):
|
||||
max_length = self.options['line-length'].value
|
||||
max_length = self.options["line-length"].value
|
||||
if len(line) > max_length:
|
||||
return [RuleViolation(self.id, self.violation_message.format(len(line), max_length), line)]
|
||||
|
||||
|
@ -130,15 +144,16 @@ class HardTab(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
|
||||
a violation, e.g: WIPING is not a violation if 'WIP' is a word that is not allowed.) """
|
||||
"""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.)"""
|
||||
|
||||
name = "line-must-not-contain"
|
||||
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}"
|
||||
|
||||
def validate(self, line, _commit):
|
||||
strings = self.options['words'].value
|
||||
strings = self.options["words"].value
|
||||
violations = []
|
||||
for string in strings:
|
||||
regex = re.compile(rf"\b{string.lower()}\b", re.IGNORECASE | re.UNICODE)
|
||||
|
@ -163,7 +178,7 @@ class TitleMaxLength(MaxLineLength):
|
|||
name = "title-max-length"
|
||||
id = "T1"
|
||||
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})"
|
||||
|
||||
|
||||
|
@ -180,7 +195,7 @@ class TitleTrailingPunctuation(LineRule):
|
|||
target = CommitMessageTitle
|
||||
|
||||
def validate(self, title, _commit):
|
||||
punctuation_marks = '?:!.,;'
|
||||
punctuation_marks = "?:!.,;"
|
||||
for punctuation_mark in punctuation_marks:
|
||||
if title.endswith(punctuation_mark):
|
||||
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"
|
||||
id = "T5"
|
||||
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)"
|
||||
|
||||
|
||||
|
@ -212,14 +227,14 @@ class TitleRegexMatches(LineRule):
|
|||
name = "title-match-regex"
|
||||
id = "T7"
|
||||
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):
|
||||
# If no regex is specified, immediately return
|
||||
if not self.options['regex'].value:
|
||||
if not self.options["regex"].value:
|
||||
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})"
|
||||
return [RuleViolation(self.id, violation_msg, title)]
|
||||
|
||||
|
@ -228,10 +243,10 @@ class TitleMinLength(LineRule):
|
|||
name = "title-min-length"
|
||||
id = "T8"
|
||||
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):
|
||||
min_length = self.options['min-length'].value
|
||||
min_length = self.options["min-length"].value
|
||||
actual_length = len(title)
|
||||
if 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):
|
||||
name = "body-min-length"
|
||||
id = "B5"
|
||||
options_spec = [IntOption('min-length', 20, "Minimum body length")]
|
||||
options_spec = [IntOption("min-length", 20, "Minimum body length")]
|
||||
|
||||
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])
|
||||
actual_length = len(body_message_no_newline)
|
||||
if 0 < actual_length < min_length:
|
||||
|
@ -284,24 +299,24 @@ class BodyMinLength(CommitRule):
|
|||
class BodyMissing(CommitRule):
|
||||
name = "body-is-missing"
|
||||
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):
|
||||
# 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
|
||||
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)]
|
||||
|
||||
|
||||
class BodyChangedFileMention(CommitRule):
|
||||
name = "body-changed-file-mention"
|
||||
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):
|
||||
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
|
||||
# in the commit msg body
|
||||
if needs_mentioned_file in commit.changed_files:
|
||||
|
@ -314,11 +329,11 @@ class BodyChangedFileMention(CommitRule):
|
|||
class BodyRegexMatches(CommitRule):
|
||||
name = "body-match-regex"
|
||||
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):
|
||||
# If no regex is specified, immediately return
|
||||
if not self.options['regex'].value:
|
||||
if not self.options["regex"].value:
|
||||
return
|
||||
|
||||
# We intentionally ignore the first line in the body as that's the empty line after the title,
|
||||
|
@ -334,7 +349,7 @@ class BodyRegexMatches(CommitRule):
|
|||
|
||||
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})"
|
||||
return [RuleViolation(self.id, violation_msg, None, len(commit.message.body) + 1)]
|
||||
|
||||
|
@ -342,33 +357,51 @@ class BodyRegexMatches(CommitRule):
|
|||
class AuthorValidEmail(CommitRule):
|
||||
name = "author-valid-email"
|
||||
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):
|
||||
# If no regex is specified, immediately return
|
||||
if not self.options['regex'].value:
|
||||
if not self.options["regex"].value:
|
||||
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)]
|
||||
|
||||
|
||||
class IgnoreByTitle(ConfigurationRule):
|
||||
name = "ignore-by-title"
|
||||
id = "I1"
|
||||
options_spec = [RegexOption('regex', None, "Regex matching the titles of commits this rule should apply to"),
|
||||
StrOption('ignore', "all", "Comma-separated list of rules to ignore")]
|
||||
options_spec = [
|
||||
RegexOption("regex", None, "Regex matching the titles of commits this rule should apply to"),
|
||||
StrOption("ignore", "all", "Comma-separated list of rules to ignore"),
|
||||
]
|
||||
|
||||
def apply(self, config, commit):
|
||||
# If no regex is specified, immediately return
|
||||
if not self.options['regex'].value:
|
||||
if not self.options["regex"].value:
|
||||
return
|
||||
|
||||
if self.options['regex'].value.match(commit.message.title):
|
||||
config.ignore = self.options['ignore'].value
|
||||
# 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"])
|
||||
|
||||
message = f"Commit title '{commit.message.title}' matches the regex " + \
|
||||
f"'{self.options['regex'].value.pattern}', ignoring rules: {self.options['ignore'].value}"
|
||||
if regex_method(commit.message.title):
|
||||
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)
|
||||
|
||||
|
@ -376,20 +409,27 @@ class IgnoreByTitle(ConfigurationRule):
|
|||
class IgnoreByBody(ConfigurationRule):
|
||||
name = "ignore-by-body"
|
||||
id = "I2"
|
||||
options_spec = [RegexOption('regex', None, "Regex matching lines of the body of commits this rule should apply to"),
|
||||
StrOption('ignore', "all", "Comma-separated list of rules to ignore")]
|
||||
options_spec = [
|
||||
RegexOption("regex", None, "Regex matching lines of the body of commits this rule should apply to"),
|
||||
StrOption("ignore", "all", "Comma-separated list of rules to ignore"),
|
||||
]
|
||||
|
||||
def apply(self, config, commit):
|
||||
# If no regex is specified, immediately return
|
||||
if not self.options['regex'].value:
|
||||
if not self.options["regex"].value:
|
||||
return
|
||||
|
||||
for line in commit.message.body:
|
||||
if self.options['regex'].value.match(line):
|
||||
config.ignore = self.options['ignore'].value
|
||||
# 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"])
|
||||
|
||||
message = f"Commit message line '{line}' matches the regex '{self.options['regex'].value.pattern}'," + \
|
||||
f" ignoring rules: {self.options['ignore'].value}"
|
||||
for line in commit.message.body:
|
||||
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)
|
||||
# No need to check other lines if we found a match
|
||||
|
@ -399,18 +439,21 @@ class IgnoreByBody(ConfigurationRule):
|
|||
class IgnoreBodyLines(ConfigurationRule):
|
||||
name = "ignore-body-lines"
|
||||
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):
|
||||
# If no regex is specified, immediately return
|
||||
if not self.options['regex'].value:
|
||||
if not self.options["regex"].value:
|
||||
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 = []
|
||||
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'"
|
||||
self.log.debug(debug_msg, line, self.options['regex'].value.pattern)
|
||||
self.log.debug(debug_msg, line, self.options["regex"].value.pattern)
|
||||
else:
|
||||
new_body.append(line)
|
||||
|
||||
|
@ -421,19 +464,25 @@ class IgnoreBodyLines(ConfigurationRule):
|
|||
class IgnoreByAuthorName(ConfigurationRule):
|
||||
name = "ignore-by-author-name"
|
||||
id = "I4"
|
||||
options_spec = [RegexOption('regex', None, "Regex matching the author name of commits this rule should apply to"),
|
||||
StrOption('ignore', "all", "Comma-separated list of rules to ignore")]
|
||||
options_spec = [
|
||||
RegexOption("regex", None, "Regex matching the author name of commits this rule should apply to"),
|
||||
StrOption("ignore", "all", "Comma-separated list of rules to ignore"),
|
||||
]
|
||||
|
||||
def apply(self, config, commit):
|
||||
# If no regex is specified, immediately return
|
||||
if not self.options['regex'].value:
|
||||
if not self.options["regex"].value:
|
||||
return
|
||||
|
||||
if self.options['regex'].value.match(commit.author_name):
|
||||
config.ignore = self.options['ignore'].value
|
||||
regex_method = Deprecation.get_regex_method(self, self.options["regex"])
|
||||
|
||||
message = (f"Commit Author Name '{commit.author_name}' matches the regex "
|
||||
f"'{self.options['regex'].value.pattern}', ignoring rules: {self.options['ignore'].value}")
|
||||
if regex_method(commit.author_name):
|
||||
config.ignore = self.options["ignore"].value
|
||||
|
||||
message = (
|
||||
f"Commit Author Name '{commit.author_name}' matches the regex "
|
||||
f"'{self.options['regex'].value.pattern}', ignoring rules: {self.options['ignore'].value}"
|
||||
)
|
||||
|
||||
self.log.debug("Ignoring commit because of rule '%s': %s", self.id, message)
|
||||
# No need to check other lines if we found a match
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
"""
|
||||
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
|
||||
|
@ -10,26 +9,28 @@ from gitlint.utils import USE_SH_LIB, DEFAULT_ENCODING
|
|||
|
||||
|
||||
def shell(cmd):
|
||||
""" Convenience function that opens a given command in a shell. Does not use 'sh' library. """
|
||||
"""Convenience function that opens a given command in a shell. Does not use 'sh' library."""
|
||||
with subprocess.Popen(cmd, shell=True) as p:
|
||||
p.communicate()
|
||||
|
||||
|
||||
if USE_SH_LIB:
|
||||
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
|
||||
from sh import CommandNotFound, ErrorReturnCode # pylint: disable=import-error
|
||||
else:
|
||||
|
||||
class CommandNotFound(Exception):
|
||||
""" Exception indicating a command was not found during execution """
|
||||
"""Exception indicating a command was not found during execution"""
|
||||
|
||||
pass
|
||||
|
||||
class ShResult:
|
||||
""" Result wrapper class. We use this to more easily migrate from using https://amoffat.github.io/sh/ to using
|
||||
the builtin subprocess module """
|
||||
"""Result wrapper class. We use this to more easily migrate from using https://amoffat.github.io/sh/ to using
|
||||
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.stdout = stdout
|
||||
self.stderr = stderr
|
||||
|
@ -39,22 +40,23 @@ else:
|
|||
return self.stdout
|
||||
|
||||
class ErrorReturnCode(ShResult, Exception):
|
||||
""" ShResult subclass for unexpected results (acts as an exception). """
|
||||
"""ShResult subclass for unexpected results (acts as an exception)."""
|
||||
|
||||
pass
|
||||
|
||||
def git(*command_parts, **kwargs):
|
||||
""" Git shell wrapper.
|
||||
"""Git shell wrapper.
|
||||
Implemented as separate function here, so we can do a 'sh' style imports:
|
||||
`from shell import git`
|
||||
"""
|
||||
args = ['git'] + list(command_parts)
|
||||
args = ["git"] + list(command_parts)
|
||||
return _exec(*args, **kwargs)
|
||||
|
||||
def _exec(*args, **kwargs):
|
||||
pipe = subprocess.PIPE
|
||||
popen_kwargs = {'stdout': pipe, 'stderr': pipe, 'shell': kwargs.get('_tty_out', False)}
|
||||
if '_cwd' in kwargs:
|
||||
popen_kwargs['cwd'] = kwargs['_cwd']
|
||||
popen_kwargs = {"stdout": pipe, "stderr": pipe, "shell": kwargs.get("_tty_out", False)}
|
||||
if "_cwd" in kwargs:
|
||||
popen_kwargs["cwd"] = kwargs["_cwd"]
|
||||
|
||||
try:
|
||||
with subprocess.Popen(args, **popen_kwargs) as p:
|
||||
|
@ -65,10 +67,10 @@ else:
|
|||
exit_code = p.returncode
|
||||
stdout = result[0].decode(DEFAULT_ENCODING)
|
||||
stderr = result[1] # 'sh' does not decode the stderr bytes to unicode
|
||||
full_cmd = '' if args is None else ' '.join(args)
|
||||
full_cmd = "" if args is None else " ".join(args)
|
||||
|
||||
# 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:
|
||||
return ShResult(full_cmd, stdout, stderr, exit_code)
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import contextlib
|
||||
import copy
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
@ -13,12 +10,22 @@ import unittest
|
|||
|
||||
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
|
||||
|
||||
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):
|
||||
""" 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
|
||||
maxDiff = None
|
||||
|
@ -30,13 +37,24 @@ class BaseTestCase(unittest.TestCase):
|
|||
def setUp(self):
|
||||
self.logcapture = LogCapture()
|
||||
self.logcapture.setFormatter(logging.Formatter(LOG_FORMAT))
|
||||
logging.getLogger('gitlint').setLevel(logging.DEBUG)
|
||||
logging.getLogger('gitlint').handlers = [self.logcapture]
|
||||
logging.getLogger("gitlint").setLevel(logging.DEBUG)
|
||||
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
|
||||
# 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
|
||||
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
|
||||
@contextlib.contextmanager
|
||||
|
@ -57,25 +75,25 @@ class BaseTestCase(unittest.TestCase):
|
|||
|
||||
@staticmethod
|
||||
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)
|
||||
with io.open(sample_path, encoding=DEFAULT_ENCODING) as content:
|
||||
with open(sample_path, encoding=DEFAULT_ENCODING) as content:
|
||||
sample = content.read()
|
||||
return sample
|
||||
|
||||
@staticmethod
|
||||
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"
|
||||
patched_module = patch(module_path, side_effect=side_effect)
|
||||
return patched_module
|
||||
|
||||
@staticmethod
|
||||
def get_expected(filename="", variable_dict=None):
|
||||
""" Utility method to read an expected file from gitlint/tests/expected and return it as a string.
|
||||
Optionally replace template variables specified by variable_dict. """
|
||||
"""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."""
|
||||
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()
|
||||
|
||||
if variable_dict:
|
||||
|
@ -87,20 +105,21 @@ class BaseTestCase(unittest.TestCase):
|
|||
return os.path.join(BaseTestCase.SAMPLES_DIR, "user_rules")
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
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
|
||||
changed files"""
|
||||
with patch("gitlint.git.git_commentchar") as comment_char:
|
||||
comment_char.return_value = "#"
|
||||
gitcontext = GitContext.from_commit_msg(commit_msg_str)
|
||||
commit = gitcontext.commits[-1]
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
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)
|
||||
commit = gitcontext.commits[-1]
|
||||
for attr, value in kwargs.items():
|
||||
|
@ -108,31 +127,31 @@ class BaseTestCase(unittest.TestCase):
|
|||
return commit
|
||||
|
||||
def assert_logged(self, expected):
|
||||
""" 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
|
||||
of all loglines. """
|
||||
"""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
|
||||
of all loglines."""
|
||||
if isinstance(expected, list):
|
||||
self.assertListEqual(self.logcapture.messages, expected)
|
||||
else:
|
||||
self.assertEqual("\n".join(self.logcapture.messages), expected)
|
||||
|
||||
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)
|
||||
|
||||
def assertRaisesRegex(self, expected_exception, expected_regex, *args, **kwargs):
|
||||
""" 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.
|
||||
"""Pass-through method to unittest.TestCase.assertRaisesRegex that applies re.escape() to the passed
|
||||
`expected_regex`. This is useful to automatically escape all file paths that might be present in the regex.
|
||||
"""
|
||||
return super().assertRaisesRegex(expected_exception, re.escape(expected_regex), *args, **kwargs)
|
||||
|
||||
def clearlog(self):
|
||||
""" Clears the log capture """
|
||||
"""Clears the log capture"""
|
||||
self.logcapture.clear()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def assertRaisesMessage(self, expected_exception, expected_msg): # pylint: disable=invalid-name
|
||||
""" Asserts an exception has occurred with a given error message """
|
||||
"""Asserts an exception has occurred with a given error message"""
|
||||
try:
|
||||
yield
|
||||
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")
|
||||
|
||||
def object_equality_test(self, obj, attr_list, ctor_kwargs=None):
|
||||
""" Helper function to easily implement object equality tests.
|
||||
Creates an object clone for every passed attribute and checks for (in)equality
|
||||
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__`.
|
||||
"""Helper function to easily implement object equality tests.
|
||||
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.
|
||||
This function assumes all attributes in `attr_list` can be passed to the ctor of `obj.__class__`.
|
||||
"""
|
||||
if not ctor_kwargs:
|
||||
ctor_kwargs = {}
|
||||
|
@ -178,7 +197,7 @@ class BaseTestCase(unittest.TestCase):
|
|||
|
||||
|
||||
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):
|
||||
logging.Handler.__init__(self, *args, **kwargs)
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
|
@ -29,11 +26,11 @@ class CLITests(BaseTestCase):
|
|||
GITLINT_SUCCESS_CODE = 0
|
||||
|
||||
def setUp(self):
|
||||
super(CLITests, self).setUp()
|
||||
super().setUp()
|
||||
self.cli = CliRunner()
|
||||
|
||||
# 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.return_value = "git version 1.2.3"
|
||||
|
||||
|
@ -42,39 +39,44 @@ class CLITests(BaseTestCase):
|
|||
|
||||
@staticmethod
|
||||
def get_system_info_dict():
|
||||
""" Returns a dict with items related to system values logged by `gitlint --debug` """
|
||||
return {'platform': platform.platform(), "python_version": sys.version, 'gitlint_version': __version__,
|
||||
'GITLINT_USE_SH_LIB': BaseTestCase.GITLINT_USE_SH_LIB, 'target': os.path.realpath(os.getcwd()),
|
||||
'DEFAULT_ENCODING': DEFAULT_ENCODING}
|
||||
"""Returns a dict with items related to system values logged by `gitlint --debug`"""
|
||||
return {
|
||||
"platform": platform.platform(),
|
||||
"python_version": sys.version,
|
||||
"gitlint_version": __version__,
|
||||
"GITLINT_USE_SH_LIB": BaseTestCase.GITLINT_USE_SH_LIB,
|
||||
"target": os.path.realpath(os.getcwd()),
|
||||
"DEFAULT_ENCODING": DEFAULT_ENCODING,
|
||||
}
|
||||
|
||||
def test_version(self):
|
||||
""" Test for --version option """
|
||||
"""Test for --version option"""
|
||||
result = self.cli.invoke(cli.cli, ["--version"])
|
||||
self.assertEqual(result.output.split("\n")[0], f"cli, version {__version__}")
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||
@patch("gitlint.git.sh")
|
||||
def test_lint(self, sh, _):
|
||||
""" Test for basic simple linting functionality """
|
||||
"""Test for basic simple linting functionality"""
|
||||
sh.git.side_effect = [
|
||||
"6f29bf81a8322a04071bb794666e48c443a90360",
|
||||
"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
||||
"commït-title\n\ncommït-body",
|
||||
"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\ncommït-title\n\ncommït-body",
|
||||
"#", # 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",
|
||||
"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)
|
||||
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)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||
@patch("gitlint.git.sh")
|
||||
def test_lint_multiple_commits(self, sh, _):
|
||||
""" Test for --commits option """
|
||||
"""Test for --commits option"""
|
||||
|
||||
# fmt: off
|
||||
sh.git.side_effect = [
|
||||
"6f29bf81a8322a04071bb794666e48c443a90360\n" + # git rev-list <SHA>
|
||||
"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"
|
||||
"commït-title1\n\ncommït-body1",
|
||||
"#", # 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/file-1\ncommit-1/file-2\n", # git diff-tree
|
||||
# git log --pretty <FORMAT> <SHA>
|
||||
"test åuthor2\x00test-email3@föo.com\x002016-12-04 15:28:15 +0100\x00åbc\n"
|
||||
"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/file-1\ncommit-2/file-2\n", # git diff-tree
|
||||
# git log --pretty <FORMAT> <SHA>
|
||||
"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00åbc\n"
|
||||
"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/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"])
|
||||
self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_multiple_commits_1"))
|
||||
self.assertEqual(result.exit_code, 3)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||
@patch("gitlint.git.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
|
||||
sh.git.side_effect = [
|
||||
"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"
|
||||
"commït-title1\n\ncommït-body1",
|
||||
"#", # 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/file-1\ncommit-1/file-2\n", # git diff-tree
|
||||
# git log --pretty <FORMAT> <SHA>
|
||||
"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",
|
||||
"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/file-1\ncommit-2/file-2\n", # git diff-tree
|
||||
# git log --pretty <FORMAT> <SHA>
|
||||
"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00åbc\n"
|
||||
"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/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"])
|
||||
# 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(result.exit_code, 3)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||
@patch("gitlint.git.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
|
||||
sh.git.side_effect = [
|
||||
"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"
|
||||
"commït-title1\n\ncommït-body1",
|
||||
"#", # 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/file-1\ncommit-1/file-2\n", # git diff-tree
|
||||
# git log --pretty <FORMAT> <SHA>
|
||||
"test åuthor2\x00test-email3@föo.com\x002016-12-04 15:28:15 +0100\x00åbc\n"
|
||||
# Normally T3 violation (trailing punctuation), but this commit is ignored because of
|
||||
# config below
|
||||
"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/file-1\ncommit-2/file-2\n", # git diff-tree
|
||||
# git log --pretty <FORMAT> <SHA>
|
||||
"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
|
||||
"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/file-1\ncommit-3/file-2\n", # git diff-tree
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
||||
result = self.cli.invoke(cli.cli, ["--commits", "foo...bar", "-c", "I1.regex=^commït-title2(.*)",
|
||||
"-c", "I2.regex=^commït-body3(.*)", "-c", "I2.ignore=B5"])
|
||||
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||
result = self.cli.invoke(
|
||||
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
|
||||
# Because we do test for the 3th commit to return violations, this test also ensures that a unique
|
||||
# config object is passed to each commit lint call
|
||||
expected = ("Commit 6f29bf81a8:\n"
|
||||
u'3: B5 Body message is too short (12<20): "commït-body1"\n\n'
|
||||
"Commit 4da2656b0d:\n"
|
||||
u'1: T3 Title has trailing punctuation (.): "commït-title3."\n')
|
||||
expected = (
|
||||
"Commit 6f29bf81a8:\n"
|
||||
'3: B5 Body message is too short (12<20): "commït-body1"\n\n'
|
||||
"Commit 4da2656b0d:\n"
|
||||
'1: T3 Title has trailing punctuation (.): "commït-title3."\n'
|
||||
)
|
||||
self.assertEqual(stderr.getvalue(), expected)
|
||||
self.assertEqual(result.exit_code, 2)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||
@patch("gitlint.git.sh")
|
||||
def test_lint_commit(self, sh, _):
|
||||
""" Test for --commit option """
|
||||
"""Test for --commit option"""
|
||||
|
||||
# fmt: off
|
||||
sh.git.side_effect = [
|
||||
"6f29bf81a8322a04071bb794666e48c443a90360\n", # git log -1 <SHA> --pretty=%H
|
||||
# git log --pretty <FORMAT> <SHA>
|
||||
"test åuthor1\x00test-email1@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
||||
"WIP: commït-title1\n\ncommït-body1",
|
||||
"#", # git config --get core.commentchar
|
||||
"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/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"])
|
||||
self.assertEqual(result.output, "")
|
||||
|
||||
self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_commit_1"))
|
||||
self.assertEqual(result.exit_code, 2)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||
@patch("gitlint.git.sh")
|
||||
def test_lint_commit_negative(self, sh, _):
|
||||
""" Negative test for --commit option """
|
||||
"""Negative test for --commit option"""
|
||||
|
||||
# Try using --commit and --commits at the same time (not allowed)
|
||||
result = self.cli.invoke(cli.cli, ["--commit", "foo", "--commits", "foo...bar"])
|
||||
|
@ -214,275 +235,309 @@ class CLITests(BaseTestCase):
|
|||
self.assertEqual(result.output, expected_output)
|
||||
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value=u'WIP: tïtle \n')
|
||||
@patch("gitlint.cli.get_stdin_data", return_value="WIP: tïtle \n")
|
||||
def test_input_stream(self, _):
|
||||
""" Test for linting when a message is passed via stdin """
|
||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
||||
"""Test for linting when a message is passed via stdin"""
|
||||
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||
result = self.cli.invoke(cli.cli)
|
||||
self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_input_stream_1"))
|
||||
self.assertEqual(result.exit_code, 3)
|
||||
self.assertEqual(result.output, "")
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value=u'WIP: tïtle \n')
|
||||
@patch("gitlint.cli.get_stdin_data", return_value="WIP: tïtle \n")
|
||||
def test_input_stream_debug(self, _):
|
||||
""" 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 """
|
||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
||||
"""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"""
|
||||
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||
result = self.cli.invoke(cli.cli, ["--debug"])
|
||||
self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_input_stream_debug_1"))
|
||||
self.assertEqual(result.exit_code, 3)
|
||||
self.assertEqual(result.output, "")
|
||||
expected_kwargs = self.get_system_info_dict()
|
||||
expected_logs = self.get_expected('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)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value="Should be ignored\n")
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.cli.get_stdin_data", return_value="Should be ignored\n")
|
||||
@patch("gitlint.git.sh")
|
||||
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 = [
|
||||
"6f29bf81a8322a04071bb794666e48c443a90360",
|
||||
"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
||||
"commït-title\n\ncommït-body",
|
||||
"#", # git config --get core.commentchar
|
||||
"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\ncommït-title\n\ncommït-body",
|
||||
"#", # 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>
|
||||
"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"])
|
||||
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)
|
||||
|
||||
# Assert that we didn't even try to get the stdin data
|
||||
self.assertEqual(stdin_data.call_count, 0)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value=u'WIP: tïtle \n')
|
||||
@patch('arrow.now', return_value=arrow.get("2020-02-19T12:18:46.675182+01:00"))
|
||||
@patch('gitlint.git.sh')
|
||||
@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("gitlint.git.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 = [
|
||||
"#", # git config --get core.commentchar
|
||||
"föo user\n", # git config --get user.name
|
||||
"föo@bar.com\n", # git config --get user.email
|
||||
"my-branch\n", # git rev-parse --abbrev-ref HEAD (=current branch)
|
||||
"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
|
||||
"#", # git config --get core.commentchar
|
||||
"1\t5\tcommit-1/file-1\n8\t9\tcommit-1/file-2\n", # git diff-tree
|
||||
"föo user\n", # git config --get user.name
|
||||
"föo@bar.com\n", # git config --get user.email
|
||||
"my-branch\n", # git rev-parse --abbrev-ref HEAD (=current branch)
|
||||
]
|
||||
|
||||
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"])
|
||||
self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_staged_stdin_1"))
|
||||
self.assertEqual(result.exit_code, 3)
|
||||
self.assertEqual(result.output, "")
|
||||
|
||||
expected_kwargs = self.get_system_info_dict()
|
||||
expected_logs = self.get_expected('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)
|
||||
|
||||
@patch('arrow.now', return_value=arrow.get("2020-02-19T12:18:46.675182+01:00"))
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("arrow.now", return_value=arrow.get("2020-02-19T12:18:46.675182+01:00"))
|
||||
@patch("gitlint.git.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 = [
|
||||
"#", # 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@bar.com\n", # git config --get user.email
|
||||
"my-branch\n", # git rev-parse --abbrev-ref HEAD (=current branch)
|
||||
"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
with self.tempdir() as tmpdir:
|
||||
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")
|
||||
|
||||
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])
|
||||
self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_staged_msg_filename_1"))
|
||||
self.assertEqual(result.exit_code, 2)
|
||||
self.assertEqual(result.output, "")
|
||||
|
||||
expected_kwargs = self.get_system_info_dict()
|
||||
expected_logs = self.get_expected('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)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
||||
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||
def test_lint_staged_negative(self, _):
|
||||
result = self.cli.invoke(cli.cli, ["--staged"])
|
||||
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
||||
self.assertEqual(result.output, ("Error: The 'staged' option (--staged) can only be used when using "
|
||||
"'--msg-filename' or when piping data to gitlint via stdin.\n"))
|
||||
self.assertEqual(
|
||||
result.output,
|
||||
"Error: The 'staged' option (--staged) can only be used when using "
|
||||
"'--msg-filename' or when piping data to gitlint via stdin.\n",
|
||||
)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||
@patch("gitlint.git.sh")
|
||||
def test_fail_without_commits(self, sh, _):
|
||||
""" Test for --debug option """
|
||||
"""Test for --debug option"""
|
||||
|
||||
sh.git.side_effect = [
|
||||
"", # First invocation of git rev-list
|
||||
"" # Second invocation of git rev-list
|
||||
]
|
||||
sh.git.side_effect = ["", ""] # First invocation of git rev-list # Second invocation of git rev-list
|
||||
|
||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
||||
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||
# By default, gitlint should silently exit with code GITLINT_SUCCESS when there are no commits
|
||||
result = self.cli.invoke(cli.cli, ["--commits", "foo..bar"])
|
||||
self.assertEqual(stderr.getvalue(), "")
|
||||
self.assertEqual(result.exit_code, cli.GITLINT_SUCCESS)
|
||||
self.assert_log_contains("DEBUG: gitlint.cli No commits in range \"foo..bar\"")
|
||||
self.assert_log_contains('DEBUG: gitlint.cli No commits in range "foo..bar"')
|
||||
|
||||
# When --fail-without-commits is set, gitlint should hard fail with code USAGE_ERROR_CODE
|
||||
self.clearlog()
|
||||
result = self.cli.invoke(cli.cli, ["--commits", "foo..bar", "--fail-without-commits"])
|
||||
self.assertEqual(result.output, 'Error: No commits in range "foo..bar"\n')
|
||||
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
||||
self.assert_log_contains("DEBUG: gitlint.cli No commits in range \"foo..bar\"")
|
||||
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, _):
|
||||
expected_output = "3: B6 Body message is missing\n"
|
||||
|
||||
with self.tempdir() as tmpdir:
|
||||
msg_filename = os.path.join(tmpdir, "msg")
|
||||
with io.open(msg_filename, 'w', encoding=DEFAULT_ENCODING) as f:
|
||||
with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f:
|
||||
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])
|
||||
self.assertEqual(stderr.getvalue(), expected_output)
|
||||
self.assertEqual(result.exit_code, 1)
|
||||
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, _):
|
||||
""" Test for --silent option """
|
||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
||||
"""Test for --silent option"""
|
||||
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||
result = self.cli.invoke(cli.cli, ["--silent"])
|
||||
self.assertEqual(stderr.getvalue(), "")
|
||||
self.assertEqual(result.exit_code, 3)
|
||||
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, _):
|
||||
""" Test for --verbosity option """
|
||||
"""Test for --verbosity option"""
|
||||
# We only test -v and -vv, more testing is really not required here
|
||||
# -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"])
|
||||
self.assertEqual(stderr.getvalue(), "1: T2\n1: T5\n3: B6\n")
|
||||
self.assertEqual(result.exit_code, 3)
|
||||
self.assertEqual(result.output, "")
|
||||
|
||||
# -vv
|
||||
expected_output = "1: T2 Title has trailing whitespace\n" + \
|
||||
"1: T5 Title contains the word 'WIP' (case-insensitive)\n" + \
|
||||
"3: B6 Body message is missing\n"
|
||||
expected_output = (
|
||||
"1: T2 Title has trailing whitespace\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")
|
||||
self.assertEqual(stderr.getvalue(), expected_output)
|
||||
self.assertEqual(result.exit_code, 3)
|
||||
self.assertEqual(result.output, "")
|
||||
|
||||
# -vvvv: not supported -> should print a config error
|
||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
||||
result = self.cli.invoke(cli.cli, ["-vvvv"], input=u'WIP: tïtle \n')
|
||||
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||
result = self.cli.invoke(cli.cli, ["-vvvv"], input="WIP: tïtle \n")
|
||||
self.assertEqual(stderr.getvalue(), "")
|
||||
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")
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||
@patch("gitlint.git.sh")
|
||||
def test_debug(self, sh, _):
|
||||
""" Test for --debug option """
|
||||
"""Test for --debug option"""
|
||||
|
||||
# fmt: off
|
||||
sh.git.side_effect = [
|
||||
"6f29bf81a8322a04071bb794666e48c443a90360\n" # git rev-list <SHA>
|
||||
"25053ccec5e28e1bb8f7551fdbb5ab213ada2401\n"
|
||||
"4da2656b0dadc76c7ee3fd0243a96cb64007f125\n",
|
||||
# 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",
|
||||
"#", # git config --get core.commentchar
|
||||
"commit-1-branch-1\ncommit-1-branch-2\n", # git branch --contains <sha>
|
||||
"commit-1/file-1\ncommit-1/file-2\n", # git diff-tree
|
||||
"test åuthor2\x00test-email2@föo.com\x002016-12-04 15:28:15 +0100\x00abc\n"
|
||||
"#", # git config --get core.commentchar
|
||||
"5\t8\tcommit-1/file-1\n2\t9\tcommit-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\x00b123\n"
|
||||
"commït-title2.\n\ncommït-body2",
|
||||
"commit-2-branch-1\ncommit-2-branch-2\n", # git branch --contains <sha>
|
||||
"commit-2/file-1\ncommit-2/file-2\n", # git diff-tree
|
||||
"test åuthor3\x00test-email3@föo.com\x002016-12-05 15:28:15 +0100\x00abc\n"
|
||||
"5\t8\tcommit-2/file-1\n7\t9\tcommit-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\x00c123\n"
|
||||
"föobar\nbar",
|
||||
"commit-3-branch-1\ncommit-3-branch-2\n", # git branch --contains <sha>
|
||||
"commit-3/file-1\ncommit-3/file-2\n", # git diff-tree
|
||||
"1\t4\tcommit-3/file-1\n3\t4\tcommit-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"))
|
||||
result = self.cli.invoke(cli.cli, ["--config", config_path, "--debug", "--commits",
|
||||
"foo...bar"])
|
||||
result = self.cli.invoke(cli.cli, ["--config", config_path, "--debug", "--commits", "foo...bar"])
|
||||
|
||||
expected = "Commit 6f29bf81a8:\n3: B5\n\n" + \
|
||||
"Commit 25053ccec5:\n1: T3\n3: B5\n\n" + \
|
||||
"Commit 4da2656b0d:\n2: B4\n3: B5\n3: B6\n"
|
||||
expected = (
|
||||
"Commit 6f29bf81a8:\n3: B5\n\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(result.exit_code, 6)
|
||||
|
||||
expected_kwargs = self.get_system_info_dict()
|
||||
expected_kwargs.update({'config_path': config_path})
|
||||
expected_logs = self.get_expected('cli/test_cli/test_debug_1', expected_kwargs)
|
||||
expected_kwargs.update({"config_path": config_path})
|
||||
expected_logs = self.get_expected("cli/test_cli/test_debug_1", expected_kwargs)
|
||||
self.assert_logged(expected_logs)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value="Test tïtle\n")
|
||||
@patch("gitlint.cli.get_stdin_data", return_value="Test tïtle\n")
|
||||
def test_extra_path(self, _):
|
||||
""" Test for --extra-path flag """
|
||||
"""Test for --extra-path flag"""
|
||||
# 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")
|
||||
result = self.cli.invoke(cli.cli, ["--extra-path", extra_path])
|
||||
expected_output = "1: UC1 Commit violåtion 1: \"Contënt 1\"\n" + \
|
||||
"3: B6 Body message is missing\n"
|
||||
expected_output = '1: UC1 Commit violåtion 1: "Contënt 1"\n' + "3: B6 Body message is missing\n"
|
||||
self.assertEqual(stderr.getvalue(), expected_output)
|
||||
self.assertEqual(result.exit_code, 2)
|
||||
|
||||
# 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"))
|
||||
result = self.cli.invoke(cli.cli, ["--extra-path", extra_path])
|
||||
expected_output = "1: UC1 Commit violåtion 1: \"Contënt 1\"\n" + \
|
||||
"3: B6 Body message is missing\n"
|
||||
expected_output = '1: UC1 Commit violåtion 1: "Contënt 1"\n' + "3: B6 Body message is missing\n"
|
||||
self.assertEqual(stderr.getvalue(), expected_output)
|
||||
self.assertEqual(result.exit_code, 2)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value="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, _):
|
||||
# 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"])
|
||||
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(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, _):
|
||||
result = self.cli.invoke(cli.cli, ["--contrib", "föobar,CC1"])
|
||||
self.assertEqual(result.output, "Config Error: No contrib rule with id or name 'föobar' found.\n")
|
||||
self.assertEqual(result.exit_code, self.CONFIG_ERROR_CODE)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value="WIP: tëst")
|
||||
@patch("gitlint.cli.get_stdin_data", return_value="WIP: tëst")
|
||||
def test_config_file(self, _):
|
||||
""" Test for --config option """
|
||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
||||
"""Test for --config option"""
|
||||
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, ["--config", config_path])
|
||||
self.assertEqual(result.output, "")
|
||||
self.assertEqual(stderr.getvalue(), "1: T5\n3: B6\n")
|
||||
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):
|
||||
""" Negative test for --config option """
|
||||
"""Negative test for --config option"""
|
||||
# Directory as config file
|
||||
config_path = self.get_sample_path("config")
|
||||
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])
|
||||
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, _):
|
||||
""" Test for the --target option """
|
||||
"""Test for the --target option"""
|
||||
with self.tempdir() as tmpdir:
|
||||
tmpdir_path = os.path.realpath(tmpdir)
|
||||
os.environ["LANGUAGE"] = "C" # Force language to english so we can check for error message
|
||||
|
@ -515,7 +591,7 @@ class CLITests(BaseTestCase):
|
|||
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
|
||||
|
||||
def test_target_negative(self):
|
||||
""" Negative test for the --target option """
|
||||
"""Negative test for the --target option"""
|
||||
# try setting a non-existing target
|
||||
result = self.cli.invoke(cli.cli, ["--target", "/föo/bar"])
|
||||
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."
|
||||
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):
|
||||
""" Test for the generate-config subcommand """
|
||||
"""Test for the generate-config subcommand"""
|
||||
result = self.cli.invoke(cli.cli, ["generate-config"], input="tëstfile\n")
|
||||
self.assertEqual(result.exit_code, self.GITLINT_SUCCESS_CODE)
|
||||
expected_msg = "Please specify a location for the sample gitlint config file [.gitlint]: tëstfile\n" + \
|
||||
f"Successfully generated {os.path.realpath('tëstfile')}\n"
|
||||
expected_msg = (
|
||||
"Please specify a location for the sample gitlint config file [.gitlint]: tëstfile\n"
|
||||
+ f"Successfully generated {os.path.realpath('tëstfile')}\n"
|
||||
)
|
||||
self.assertEqual(result.output, expected_msg)
|
||||
generate_config.assert_called_once_with(os.path.realpath("tëstfile"))
|
||||
|
||||
def test_generate_config_negative(self):
|
||||
""" Negative test for the generate-config subcommand """
|
||||
"""Negative test for the generate-config subcommand"""
|
||||
# Non-existing directory
|
||||
fake_dir = os.path.abspath("/föo")
|
||||
fake_path = os.path.join(fake_dir, "bar")
|
||||
result = self.cli.invoke(cli.cli, ["generate-config"], input=fake_path)
|
||||
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
||||
expected_msg = f"Please specify a location for the sample gitlint config file [.gitlint]: {fake_path}\n" + \
|
||||
f"Error: Directory '{fake_dir}' does not exist.\n"
|
||||
expected_msg = (
|
||||
f"Please specify a location for the sample gitlint config file [.gitlint]: {fake_path}\n"
|
||||
+ f"Error: Directory '{fake_dir}' does not exist.\n"
|
||||
)
|
||||
self.assertEqual(result.output, expected_msg)
|
||||
|
||||
# Existing file
|
||||
sample_path = self.get_sample_path(os.path.join("config", "gitlintconfig"))
|
||||
result = self.cli.invoke(cli.cli, ["generate-config"], input=sample_path)
|
||||
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
||||
expected_msg = "Please specify a location for the sample gitlint " + \
|
||||
f"config file [.gitlint]: {sample_path}\n" + \
|
||||
f"Error: File \"{sample_path}\" already exists.\n"
|
||||
expected_msg = (
|
||||
"Please specify a location for the sample gitlint "
|
||||
f"config file [.gitlint]: {sample_path}\n"
|
||||
f'Error: File "{sample_path}" already exists.\n'
|
||||
)
|
||||
self.assertEqual(result.output, expected_msg)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||
@patch("gitlint.git.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")
|
||||
result = self.cli.invoke(cli.cli)
|
||||
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||
@patch("gitlint.git.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: ""
|
||||
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)
|
||||
|
||||
@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, _):
|
||||
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"))
|
||||
result = self.cli.invoke(cli.cli, ["--config", config_path, "--debug"])
|
||||
self.assertEqual(result.output, "")
|
||||
|
@ -588,6 +670,6 @@ class CLITests(BaseTestCase):
|
|||
|
||||
# Assert debug logs are correct
|
||||
expected_kwargs = self.get_system_info_dict()
|
||||
expected_kwargs.update({'config_path': config_path})
|
||||
expected_logs = self.get_expected('cli/test_cli/test_named_rules_2', expected_kwargs)
|
||||
expected_kwargs.update({"config_path": config_path})
|
||||
expected_logs = self.get_expected("cli/test_cli/test_named_rules_2", expected_kwargs)
|
||||
self.assert_logged(expected_logs)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import io
|
||||
from io import StringIO
|
||||
import os
|
||||
|
@ -23,21 +21,21 @@ class CLIHookTests(BaseTestCase):
|
|||
CONFIG_ERROR_CODE = 255
|
||||
|
||||
def setUp(self):
|
||||
super(CLIHookTests, self).setUp()
|
||||
super().setUp()
|
||||
self.cli = CliRunner()
|
||||
|
||||
# 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.return_value = "git version 1.2.3"
|
||||
|
||||
def tearDown(self):
|
||||
self.git_version_path.stop()
|
||||
|
||||
@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.GitHookInstaller.install_commit_msg_hook")
|
||||
@patch("gitlint.hooks.git_hooks_dir", return_value=os.path.join("/hür", "dur"))
|
||||
def test_install_hook(self, _, install_hook):
|
||||
""" Test for install-hook subcommand """
|
||||
"""Test for install-hook subcommand"""
|
||||
result = self.cli.invoke(cli.cli, ["install-hook"])
|
||||
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"
|
||||
|
@ -47,10 +45,10 @@ class CLIHookTests(BaseTestCase):
|
|||
expected_config.target = os.path.realpath(os.getcwd())
|
||||
install_hook.assert_called_once_with(expected_config)
|
||||
|
||||
@patch('gitlint.hooks.GitHookInstaller.install_commit_msg_hook')
|
||||
@patch('gitlint.hooks.git_hooks_dir', return_value=os.path.join("/hür", "dur"))
|
||||
@patch("gitlint.hooks.GitHookInstaller.install_commit_msg_hook")
|
||||
@patch("gitlint.hooks.git_hooks_dir", return_value=os.path.join("/hür", "dur"))
|
||||
def test_install_hook_target(self, _, install_hook):
|
||||
""" Test for install-hook subcommand with a specific --target option specified """
|
||||
"""Test for install-hook subcommand with a specific --target option specified"""
|
||||
# Specified target
|
||||
result = self.cli.invoke(cli.cli, ["--target", self.SAMPLES_DIR, "install-hook"])
|
||||
expected_path = os.path.join("/hür", "dur", hooks.COMMIT_MSG_HOOK_DST_PATH)
|
||||
|
@ -62,9 +60,9 @@ class CLIHookTests(BaseTestCase):
|
|||
expected_config.target = self.SAMPLES_DIR
|
||||
install_hook.assert_called_once_with(expected_config)
|
||||
|
||||
@patch('gitlint.hooks.GitHookInstaller.install_commit_msg_hook', side_effect=hooks.GitHookInstallerError("tëst"))
|
||||
@patch("gitlint.hooks.GitHookInstaller.install_commit_msg_hook", side_effect=hooks.GitHookInstallerError("tëst"))
|
||||
def test_install_hook_negative(self, install_hook):
|
||||
""" Negative test for install-hook subcommand """
|
||||
"""Negative test for install-hook subcommand"""
|
||||
result = self.cli.invoke(cli.cli, ["install-hook"])
|
||||
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
|
||||
self.assertEqual(result.output, "tëst\n")
|
||||
|
@ -72,10 +70,10 @@ class CLIHookTests(BaseTestCase):
|
|||
expected_config.target = os.path.realpath(os.getcwd())
|
||||
install_hook.assert_called_once_with(expected_config)
|
||||
|
||||
@patch('gitlint.hooks.GitHookInstaller.uninstall_commit_msg_hook')
|
||||
@patch('gitlint.hooks.git_hooks_dir', return_value=os.path.join("/hür", "dur"))
|
||||
@patch("gitlint.hooks.GitHookInstaller.uninstall_commit_msg_hook")
|
||||
@patch("gitlint.hooks.git_hooks_dir", return_value=os.path.join("/hür", "dur"))
|
||||
def test_uninstall_hook(self, _, uninstall_hook):
|
||||
""" Test for uninstall-hook subcommand """
|
||||
"""Test for uninstall-hook subcommand"""
|
||||
result = self.cli.invoke(cli.cli, ["uninstall-hook"])
|
||||
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"
|
||||
|
@ -85,9 +83,9 @@ class CLIHookTests(BaseTestCase):
|
|||
expected_config.target = os.path.realpath(os.getcwd())
|
||||
uninstall_hook.assert_called_once_with(expected_config)
|
||||
|
||||
@patch('gitlint.hooks.GitHookInstaller.uninstall_commit_msg_hook', side_effect=hooks.GitHookInstallerError("tëst"))
|
||||
@patch("gitlint.hooks.GitHookInstaller.uninstall_commit_msg_hook", side_effect=hooks.GitHookInstallerError("tëst"))
|
||||
def test_uninstall_hook_negative(self, uninstall_hook):
|
||||
""" Negative test for uninstall-hook subcommand """
|
||||
"""Negative test for uninstall-hook subcommand"""
|
||||
result = self.cli.invoke(cli.cli, ["uninstall-hook"])
|
||||
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
|
||||
self.assertEqual(result.output, "tëst\n")
|
||||
|
@ -96,8 +94,8 @@ class CLIHookTests(BaseTestCase):
|
|||
uninstall_hook.assert_called_once_with(expected_config)
|
||||
|
||||
def test_run_hook_no_tty(self):
|
||||
""" Test for run-hook subcommand.
|
||||
When no TTY is available (like is the case for this test), the hook will abort after the first check.
|
||||
"""Test for run-hook subcommand.
|
||||
When no TTY is available (like is the case for this test), the hook will abort after the first check.
|
||||
"""
|
||||
|
||||
# No need to patch git as we're passing a msg-filename to run-hook, so no git calls are made.
|
||||
|
@ -110,20 +108,20 @@ class CLIHookTests(BaseTestCase):
|
|||
|
||||
with self.tempdir() as tmpdir:
|
||||
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")
|
||||
|
||||
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"])
|
||||
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"))
|
||||
|
||||
# exit code is 1 because aborted (no stdin available)
|
||||
self.assertEqual(result.exit_code, 1)
|
||||
|
||||
@patch('gitlint.cli.shell')
|
||||
@patch("gitlint.cli.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"]
|
||||
expected_editors = ["vim -n", "myeditor"]
|
||||
|
@ -131,20 +129,28 @@ class CLIHookTests(BaseTestCase):
|
|||
|
||||
for i in range(0, len(set_editors)):
|
||||
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:
|
||||
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")
|
||||
|
||||
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"])
|
||||
self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_edit_1_stdout',
|
||||
{"commit_msg": commit_messages[i]}))
|
||||
expected = self.get_expected("cli/test_cli_hooks/test_hook_edit_1_stderr",
|
||||
{"commit_msg": commit_messages[i]})
|
||||
self.assertEqual(
|
||||
result.output,
|
||||
self.get_expected(
|
||||
"cli/test_cli_hooks/test_hook_edit_1_stdout", {"commit_msg": commit_messages[i]}
|
||||
),
|
||||
)
|
||||
expected = self.get_expected(
|
||||
"cli/test_cli_hooks/test_hook_edit_1_stderr", {"commit_msg": commit_messages[i]}
|
||||
)
|
||||
self.assertEqual(stderr.getvalue(), expected)
|
||||
|
||||
# exit code = number of violations
|
||||
|
@ -155,17 +161,17 @@ class CLIHookTests(BaseTestCase):
|
|||
self.assert_log_contains(f"DEBUG: gitlint.cli run-hook: {expected_editors[i]} {msg_filename}")
|
||||
|
||||
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:
|
||||
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")
|
||||
|
||||
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"])
|
||||
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"))
|
||||
|
||||
# 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")
|
||||
|
||||
def test_run_hook_yes(self):
|
||||
""" Test for run-hook subcommand, answering 'y(es)' after commit-hook """
|
||||
with self.patch_input(['y']):
|
||||
"""Test for run-hook subcommand, answering 'y(es)' after commit-hook"""
|
||||
with self.patch_input(["y"]):
|
||||
with self.tempdir() as tmpdir:
|
||||
msg_filename = os.path.join(tmpdir, "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")
|
||||
|
||||
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"])
|
||||
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"))
|
||||
|
||||
# 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.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message accepted")
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||
@patch("gitlint.git.sh")
|
||||
def test_run_hook_negative(self, sh, _):
|
||||
""" Negative test for the run-hook subcommand: testing whether exceptions are correctly handled when
|
||||
"""Negative test for the run-hook subcommand: testing whether exceptions are correctly handled when
|
||||
running `gitlint run-hook`.
|
||||
"""
|
||||
# GIT_CONTEXT_ERROR_CODE: git error
|
||||
error_msg = b"fatal: not a git repository (or any of the parent directories): .git"
|
||||
sh.git.side_effect = ErrorReturnCode("full command", b"stdout", error_msg)
|
||||
result = self.cli.invoke(cli.cli, ["run-hook"])
|
||||
expected = self.get_expected('cli/test_cli_hooks/test_run_hook_negative_1', {'git_repo': os.getcwd()})
|
||||
expected = self.get_expected("cli/test_cli_hooks/test_run_hook_negative_1", {"git_repo": os.getcwd()})
|
||||
self.assertEqual(result.output, expected)
|
||||
self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE)
|
||||
|
||||
# USAGE_ERROR_CODE: incorrect use of gitlint
|
||||
result = self.cli.invoke(cli.cli, ["--staged", "run-hook"])
|
||||
self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_run_hook_negative_2'))
|
||||
self.assertEqual(result.output, self.get_expected("cli/test_cli_hooks/test_run_hook_negative_2"))
|
||||
self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE)
|
||||
|
||||
# CONFIG_ERROR_CODE: incorrect config. Note that this is handled before the hook even runs
|
||||
|
@ -215,67 +221,66 @@ class CLIHookTests(BaseTestCase):
|
|||
self.assertEqual(result.output, "Config Error: No such rule 'föo'\n")
|
||||
self.assertEqual(result.exit_code, self.CONFIG_ERROR_CODE)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value="WIP: Test hook stdin tïtle\n")
|
||||
@patch("gitlint.cli.get_stdin_data", return_value="WIP: Test hook stdin tïtle\n")
|
||||
def test_run_hook_stdin_violations(self, _):
|
||||
""" Test for passing stdin data to run-hook, expecting some violations. Equivalent of:
|
||||
$ echo "WIP: Test hook stdin tïtle" | gitlint run-hook
|
||||
"""Test for passing stdin data to run-hook, expecting some violations. Equivalent of:
|
||||
$ echo "WIP: Test hook stdin tïtle" | gitlint run-hook
|
||||
"""
|
||||
|
||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
||||
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||
result = self.cli.invoke(cli.cli, ["run-hook"])
|
||||
expected_stderr = self.get_expected('cli/test_cli_hooks/test_hook_stdin_violations_1_stderr')
|
||||
expected_stderr = self.get_expected("cli/test_cli_hooks/test_hook_stdin_violations_1_stderr")
|
||||
self.assertEqual(stderr.getvalue(), expected_stderr)
|
||||
self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_stdin_violations_1_stdout'))
|
||||
self.assertEqual(result.output, self.get_expected("cli/test_cli_hooks/test_hook_stdin_violations_1_stdout"))
|
||||
# Hook will auto-abort because we're using stdin. Abort = exit code 1
|
||||
self.assertEqual(result.exit_code, 1)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value="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, _):
|
||||
""" 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
|
||||
"""Test for passing stdin data to run-hook, expecting *NO* violations, Equivalent of:
|
||||
$ echo -e "Test tïtle\n\nTest bödy that is long enough" | gitlint run-hook
|
||||
"""
|
||||
|
||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
||||
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||
result = self.cli.invoke(cli.cli, ["run-hook"])
|
||||
self.assertEqual(stderr.getvalue(), "") # no errors = no stderr output
|
||||
expected_stdout = self.get_expected('cli/test_cli_hooks/test_hook_stdin_no_violations_1_stdout')
|
||||
expected_stdout = self.get_expected("cli/test_cli_hooks/test_hook_stdin_no_violations_1_stdout")
|
||||
self.assertEqual(result.output, expected_stdout)
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value="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, _):
|
||||
""" 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
|
||||
"""Test that gitlint still respects config when running run-hook, equivalent of:
|
||||
$ echo "WIP: Test hook config tïtle" | gitlint -c title-max-length.line-length=5 --ignore B6 run-hook
|
||||
"""
|
||||
|
||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
||||
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||
result = self.cli.invoke(cli.cli, ["-c", "title-max-length.line-length=5", "--ignore", "B6", "run-hook"])
|
||||
self.assertEqual(stderr.getvalue(), self.get_expected('cli/test_cli_hooks/test_hook_config_1_stderr'))
|
||||
self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_config_1_stdout'))
|
||||
self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli_hooks/test_hook_config_1_stderr"))
|
||||
self.assertEqual(result.output, self.get_expected("cli/test_cli_hooks/test_hook_config_1_stdout"))
|
||||
# Hook will auto-abort because we're using stdin. Abort = exit code 1
|
||||
self.assertEqual(result.exit_code, 1)
|
||||
|
||||
@patch('gitlint.cli.get_stdin_data', return_value=False)
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.cli.get_stdin_data", return_value=False)
|
||||
@patch("gitlint.git.sh")
|
||||
def test_run_hook_local_commit(self, sh, _):
|
||||
""" Test running the hook on the last commit-msg from the local repo, equivalent of:
|
||||
$ gitlint run-hook
|
||||
and then choosing 'e'
|
||||
"""Test running the hook on the last commit-msg from the local repo, equivalent of:
|
||||
$ gitlint run-hook
|
||||
and then choosing 'e'
|
||||
"""
|
||||
sh.git.side_effect = [
|
||||
"6f29bf81a8322a04071bb794666e48c443a90360",
|
||||
"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
||||
"WIP: commït-title\n\ncommït-body",
|
||||
"test åuthor\x00test-email@föo.com\x002016-12-03 15:28:15 +0100\x00åbc\nWIP: commït-title\n\ncommït-body",
|
||||
"#", # 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",
|
||||
"file1.txt\npåth/to/file2.txt\n"
|
||||
]
|
||||
|
||||
with self.patch_input(['e']):
|
||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
||||
with self.patch_input(["e"]):
|
||||
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||
result = self.cli.invoke(cli.cli, ["run-hook"])
|
||||
expected = self.get_expected('cli/test_cli_hooks/test_hook_local_commit_1_stderr')
|
||||
expected = self.get_expected("cli/test_cli_hooks/test_hook_local_commit_1_stderr")
|
||||
self.assertEqual(stderr.getvalue(), expected)
|
||||
self.assertEqual(result.output, self.get_expected('cli/test_cli_hooks/test_hook_local_commit_1_stdout'))
|
||||
self.assertEqual(result.output, self.get_expected("cli/test_cli_hooks/test_hook_local_commit_1_stdout"))
|
||||
# If we can't edit the message, run-hook follows regular gitlint behavior and exit code = # violations
|
||||
self.assertEqual(result.exit_code, 2)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from gitlint import rules
|
||||
|
@ -9,16 +7,15 @@ from gitlint.tests.base import BaseTestCase
|
|||
|
||||
|
||||
class LintConfigTests(BaseTestCase):
|
||||
|
||||
def test_set_rule_option(self):
|
||||
config = LintConfig()
|
||||
|
||||
# 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
|
||||
config.set_rule_option('title-max-length', 'line-length', 60)
|
||||
self.assertEqual(config.get_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)
|
||||
|
||||
def test_set_rule_option_negative(self):
|
||||
config = LintConfig()
|
||||
|
@ -26,18 +23,20 @@ class LintConfigTests(BaseTestCase):
|
|||
# non-existing rule
|
||||
expected_error_msg = "No such rule 'föobar'"
|
||||
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
|
||||
config.set_rule_option(u'föobar', u'lïne-length', 60)
|
||||
config.set_rule_option("föobar", "lïne-length", 60)
|
||||
|
||||
# non-existing option
|
||||
expected_error_msg = "Rule 'title-max-length' has no option 'föobar'"
|
||||
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
|
||||
config.set_rule_option('title-max-length', u'föobar', 60)
|
||||
config.set_rule_option("title-max-length", "föobar", 60)
|
||||
|
||||
# invalid option value
|
||||
expected_error_msg = "'föo' is not a valid value for option 'title-max-length.line-length'. " + \
|
||||
"Option 'line-length' must be a positive integer (current value: 'föo')."
|
||||
expected_error_msg = (
|
||||
"'föo' is not a valid value for option 'title-max-length.line-length'. "
|
||||
"Option 'line-length' must be a positive integer (current value: 'föo')."
|
||||
)
|
||||
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
|
||||
config.set_rule_option('title-max-length', 'line-length', "föo")
|
||||
config.set_rule_option("title-max-length", "line-length", "föo")
|
||||
|
||||
def test_set_general_option(self):
|
||||
config = LintConfig()
|
||||
|
@ -45,12 +44,14 @@ class LintConfigTests(BaseTestCase):
|
|||
# Check that default general options are correct
|
||||
self.assertTrue(config.ignore_merge_commits)
|
||||
self.assertTrue(config.ignore_fixup_commits)
|
||||
self.assertTrue(config.ignore_fixup_amend_commits)
|
||||
self.assertTrue(config.ignore_squash_commits)
|
||||
self.assertTrue(config.ignore_revert_commits)
|
||||
|
||||
self.assertFalse(config.ignore_stdin)
|
||||
self.assertFalse(config.staged)
|
||||
self.assertFalse(config.fail_without_commits)
|
||||
self.assertFalse(config.regex_style_search)
|
||||
self.assertFalse(config.debug)
|
||||
self.assertEqual(config.verbosity, 3)
|
||||
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")
|
||||
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
|
||||
config.set_general_option("ignore-squash-commits", "false")
|
||||
self.assertFalse(config.ignore_squash_commits)
|
||||
|
@ -100,6 +105,10 @@ class LintConfigTests(BaseTestCase):
|
|||
config.set_general_option("fail-without-commits", "true")
|
||||
self.assertTrue(config.fail_without_commits)
|
||||
|
||||
# regex-style-search
|
||||
config.set_general_option("regex-style-search", "true")
|
||||
self.assertTrue(config.regex_style_search)
|
||||
|
||||
# target
|
||||
config.set_general_option("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.assertEqual(str(type(actual_rule)), "<class 'conventional_commit.ConventionalCommit'>")
|
||||
self.assertEqual(actual_rule.id, 'CT1')
|
||||
self.assertEqual(actual_rule.name, u'contrib-title-conventional-commits')
|
||||
self.assertEqual(actual_rule.id, "CT1")
|
||||
self.assertEqual(actual_rule.name, "contrib-title-conventional-commits")
|
||||
self.assertEqual(actual_rule.target, rules.CommitMessageTitle)
|
||||
|
||||
expected_rule_option = options.ListOption(
|
||||
|
@ -129,15 +138,15 @@ class LintConfigTests(BaseTestCase):
|
|||
)
|
||||
|
||||
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
|
||||
actual_rule = config.rules.find_rule("contrib-body-requires-signed-off-by")
|
||||
self.assertTrue(actual_rule.is_contrib)
|
||||
|
||||
self.assertEqual(str(type(actual_rule)), "<class 'signedoff_by.SignedOffBy'>")
|
||||
self.assertEqual(actual_rule.id, 'CC1')
|
||||
self.assertEqual(actual_rule.name, u'contrib-body-requires-signed-off-by')
|
||||
self.assertEqual(actual_rule.id, "CC1")
|
||||
self.assertEqual(actual_rule.name, "contrib-body-requires-signed-off-by")
|
||||
|
||||
# reset value (this is a different code path)
|
||||
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
|
||||
side_effects = [rules.UserRuleError("üser-rule"), options.RuleOptionError("rüle-option")]
|
||||
for side_effect in side_effects:
|
||||
with patch('gitlint.config.rule_finder.find_rule_classes', side_effect=side_effect):
|
||||
with patch("gitlint.config.rule_finder.find_rule_classes", side_effect=side_effect):
|
||||
with self.assertRaisesMessage(LintConfigError, str(side_effect)):
|
||||
config.contrib = "contrib-title-conventional-commits"
|
||||
|
||||
|
@ -166,15 +175,15 @@ class LintConfigTests(BaseTestCase):
|
|||
|
||||
config.set_general_option("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.assertEqual(str(type(actual_rule)), "<class 'my_commit_rules.MyUserCommitRule'>")
|
||||
self.assertEqual(actual_rule.id, 'UC1')
|
||||
self.assertEqual(actual_rule.name, u'my-üser-commit-rule')
|
||||
self.assertEqual(actual_rule.id, "UC1")
|
||||
self.assertEqual(actual_rule.name, "my-üser-commit-rule")
|
||||
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.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)
|
||||
config.set_general_option("extra-path", self.SAMPLES_DIR)
|
||||
|
@ -189,8 +198,9 @@ class LintConfigTests(BaseTestCase):
|
|||
config.extra_path = "föo/bar"
|
||||
|
||||
# extra path contains classes with errors
|
||||
with self.assertRaisesMessage(LintConfigError,
|
||||
"User-defined rule class 'MyUserLineRule' must have a 'validate' method"):
|
||||
with self.assertRaisesMessage(
|
||||
LintConfigError, "User-defined rule class 'MyUserLineRule' must have a 'validate' method"
|
||||
):
|
||||
config.extra_path = self.get_sample_path("user_rules/incorrect_linerule")
|
||||
|
||||
def test_set_general_option_negative(self):
|
||||
|
@ -218,31 +228,37 @@ class LintConfigTests(BaseTestCase):
|
|||
config.verbosity = value
|
||||
|
||||
# invalid ignore_xxx_commits
|
||||
ignore_attributes = ["ignore_merge_commits", "ignore_fixup_commits", "ignore_squash_commits",
|
||||
"ignore_revert_commits"]
|
||||
ignore_attributes = [
|
||||
"ignore_merge_commits",
|
||||
"ignore_fixup_commits",
|
||||
"ignore_fixup_amend_commits",
|
||||
"ignore_squash_commits",
|
||||
"ignore_revert_commits",
|
||||
]
|
||||
incorrect_values = [-1, 4, "föo"]
|
||||
for attribute in ignore_attributes:
|
||||
for value in incorrect_values:
|
||||
option_name = attribute.replace("_", "-")
|
||||
with self.assertRaisesMessage(LintConfigError,
|
||||
f"Option '{option_name}' must be either 'true' or 'false'"):
|
||||
with self.assertRaisesMessage(
|
||||
LintConfigError, f"Option '{option_name}' must be either 'true' or 'false'"
|
||||
):
|
||||
setattr(config, attribute, value)
|
||||
|
||||
# invalid ignore -> not here because ignore is a ListOption which converts everything to a string before
|
||||
# splitting which means it it will accept just about everything
|
||||
|
||||
# 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("_", "-")
|
||||
with self.assertRaisesMessage(LintConfigError,
|
||||
f"Option '{option_name}' must be either 'true' or 'false'"):
|
||||
with self.assertRaisesMessage(LintConfigError, f"Option '{option_name}' must be either 'true' or 'false'"):
|
||||
setattr(config, attribute, "föobar")
|
||||
|
||||
# extra-path has its own negative test
|
||||
|
||||
# invalid target
|
||||
with self.assertRaisesMessage(LintConfigError,
|
||||
"Option target must be an existing directory (current value: 'föo/bar')"):
|
||||
with self.assertRaisesMessage(
|
||||
LintConfigError, "Option target must be an existing directory (current value: 'föo/bar')"
|
||||
):
|
||||
config.target = "föo/bar"
|
||||
|
||||
def test_ignore_independent_from_rules(self):
|
||||
|
@ -259,12 +275,25 @@ class LintConfigTests(BaseTestCase):
|
|||
self.assertNotEqual(LintConfig(), LintConfigGenerator())
|
||||
|
||||
# Ensure LintConfig are not equal if they differ on their attributes
|
||||
attrs = [("verbosity", 1), ("rules", []), ("ignore_stdin", True), ("debug", True),
|
||||
("ignore", ["T1"]), ("staged", True), ("_config_path", self.get_sample_path()),
|
||||
("ignore_merge_commits", False), ("ignore_fixup_commits", False),
|
||||
("ignore_squash_commits", False), ("ignore_revert_commits", False),
|
||||
("extra_path", self.get_sample_path("user_rules")), ("target", self.get_sample_path()),
|
||||
("contrib", ["CC1"])]
|
||||
attrs = [
|
||||
("verbosity", 1),
|
||||
("rules", []),
|
||||
("ignore_stdin", True),
|
||||
("fail_without_commits", True),
|
||||
("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:
|
||||
config = LintConfig()
|
||||
setattr(config, attr, val)
|
||||
|
@ -281,7 +310,7 @@ class LintConfigTests(BaseTestCase):
|
|||
|
||||
class LintConfigGeneratorTests(BaseTestCase):
|
||||
@staticmethod
|
||||
@patch('gitlint.config.shutil.copyfile')
|
||||
@patch("gitlint.config.shutil.copyfile")
|
||||
def test_install_commit_msg_hook_negative(copy):
|
||||
LintConfigGenerator.generate_config("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
|
||||
|
||||
from gitlint.tests.base import BaseTestCase
|
||||
|
@ -14,24 +13,27 @@ class LintConfigBuilderTests(BaseTestCase):
|
|||
config = config_builder.build()
|
||||
|
||||
# assert some defaults
|
||||
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.assertListEqual(config.get_rule_option('title-must-not-contain-word', 'words'), ["WIP"])
|
||||
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.assertListEqual(config.get_rule_option("title-must-not-contain-word", "words"), ["WIP"])
|
||||
self.assertEqual(config.verbosity, 3)
|
||||
|
||||
# Make some changes and check blueprint
|
||||
config_builder.set_option('title-max-length', 'line-length', 100)
|
||||
config_builder.set_option('general', 'verbosity', 2)
|
||||
config_builder.set_option('title-must-not-contain-word', 'words', ["foo", "bar"])
|
||||
expected_blueprint = {'title-must-not-contain-word': {'words': ['foo', 'bar']},
|
||||
'title-max-length': {'line-length': 100}, 'general': {'verbosity': 2}}
|
||||
config_builder.set_option("title-max-length", "line-length", 100)
|
||||
config_builder.set_option("general", "verbosity", 2)
|
||||
config_builder.set_option("title-must-not-contain-word", "words", ["foo", "bar"])
|
||||
expected_blueprint = {
|
||||
"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)
|
||||
|
||||
# Build config and verify that the changes have occurred and no other changes
|
||||
config = config_builder.build()
|
||||
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.assertListEqual(config.get_rule_option('title-must-not-contain-word', 'words'), ["foo", "bar"])
|
||||
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.assertListEqual(config.get_rule_option("title-must-not-contain-word", "words"), ["foo", "bar"])
|
||||
self.assertEqual(config.verbosity, 2)
|
||||
|
||||
def test_set_from_commit_ignore_all(self):
|
||||
|
@ -82,8 +84,8 @@ class LintConfigBuilderTests(BaseTestCase):
|
|||
self.assertIsNone(config.extra_path)
|
||||
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('body-max-line-length', 'line-length'), 30)
|
||||
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)
|
||||
|
||||
def test_set_from_config_file_negative(self):
|
||||
config_builder = LintConfigBuilder()
|
||||
|
@ -129,8 +131,10 @@ class LintConfigBuilderTests(BaseTestCase):
|
|||
path = self.get_sample_path("config/invalid-option-value")
|
||||
config_builder = LintConfigBuilder()
|
||||
config_builder.set_from_config_file(path)
|
||||
expected_error_msg = "'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')."
|
||||
expected_error_msg = (
|
||||
"'föo' is not a valid value for option 'title-max-length.line-length'. "
|
||||
"Option 'line-length' must be a positive integer (current value: 'föo')."
|
||||
)
|
||||
with self.assertRaisesMessage(LintConfigError, expected_error_msg):
|
||||
config_builder.build()
|
||||
|
||||
|
@ -139,14 +143,19 @@ class LintConfigBuilderTests(BaseTestCase):
|
|||
|
||||
# change and assert changes
|
||||
config_builder = LintConfigBuilder()
|
||||
config_builder.set_config_from_string_list(['general.verbosity=1', 'title-max-length.line-length=60',
|
||||
'body-max-line-length.line-length=120',
|
||||
"title-must-not-contain-word.words=håha"])
|
||||
config_builder.set_config_from_string_list(
|
||||
[
|
||||
"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()
|
||||
self.assertEqual(config.get_rule_option('title-max-length', 'line-length'), 60)
|
||||
self.assertEqual(config.get_rule_option('body-max-line-length', 'line-length'), 120)
|
||||
self.assertListEqual(config.get_rule_option('title-must-not-contain-word', 'words'), ["håha"])
|
||||
self.assertEqual(config.get_rule_option("title-max-length", "line-length"), 60)
|
||||
self.assertEqual(config.get_rule_option("body-max-line-length", "line-length"), 120)
|
||||
self.assertListEqual(config.get_rule_option("title-must-not-contain-word", "words"), ["håha"])
|
||||
self.assertEqual(config.verbosity, 1)
|
||||
|
||||
def test_set_config_from_string_list_negative(self):
|
||||
|
@ -175,12 +184,12 @@ class LintConfigBuilderTests(BaseTestCase):
|
|||
# no period between rule and option names
|
||||
expected_msg = "'föobar=1' is an invalid configuration option. Use '<rule>.<option>=<value>'"
|
||||
with self.assertRaisesMessage(LintConfigError, expected_msg):
|
||||
config_builder.set_config_from_string_list([u'föobar=1'])
|
||||
config_builder.set_config_from_string_list(["föobar=1"])
|
||||
|
||||
def test_rebuild_config(self):
|
||||
# normal config build
|
||||
config_builder = LintConfigBuilder()
|
||||
config_builder.set_option('general', 'verbosity', 3)
|
||||
config_builder.set_option("general", "verbosity", 3)
|
||||
lint_config = config_builder.build()
|
||||
self.assertEqual(lint_config.verbosity, 3)
|
||||
|
||||
|
@ -193,9 +202,9 @@ class LintConfigBuilderTests(BaseTestCase):
|
|||
|
||||
def test_clone(self):
|
||||
config_builder = LintConfigBuilder()
|
||||
config_builder.set_option('general', 'verbosity', 2)
|
||||
config_builder.set_option('title-max-length', 'line-length', 100)
|
||||
expected = {'title-max-length': {'line-length': 100}, 'general': {'verbosity': 2}}
|
||||
config_builder.set_option("general", "verbosity", 2)
|
||||
config_builder.set_option("title-max-length", "line-length", 100)
|
||||
expected = {"title-max-length": {"line-length": 100}, "general": {"verbosity": 2}}
|
||||
self.assertDictEqual(config_builder._config_blueprint, expected)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
|
||||
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
|
||||
# Assert that whitespace in the rule name is stripped
|
||||
rule_qualifiers = [u'T7:my-extra-rüle', u' T7 : my-extra-rüle ', u'\tT7:\tmy-extra-rüle\t',
|
||||
u'T7:\t\n \tmy-extra-rüle\t\n\n', "title-match-regex:my-extra-rüle"]
|
||||
rule_qualifiers = [
|
||||
"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:
|
||||
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)
|
||||
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.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)
|
||||
|
||||
# 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
|
||||
for other_rule_qualifier in rule_qualifiers:
|
||||
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
|
||||
self.assertNotEqual(cb.build().rules, expected_rules)
|
||||
# after setting the option on the expected rule, it should be equal
|
||||
my_rule.options['regex'].set(other_rule_qualifier + "bōr")
|
||||
my_rule.options["regex"].set(other_rule_qualifier + "bōr")
|
||||
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):
|
||||
# T7 = title-match-regex
|
||||
# Invalid rule name
|
||||
for invalid_name in ["", " ", " ", "\t", "\n", "å b", "å:b", "åb:", ":åb"]:
|
||||
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"
|
||||
with self.assertRaisesMessage(LintConfigError, expected_msg):
|
||||
config_builder.build()
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from io import StringIO
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
@ -13,9 +11,10 @@ from gitlint.config import LintConfigBuilder
|
|||
|
||||
class LintConfigPrecedenceTests(BaseTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
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, _):
|
||||
# TODO(jroovers): this test really only test verbosity, we need to do some refactoring to gitlint.cli
|
||||
# to more easily test everything
|
||||
|
@ -28,60 +27,63 @@ class LintConfigPrecedenceTests(BaseTestCase):
|
|||
config_path = self.get_sample_path("config/gitlintconfig")
|
||||
|
||||
# 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])
|
||||
self.assertEqual(result.output, "")
|
||||
self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n")
|
||||
|
||||
# 2. environment variables
|
||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
||||
result = self.cli.invoke(cli.cli, ["-c", "general.verbosity=2", "--config", config_path],
|
||||
env={"GITLINT_VERBOSITY": "3"})
|
||||
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||
result = self.cli.invoke(
|
||||
cli.cli, ["-c", "general.verbosity=2", "--config", config_path], env={"GITLINT_VERBOSITY": "3"}
|
||||
)
|
||||
self.assertEqual(result.output, "")
|
||||
self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP:fö\"\n")
|
||||
|
||||
# 3. commandline -c flags
|
||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
||||
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||
result = self.cli.invoke(cli.cli, ["-c", "general.verbosity=2", "--config", config_path])
|
||||
self.assertEqual(result.output, "")
|
||||
self.assertEqual(stderr.getvalue(), "1: T5 Title contains the word 'WIP' (case-insensitive)\n")
|
||||
|
||||
# 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])
|
||||
self.assertEqual(result.output, "")
|
||||
self.assertEqual(stderr.getvalue(), "1: T5\n")
|
||||
|
||||
# 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)
|
||||
self.assertEqual(result.output, "")
|
||||
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):
|
||||
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
|
||||
result = self.cli.invoke(cli.cli, ["-c", "general.ignore=T5", "--ignore", "B6"])
|
||||
self.assertEqual(result.output, "")
|
||||
self.assertEqual(result.exit_code, 1)
|
||||
# We still expect the T5 violation, but no B6 violation as --ignore overwrites -c general.ignore
|
||||
self.assertEqual(stderr.getvalue(),
|
||||
"1: T5 Title contains the word 'WIP' (case-insensitive): \"WIP: This is å test\"\n")
|
||||
self.assertEqual(
|
||||
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
|
||||
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"
|
||||
# --ignore takes precedence over -c general.ignore
|
||||
result = self.cli.invoke(cli.cli, ["-c", "general.ignore=title-max-length",
|
||||
"-c", "title-max-length.line-length=5",
|
||||
"--ignore", "B6"])
|
||||
result = self.cli.invoke(
|
||||
cli.cli,
|
||||
["-c", "general.ignore=title-max-length", "-c", "title-max-length.line-length=5", "--ignore", "B6"],
|
||||
)
|
||||
self.assertEqual(result.output, "")
|
||||
self.assertEqual(result.exit_code, 1)
|
||||
|
||||
# We still expect the T1 violation with custom config,
|
||||
# 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):
|
||||
# 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.
|
||||
|
||||
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")
|
||||
config_builder.set_option('general', 'extra-path', user_rules_path)
|
||||
config_builder.set_option("general", "extra-path", user_rules_path)
|
||||
config = config_builder.build()
|
||||
|
||||
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 gitlint import rules
|
||||
from gitlint.config import RuleCollection
|
||||
|
@ -7,7 +5,6 @@ from gitlint.tests.base import BaseTestCase
|
|||
|
||||
|
||||
class RuleCollectionTests(BaseTestCase):
|
||||
|
||||
def test_add_rule(self):
|
||||
collection = RuleCollection()
|
||||
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
|
||||
expected = rules.TitleMaxLength()
|
||||
rule = collection.find_rule('T1')
|
||||
rule = collection.find_rule("T1")
|
||||
self.assertEqual(rule, expected)
|
||||
self.assertEqual(rule.my_attr, "föo")
|
||||
|
||||
# find by name
|
||||
expected2 = rules.TitleTrailingWhitespace()
|
||||
rule = collection.find_rule('title-trailing-whitespace')
|
||||
rule = collection.find_rule("title-trailing-whitespace")
|
||||
self.assertEqual(rule, expected2)
|
||||
self.assertEqual(rule.my_attr, "föo")
|
||||
|
||||
# find non-existing
|
||||
rule = collection.find_rule(u'föo')
|
||||
rule = collection.find_rule("föo")
|
||||
self.assertIsNone(rule)
|
||||
|
||||
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.rules import RuleViolation
|
||||
from gitlint.contrib.rules.conventional_commit import ConventionalCommit
|
||||
|
@ -7,10 +5,9 @@ from gitlint.config import LintConfig
|
|||
|
||||
|
||||
class ContribConventionalCommitTests(BaseTestCase):
|
||||
|
||||
def test_enable(self):
|
||||
# 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.contrib = [rule_ref]
|
||||
self.assertIn(ConventionalCommit(), config.rules)
|
||||
|
@ -24,28 +21,38 @@ class ContribConventionalCommitTests(BaseTestCase):
|
|||
self.assertListEqual([], violations)
|
||||
|
||||
# assert violation on wrong type
|
||||
expected_violation = RuleViolation("CT1", "Title does not start with one of fix, feat, chore, docs,"
|
||||
" style, refactor, perf, test, revert, ci, build", "bår: foo")
|
||||
expected_violation = RuleViolation(
|
||||
"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)
|
||||
self.assertListEqual([expected_violation], violations)
|
||||
|
||||
# assert violation when use strange chars after correct type
|
||||
expected_violation = RuleViolation("CT1", "Title does not start with one of fix, feat, chore, docs,"
|
||||
" style, refactor, perf, test, revert, ci, build",
|
||||
"feat_wrong_chars: föo")
|
||||
expected_violation = RuleViolation(
|
||||
"CT1",
|
||||
"Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert, ci, build",
|
||||
"feat_wrong_chars: föo",
|
||||
)
|
||||
violations = rule.validate("feat_wrong_chars: föo", None)
|
||||
self.assertListEqual([expected_violation], violations)
|
||||
|
||||
# assert violation when use strange chars after correct type
|
||||
expected_violation = RuleViolation("CT1", "Title does not start with one of fix, feat, chore, docs,"
|
||||
" style, refactor, perf, test, revert, ci, build",
|
||||
"feat_wrong_chars(scope): föo")
|
||||
expected_violation = RuleViolation(
|
||||
"CT1",
|
||||
"Title does not start with one of fix, feat, chore, docs, style, refactor, perf, test, revert, ci, build",
|
||||
"feat_wrong_chars(scope): föo",
|
||||
)
|
||||
violations = rule.validate("feat_wrong_chars(scope): föo", None)
|
||||
self.assertListEqual([expected_violation], violations)
|
||||
|
||||
# assert violation on wrong format
|
||||
expected_violation = RuleViolation("CT1", "Title does not follow ConventionalCommits.org format "
|
||||
"'type(optional-scope): description'", "fix föo")
|
||||
expected_violation = RuleViolation(
|
||||
"CT1",
|
||||
"Title does not follow ConventionalCommits.org format 'type(optional-scope): description'",
|
||||
"fix föo",
|
||||
)
|
||||
violations = rule.validate("fix föo", None)
|
||||
self.assertListEqual([expected_violation], violations)
|
||||
|
||||
|
@ -58,7 +65,7 @@ class ContribConventionalCommitTests(BaseTestCase):
|
|||
self.assertListEqual([], violations)
|
||||
|
||||
# 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"]:
|
||||
violations = rule.validate(typ + ": hür dur", None)
|
||||
self.assertListEqual([], violations)
|
||||
|
@ -69,7 +76,7 @@ class ContribConventionalCommitTests(BaseTestCase):
|
|||
self.assertListEqual([expected_violation], violations)
|
||||
|
||||
# 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"]:
|
||||
violations = rule.validate(typ + ": hür dur", None)
|
||||
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.rules import RuleViolation
|
||||
from gitlint.contrib.rules.signedoff_by import SignedOffBy
|
||||
|
@ -8,10 +6,9 @@ from gitlint.config import LintConfig
|
|||
|
||||
|
||||
class ContribSignedOffByTests(BaseTestCase):
|
||||
|
||||
def test_enable(self):
|
||||
# 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.contrib = [rule_ref]
|
||||
self.assertIn(SignedOffBy(), config.rules)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
from gitlint.tests.base import BaseTestCase
|
||||
|
@ -8,13 +7,12 @@ from gitlint import rule_finder, rules
|
|||
|
||||
|
||||
class ContribRuleTests(BaseTestCase):
|
||||
|
||||
CONTRIB_DIR = os.path.dirname(os.path.realpath(contrib_rules.__file__))
|
||||
|
||||
def test_contrib_tests_exist(self):
|
||||
""" 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
|
||||
of the tests file), it's a good leading indicator. """
|
||||
"""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
|
||||
of the tests file), it's a good leading indicator."""
|
||||
|
||||
contrib_tests_dir = os.path.dirname(os.path.realpath(contrib_tests.__file__))
|
||||
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
|
||||
for filename in os.listdir(self.CONTRIB_DIR):
|
||||
if filename.endswith(".py") and filename not in ["__init__.py"]:
|
||||
expected_test_file = "test_" + filename
|
||||
error_msg = "Every Contrib Rule must have associated tests. " + \
|
||||
f"Expected test file {os.path.join(contrib_tests_dir, expected_test_file)} not found."
|
||||
expected_test_file = f"test_{filename}"
|
||||
error_msg = (
|
||||
"Every Contrib Rule must have associated tests. "
|
||||
f"Expected test file {os.path.join(contrib_tests_dir, expected_test_file)} not found."
|
||||
)
|
||||
self.assertIn(expected_test_file, contrib_test_files, error_msg)
|
||||
|
||||
def test_contrib_rule_naming_conventions(self):
|
||||
""" 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)
|
||||
because these are contrib rules: once they're part of gitlint they can't change unless they pass this test
|
||||
again.
|
||||
"""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)
|
||||
because these are contrib rules: once they're part of gitlint they can't change unless they pass this test
|
||||
again.
|
||||
"""
|
||||
rule_classes = rule_finder.find_rule_classes(self.CONTRIB_DIR)
|
||||
|
||||
|
@ -47,10 +47,10 @@ class ContribRuleTests(BaseTestCase):
|
|||
self.assertTrue(clazz.id.startswith("CB"))
|
||||
|
||||
def test_contrib_rule_uniqueness(self):
|
||||
""" 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)
|
||||
because these are contrib rules: once they're part of gitlint they can't change unless they pass this test
|
||||
again.
|
||||
"""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)
|
||||
because these are contrib rules: once they're part of gitlint they can't change unless they pass this test
|
||||
again.
|
||||
"""
|
||||
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))
|
||||
|
||||
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)
|
||||
|
||||
# No exceptions = what we want :-)
|
||||
|
|
|
@ -13,11 +13,13 @@ contrib: []
|
|||
ignore: title-trailing-whitespace,B2
|
||||
ignore-merge-commits: False
|
||||
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: 1
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -59,7 +61,7 @@ target: {target}
|
|||
B8: body-match-regex
|
||||
regex=None
|
||||
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.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 ('config', '--get', 'core.commentchar')
|
||||
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 ('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', '6f29bf81a8322a04071bb794666e48c443a90360')
|
||||
DEBUG: gitlint.lint Commit Object
|
||||
--- Commit Message ----
|
||||
commït-title1
|
||||
|
@ -79,15 +81,20 @@ Author: test åuthor1 <test-email1@föo.com>
|
|||
Date: 2016-12-03 15:28:15 +0100
|
||||
is-merge-commit: False
|
||||
is-fixup-commit: False
|
||||
is-fixup-amend-commit: False
|
||||
is-squash-commit: False
|
||||
is-revert-commit: False
|
||||
Parents: ['a123']
|
||||
Branches: ['commit-1-branch-1', 'commit-1-branch-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.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 ('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', '25053ccec5e28e1bb8f7551fdbb5ab213ada2401')
|
||||
DEBUG: gitlint.lint Commit Object
|
||||
--- Commit Message ----
|
||||
commït-title2.
|
||||
|
@ -98,15 +105,20 @@ Author: test åuthor2 <test-email2@föo.com>
|
|||
Date: 2016-12-04 15:28:15 +0100
|
||||
is-merge-commit: False
|
||||
is-fixup-commit: False
|
||||
is-fixup-amend-commit: False
|
||||
is-squash-commit: False
|
||||
is-revert-commit: False
|
||||
Parents: ['b123']
|
||||
Branches: ['commit-2-branch-1', 'commit-2-branch-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.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 ('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', '4da2656b0dadc76c7ee3fd0243a96cb64007f125')
|
||||
DEBUG: gitlint.lint Commit Object
|
||||
--- Commit Message ----
|
||||
föobar
|
||||
|
@ -116,9 +128,14 @@ Author: test åuthor3 <test-email3@föo.com>
|
|||
Date: 2016-12-05 15:28:15 +0100
|
||||
is-merge-commit: False
|
||||
is-fixup-commit: False
|
||||
is-fixup-amend-commit: False
|
||||
is-squash-commit: False
|
||||
is-revert-commit: False
|
||||
Parents: ['c123']
|
||||
Branches: ['commit-3-branch-1', 'commit-3-branch-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
|
|
@ -13,11 +13,13 @@ 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}
|
||||
|
@ -59,7 +61,7 @@ target: {target}
|
|||
B8: body-match-regex
|
||||
regex=None
|
||||
M1: author-valid-email
|
||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
||||
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||
|
||||
DEBUG: gitlint.cli Stdin data: 'WIP: tïtle
|
||||
'
|
||||
|
@ -75,9 +77,12 @@ Author: None <None>
|
|||
Date: None
|
||||
is-merge-commit: False
|
||||
is-fixup-commit: False
|
||||
is-fixup-amend-commit: False
|
||||
is-squash-commit: False
|
||||
is-revert-commit: False
|
||||
Parents: []
|
||||
Branches: []
|
||||
Changed Files: []
|
||||
Changed Files Stats: {{}}
|
||||
-----------------------
|
||||
DEBUG: gitlint.cli Exit Code = 3
|
|
@ -13,11 +13,13 @@ 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: True
|
||||
fail-without-commits: False
|
||||
regex-style-search: False
|
||||
verbosity: 3
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -59,17 +61,17 @@ target: {target}
|
|||
B8: body-match-regex
|
||||
regex=None
|
||||
M1: author-valid-email
|
||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
||||
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||
|
||||
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
|
||||
DEBUG: gitlint.cli Using --msg-filename.
|
||||
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
||||
DEBUG: gitlint.cli Linting 1 commit(s)
|
||||
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
|
||||
DEBUG: gitlint.git ('diff', '--staged', '--numstat', '-r')
|
||||
DEBUG: gitlint.git ('config', '--get', 'user.name')
|
||||
DEBUG: gitlint.git ('config', '--get', 'user.email')
|
||||
DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
|
||||
DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r')
|
||||
DEBUG: gitlint.lint Commit Object
|
||||
--- Commit Message ----
|
||||
WIP: msg-filename tïtle
|
||||
|
@ -78,9 +80,14 @@ Author: föo user <föo@bar.com>
|
|||
Date: 2020-02-19 12:18:46 +0100
|
||||
is-merge-commit: False
|
||||
is-fixup-commit: False
|
||||
is-fixup-amend-commit: False
|
||||
is-squash-commit: False
|
||||
is-revert-commit: False
|
||||
Parents: []
|
||||
Branches: ['my-branch']
|
||||
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
|
|
@ -13,11 +13,13 @@ 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: True
|
||||
fail-without-commits: False
|
||||
regex-style-search: False
|
||||
verbosity: 3
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -59,7 +61,7 @@ target: {target}
|
|||
B8: body-match-regex
|
||||
regex=None
|
||||
M1: author-valid-email
|
||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
||||
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||
|
||||
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
|
||||
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.cli Linting 1 commit(s)
|
||||
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.email')
|
||||
DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
|
||||
DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r')
|
||||
DEBUG: gitlint.lint Commit Object
|
||||
--- Commit Message ----
|
||||
WIP: tïtle
|
||||
|
@ -80,9 +82,14 @@ Author: föo user <föo@bar.com>
|
|||
Date: 2020-02-19 12:18:46 +0100
|
||||
is-merge-commit: False
|
||||
is-fixup-commit: False
|
||||
is-fixup-amend-commit: False
|
||||
is-squash-commit: False
|
||||
is-revert-commit: False
|
||||
Parents: []
|
||||
Branches: ['my-branch']
|
||||
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
|
|
@ -13,11 +13,13 @@ 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}
|
||||
|
@ -59,7 +61,7 @@ target: {target}
|
|||
B8: body-match-regex
|
||||
regex=None
|
||||
M1: author-valid-email
|
||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
||||
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||
T5:extra-wörds: title-must-not-contain-word:extra-wörds
|
||||
words=hür,tëst
|
||||
T5:even-more-wörds: title-must-not-contain-word:even-more-wörds
|
||||
|
@ -78,9 +80,12 @@ Author: None <None>
|
|||
Date: None
|
||||
is-merge-commit: False
|
||||
is-fixup-commit: False
|
||||
is-fixup-amend-commit: False
|
||||
is-squash-commit: False
|
||||
is-revert-commit: False
|
||||
Parents: []
|
||||
Branches: []
|
||||
Changed Files: []
|
||||
Changed Files Stats: {{}}
|
||||
-----------------------
|
||||
DEBUG: gitlint.cli Exit Code = 4
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, call
|
||||
|
||||
from gitlint.shell import ErrorReturnCode, CommandNotFound
|
||||
|
||||
|
@ -10,25 +9,23 @@ from gitlint.git import GitContext, GitContextError, GitNotInstalledError, git_c
|
|||
|
||||
|
||||
class GitTests(BaseTestCase):
|
||||
|
||||
# Expected special_args passed to 'sh'
|
||||
expected_sh_special_args = {
|
||||
'_tty_out': False,
|
||||
'_cwd': "fåke/path"
|
||||
}
|
||||
expected_sh_special_args = {"_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):
|
||||
sh.git.side_effect = CommandNotFound("git")
|
||||
expected_msg = "'git' command not found. You need to install git to use gitlint on a local repository. " + \
|
||||
"See https://git-scm.com/book/en/v2/Getting-Started-Installing-Git on how to install git."
|
||||
expected_msg = (
|
||||
"'git' command not found. You need to install git to use gitlint on a local repository. "
|
||||
+ "See https://git-scm.com/book/en/v2/Getting-Started-Installing-Git on how to install git."
|
||||
)
|
||||
with self.assertRaisesMessage(GitNotInstalledError, expected_msg):
|
||||
GitContext.from_local_repository("fåke/path")
|
||||
|
||||
# assert that commit message was read using git command
|
||||
sh.git.assert_called_once_with("log", "-1", "--pretty=%H", **self.expected_sh_special_args)
|
||||
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.git.sh")
|
||||
def test_get_latest_commit_git_error(self, sh):
|
||||
# Current directory not a git repo
|
||||
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
|
||||
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):
|
||||
# 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)
|
||||
|
||||
|
@ -64,25 +61,38 @@ class GitTests(BaseTestCase):
|
|||
|
||||
# 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.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'
|
||||
err = (b"HEAD"
|
||||
b"fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree."
|
||||
b"Use '--' to separate paths from revisions, like this:"
|
||||
b"'git <command> [<revision>...] -- [<file>...]'")
|
||||
err = (
|
||||
b"HEAD"
|
||||
b"fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree."
|
||||
b"Use '--' to separate paths from revisions, like this:"
|
||||
b"'git <command> [<revision>...] -- [<file>...]'"
|
||||
)
|
||||
|
||||
sh.git.side_effect = [
|
||||
"#\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.current_branch
|
||||
context = GitContext.from_commit_msg("test")
|
||||
self.assertEqual(context.current_branch, "test-branch")
|
||||
|
||||
# assert that commit message was read using git command
|
||||
sh.git.assert_called_with("rev-parse", "--abbrev-ref", "HEAD", _tty_out=False, _cwd=None)
|
||||
# assert that we try using `git rev-parse` first, and if that fails (as will be the case with the first commit),
|
||||
# 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")
|
||||
def test_git_commentchar(self, git):
|
||||
|
@ -93,11 +103,10 @@ class GitTests(BaseTestCase):
|
|||
git.return_value = "ä"
|
||||
self.assertEqual(git_commentchar(), "ä")
|
||||
|
||||
git.return_value = ';\n'
|
||||
self.assertEqual(git_commentchar(os.path.join("/föo", "bar")), ';')
|
||||
git.return_value = ";\n"
|
||||
self.assertEqual(git_commentchar(os.path.join("/föo", "bar")), ";")
|
||||
|
||||
git.assert_called_with("config", "--get", "core.commentchar", _ok_code=[0, 1],
|
||||
_cwd=os.path.join("/föo", "bar"))
|
||||
git.assert_called_with("config", "--get", "core.commentchar", _ok_code=[0, 1], _cwd=os.path.join("/föo", "bar"))
|
||||
|
||||
@patch("gitlint.git._git")
|
||||
def test_git_hooks_dir(self, git):
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import copy
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import dateutil
|
||||
|
||||
|
@ -9,29 +9,33 @@ import arrow
|
|||
from unittest.mock import patch, call
|
||||
|
||||
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
|
||||
|
||||
|
||||
class GitCommitTests(BaseTestCase):
|
||||
|
||||
# Expected special_args passed to 'sh'
|
||||
expected_sh_special_args = {
|
||||
'_tty_out': False,
|
||||
'_cwd': "fåke/path"
|
||||
}
|
||||
expected_sh_special_args = {"_tty_out": False, "_cwd": "fåke/path"}
|
||||
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.git.sh")
|
||||
def test_get_latest_commit(self, sh):
|
||||
sample_sha = "d8ac47e9f2923c7f22d8668e3a1ed04eb4cdbca9"
|
||||
|
||||
sh.git.side_effect = [
|
||||
sample_sha,
|
||||
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
||||
"cömmit-title\n\ncömmit-body",
|
||||
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\ncömmit-title\n\ncömmit-body",
|
||||
"#", # git config --get core.commentchar
|
||||
"file1.txt\npåth/to/file2.txt\n",
|
||||
"foöbar\n* hürdur\n"
|
||||
"4\t15\tfile1.txt\n-\t-\tpåth/to/file2.bin\n",
|
||||
"foöbar\n* hürdur\n",
|
||||
]
|
||||
|
||||
context = GitContext.from_local_repository("fåke/path")
|
||||
|
@ -39,10 +43,17 @@ class GitCommitTests(BaseTestCase):
|
|||
expected_calls = [
|
||||
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('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,
|
||||
**self.expected_sh_special_args),
|
||||
call('branch', '--contains', sample_sha, **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",
|
||||
"--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
|
||||
|
@ -55,18 +66,26 @@ class GitCommitTests(BaseTestCase):
|
|||
self.assertEqual(last_commit.message.body, ["", "cömmit-body"])
|
||||
self.assertEqual(last_commit.author_name, "test åuthor")
|
||||
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
||||
self.assertEqual(last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15,
|
||||
tzinfo=dateutil.tz.tzoffset("+0100", 3600)))
|
||||
self.assertEqual(
|
||||
last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15, tzinfo=dateutil.tz.tzoffset("+0100", 3600))
|
||||
)
|
||||
self.assertListEqual(last_commit.parents, ["åbc"])
|
||||
self.assertFalse(last_commit.is_merge_commit)
|
||||
self.assertFalse(last_commit.is_fixup_commit)
|
||||
self.assertFalse(last_commit.is_fixup_amend_commit)
|
||||
self.assertFalse(last_commit.is_squash_commit)
|
||||
self.assertFalse(last_commit.is_revert_commit)
|
||||
|
||||
# First 2 'git log' calls should've happened at this point
|
||||
self.assertListEqual(sh.git.mock_calls, expected_calls[:3])
|
||||
|
||||
self.assertListEqual(last_commit.changed_files, ["file1.txt", "påth/to/file2.txt"])
|
||||
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
|
||||
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
|
||||
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):
|
||||
sample_refspec = "åbc123..def456"
|
||||
sample_sha = "åbc123"
|
||||
|
||||
sh.git.side_effect = [
|
||||
sample_sha, # git rev-list <sample_refspec>
|
||||
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
||||
"cömmit-title\n\ncömmit-body",
|
||||
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\ncömmit-title\n\ncömmit-body",
|
||||
"#", # git config --get core.commentchar
|
||||
"file1.txt\npåth/to/file2.txt\n",
|
||||
"foöbar\n* hürdur\n"
|
||||
"7\t10\tfile1.txt\n9\t12\tpåth/to/file2.txt\n",
|
||||
"foöbar\n* hürdur\n",
|
||||
]
|
||||
|
||||
context = GitContext.from_local_repository("fåke/path", refspec=sample_refspec)
|
||||
|
@ -93,10 +111,17 @@ class GitCommitTests(BaseTestCase):
|
|||
expected_calls = [
|
||||
call("rev-list", sample_refspec, **self.expected_sh_special_args),
|
||||
call("log", sample_sha, "-1", "--pretty=%aN%x00%aE%x00%ai%x00%P%n%B", **self.expected_sh_special_args),
|
||||
call('config', '--get', 'core.commentchar', _ok_code=[0, 1], **self.expected_sh_special_args),
|
||||
call('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', sample_sha,
|
||||
**self.expected_sh_special_args),
|
||||
call('branch', '--contains', sample_sha, **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",
|
||||
"--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
|
||||
|
@ -109,11 +134,13 @@ class GitCommitTests(BaseTestCase):
|
|||
self.assertEqual(last_commit.message.body, ["", "cömmit-body"])
|
||||
self.assertEqual(last_commit.author_name, "test åuthor")
|
||||
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
||||
self.assertEqual(last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15,
|
||||
tzinfo=dateutil.tz.tzoffset("+0100", 3600)))
|
||||
self.assertEqual(
|
||||
last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15, tzinfo=dateutil.tz.tzoffset("+0100", 3600))
|
||||
)
|
||||
self.assertListEqual(last_commit.parents, ["åbc"])
|
||||
self.assertFalse(last_commit.is_merge_commit)
|
||||
self.assertFalse(last_commit.is_fixup_commit)
|
||||
self.assertFalse(last_commit.is_fixup_amend_commit)
|
||||
self.assertFalse(last_commit.is_squash_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(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
|
||||
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
|
||||
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):
|
||||
sample_hash = "åbc123"
|
||||
|
||||
sh.git.side_effect = [
|
||||
sample_hash, # git log -1 <sample_hash>
|
||||
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\n"
|
||||
"cömmit-title\n\ncömmit-body",
|
||||
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc\ncömmit-title\n\ncömmit-body",
|
||||
"#", # git config --get core.commentchar
|
||||
"file1.txt\npåth/to/file2.txt\n",
|
||||
"foöbar\n* hürdur\n"
|
||||
"8\t3\tfile1.txt\n1\t4\tpåth/to/file2.txt\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
|
||||
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('config', '--get', 'core.commentchar', _ok_code=[0, 1], **self.expected_sh_special_args),
|
||||
call('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', sample_hash,
|
||||
**self.expected_sh_special_args),
|
||||
call('branch', '--contains', sample_hash, **self.expected_sh_special_args)
|
||||
call("config", "--get", "core.commentchar", _ok_code=[0, 1], **self.expected_sh_special_args),
|
||||
call(
|
||||
"diff-tree",
|
||||
"--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
|
||||
|
@ -162,11 +201,13 @@ class GitCommitTests(BaseTestCase):
|
|||
self.assertEqual(last_commit.message.body, ["", "cömmit-body"])
|
||||
self.assertEqual(last_commit.author_name, "test åuthor")
|
||||
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
||||
self.assertEqual(last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15,
|
||||
tzinfo=dateutil.tz.tzoffset("+0100", 3600)))
|
||||
self.assertEqual(
|
||||
last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15, tzinfo=dateutil.tz.tzoffset("+0100", 3600))
|
||||
)
|
||||
self.assertListEqual(last_commit.parents, ["åbc"])
|
||||
self.assertFalse(last_commit.is_merge_commit)
|
||||
self.assertFalse(last_commit.is_fixup_commit)
|
||||
self.assertFalse(last_commit.is_fixup_amend_commit)
|
||||
self.assertFalse(last_commit.is_squash_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(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
|
||||
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
|
||||
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):
|
||||
sample_sha = "d8ac47e9f2923c7f22d8668e3a1ed04eb4cdbca9"
|
||||
|
||||
sh.git.side_effect = [
|
||||
sample_sha,
|
||||
"test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc def\n"
|
||||
"Merge \"foo bår commit\"",
|
||||
'test åuthor\x00test-emåil@foo.com\x002016-12-03 15:28:15 +0100\x00åbc def\nMerge "foo bår commit"',
|
||||
"#", # git config --get core.commentchar
|
||||
"file1.txt\npåth/to/file2.txt\n",
|
||||
"foöbar\n* hürdur\n"
|
||||
"6\t2\tfile1.txt\n1\t4\tpåth/to/file2.txt\n",
|
||||
"foöbar\n* hürdur\n",
|
||||
]
|
||||
|
||||
context = GitContext.from_local_repository("fåke/path")
|
||||
|
@ -199,10 +331,17 @@ class GitCommitTests(BaseTestCase):
|
|||
expected_calls = [
|
||||
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('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,
|
||||
**self.expected_sh_special_args),
|
||||
call('branch', '--contains', sample_sha, **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",
|
||||
"--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
|
||||
|
@ -211,15 +350,17 @@ class GitCommitTests(BaseTestCase):
|
|||
last_commit = context.commits[-1]
|
||||
self.assertIsInstance(last_commit, LocalGitCommit)
|
||||
self.assertEqual(last_commit.sha, sample_sha)
|
||||
self.assertEqual(last_commit.message.title, "Merge \"foo bår commit\"")
|
||||
self.assertEqual(last_commit.message.title, 'Merge "foo bår commit"')
|
||||
self.assertEqual(last_commit.message.body, [])
|
||||
self.assertEqual(last_commit.author_name, "test åuthor")
|
||||
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
||||
self.assertEqual(last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15,
|
||||
tzinfo=dateutil.tz.tzoffset("+0100", 3600)))
|
||||
self.assertEqual(
|
||||
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.assertTrue(last_commit.is_merge_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_revert_commit)
|
||||
|
||||
|
@ -227,6 +368,11 @@ class GitCommitTests(BaseTestCase):
|
|||
self.assertListEqual(sh.git.mock_calls, expected_calls[:3])
|
||||
|
||||
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
|
||||
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
|
||||
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):
|
||||
commit_types = ["fixup", "squash"]
|
||||
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():
|
||||
sample_sha = "d8ac47e9f2923c7f22d8668e3a1ed04eb4cdbca9"
|
||||
|
||||
sh.git.side_effect = [
|
||||
sample_sha,
|
||||
"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
|
||||
"file1.txt\npåth/to/file2.txt\n",
|
||||
"foöbar\n* hürdur\n"
|
||||
"8\t2\tfile1.txt\n7\t3\tpåth/to/file2.txt\n",
|
||||
"foöbar\n* hürdur\n",
|
||||
]
|
||||
|
||||
context = GitContext.from_local_repository("fåke/path")
|
||||
|
@ -254,10 +400,17 @@ class GitCommitTests(BaseTestCase):
|
|||
expected_calls = [
|
||||
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('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,
|
||||
**self.expected_sh_special_args),
|
||||
call('branch', '--contains', sample_sha, **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",
|
||||
"--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
|
||||
|
@ -266,27 +419,31 @@ class GitCommitTests(BaseTestCase):
|
|||
last_commit = context.commits[-1]
|
||||
self.assertIsInstance(last_commit, LocalGitCommit)
|
||||
self.assertEqual(last_commit.sha, sample_sha)
|
||||
self.assertEqual(last_commit.message.title, 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.author_name, "test åuthor")
|
||||
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
||||
self.assertEqual(last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15,
|
||||
tzinfo=dateutil.tz.tzoffset("+0100", 3600)))
|
||||
self.assertEqual(
|
||||
last_commit.date, datetime.datetime(2016, 12, 3, 15, 28, 15, tzinfo=dateutil.tz.tzoffset("+0100", 3600))
|
||||
)
|
||||
self.assertListEqual(last_commit.parents, ["åbc"])
|
||||
|
||||
# First 2 'git log' calls should've happened at this point
|
||||
self.assertEqual(sh.git.mock_calls, expected_calls[:3])
|
||||
|
||||
# Asserting that squash and fixup are correct
|
||||
for type in commit_types:
|
||||
attr = "is_" + type + "_commit"
|
||||
for type, attr in commit_prefixes.items():
|
||||
self.assertEqual(getattr(last_commit, attr), commit_type == type)
|
||||
|
||||
self.assertFalse(last_commit.is_merge_commit)
|
||||
self.assertFalse(last_commit.is_revert_commit)
|
||||
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
|
||||
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"))
|
||||
|
||||
expected_title = "Commit title contåining 'WIP', as well as trailing punctuation."
|
||||
expected_body = ["This line should be empty",
|
||||
"This is the first line of the commit message body and it is meant to test a " +
|
||||
"line that exceeds the maximum line length of 80 characters.",
|
||||
"This line has a tråiling space. ",
|
||||
"This line has a trailing tab.\t"]
|
||||
expected_body = [
|
||||
"This line should be empty",
|
||||
"This is the first line of the commit message body and it is meant to test a "
|
||||
+ "line that exceeds the maximum line length of 80 characters.",
|
||||
"This line has a tråiling space. ",
|
||||
"This line has a trailing tab.\t",
|
||||
]
|
||||
expected_full = expected_title + "\n" + "\n".join(expected_body)
|
||||
expected_original = expected_full + (
|
||||
"\n# This is a cömmented line\n"
|
||||
expected_original = (
|
||||
expected_full + "\n# This is a cömmented line\n"
|
||||
"# ------------------------ >8 ------------------------\n"
|
||||
"# Anything after this line should be cleaned up\n"
|
||||
"# this line appears on `git commit -v` command\n"
|
||||
|
@ -335,6 +494,7 @@ class GitCommitTests(BaseTestCase):
|
|||
self.assertListEqual(commit.branches, [])
|
||||
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)
|
||||
self.assertEqual(len(gitcontext.commits), 1)
|
||||
|
@ -355,6 +515,7 @@ class GitCommitTests(BaseTestCase):
|
|||
self.assertListEqual(commit.branches, [])
|
||||
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)
|
||||
self.assertEqual(len(gitcontext.commits), 1)
|
||||
|
@ -376,6 +537,7 @@ class GitCommitTests(BaseTestCase):
|
|||
self.assertListEqual(commit.branches, [])
|
||||
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)
|
||||
self.assertEqual(len(gitcontext.commits), 1)
|
||||
|
@ -400,6 +562,7 @@ class GitCommitTests(BaseTestCase):
|
|||
self.assertFalse(commit.is_merge_commit)
|
||||
self.assertFalse(commit.is_fixup_commit)
|
||||
self.assertFalse(commit.is_squash_commit)
|
||||
self.assertFalse(commit.is_fixup_amend_commit)
|
||||
self.assertFalse(commit.is_revert_commit)
|
||||
self.assertEqual(len(gitcontext.commits), 1)
|
||||
|
||||
|
@ -421,18 +584,19 @@ class GitCommitTests(BaseTestCase):
|
|||
self.assertListEqual(commit.branches, [])
|
||||
self.assertTrue(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)
|
||||
self.assertEqual(len(gitcontext.commits), 1)
|
||||
|
||||
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)
|
||||
commit = gitcontext.commits[-1]
|
||||
|
||||
self.assertIsInstance(commit, GitCommit)
|
||||
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.full, commit_msg)
|
||||
self.assertEqual(commit.message.original, commit_msg)
|
||||
|
@ -443,13 +607,16 @@ class GitCommitTests(BaseTestCase):
|
|||
self.assertListEqual(commit.branches, [])
|
||||
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.assertTrue(commit.is_revert_commit)
|
||||
self.assertEqual(len(gitcontext.commits), 1)
|
||||
|
||||
def test_from_commit_msg_fixup_squash_commit(self):
|
||||
commit_types = ["fixup", "squash"]
|
||||
for commit_type in commit_types:
|
||||
def test_from_commit_msg_fixup_squash_amend_commit(self):
|
||||
# mapping between cleanup commit prefixes and the commit object attribute
|
||||
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"
|
||||
gitcontext = GitContext.from_commit_msg(commit_msg)
|
||||
commit = gitcontext.commits[-1]
|
||||
|
@ -469,34 +636,33 @@ class GitCommitTests(BaseTestCase):
|
|||
self.assertFalse(commit.is_merge_commit)
|
||||
self.assertFalse(commit.is_revert_commit)
|
||||
# Asserting that squash and fixup are correct
|
||||
for type in commit_types:
|
||||
attr = "is_" + type + "_commit"
|
||||
self.assertEqual(getattr(commit, attr), commit_type == type)
|
||||
for type, commit_attr_name in commit_prefixes.items():
|
||||
self.assertEqual(getattr(commit, commit_attr_name), commit_type == type)
|
||||
|
||||
@patch('gitlint.git.sh')
|
||||
@patch('arrow.now')
|
||||
@patch("gitlint.git.sh")
|
||||
@patch("arrow.now")
|
||||
def test_staged_commit(self, now, sh):
|
||||
# StagedLocalGitCommit()
|
||||
|
||||
sh.git.side_effect = [
|
||||
"#", # git config --get core.commentchar
|
||||
"test åuthor\n", # git config --get user.name
|
||||
"test-emåil@foo.com\n", # git config --get user.email
|
||||
"my-brånch\n", # git rev-parse --abbrev-ref HEAD
|
||||
"file1.txt\npåth/to/file2.txt\n",
|
||||
"#", # git config --get core.commentchar
|
||||
"test åuthor\n", # git config --get user.name
|
||||
"test-emåil@foo.com\n", # git config --get user.email
|
||||
"my-brånch\n", # git rev-parse --abbrev-ref HEAD
|
||||
"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")]
|
||||
|
||||
# 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")
|
||||
|
||||
# git calls we're expexting
|
||||
# git calls we're expecting
|
||||
expected_calls = [
|
||||
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.email', **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.email", **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]
|
||||
|
@ -513,13 +679,15 @@ class GitCommitTests(BaseTestCase):
|
|||
self.assertEqual(last_commit.author_email, "test-emåil@foo.com")
|
||||
self.assertListEqual(sh.git.mock_calls, expected_calls[0:3])
|
||||
|
||||
self.assertEqual(last_commit.date, datetime.datetime(2020, 2, 19, 12, 18, 46,
|
||||
tzinfo=dateutil.tz.tzoffset("+0100", 3600)))
|
||||
self.assertEqual(
|
||||
last_commit.date, datetime.datetime(2020, 2, 19, 12, 18, 46, tzinfo=dateutil.tz.tzoffset("+0100", 3600))
|
||||
)
|
||||
now.assert_called_once()
|
||||
|
||||
self.assertListEqual(last_commit.parents, [])
|
||||
self.assertFalse(last_commit.is_merge_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_revert_commit)
|
||||
|
||||
|
@ -527,42 +695,48 @@ class GitCommitTests(BaseTestCase):
|
|||
self.assertListEqual(sh.git.mock_calls, expected_calls[0:4])
|
||||
|
||||
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])
|
||||
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.git.sh")
|
||||
def test_staged_commit_with_missing_username(self, sh):
|
||||
# StagedLocalGitCommit()
|
||||
|
||||
sh.git.side_effect = [
|
||||
"#", # git config --get core.commentchar
|
||||
ErrorReturnCode('git config --get user.name', b"", b""),
|
||||
"#", # git config --get core.commentchar
|
||||
ErrorReturnCode("git config --get user.name", b"", b""),
|
||||
]
|
||||
|
||||
expected_msg = "Missing git configuration: please set user.name"
|
||||
with self.assertRaisesMessage(GitContextError, expected_msg):
|
||||
ctx = GitContext.from_staged_commit("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):
|
||||
# StagedLocalGitCommit()
|
||||
|
||||
sh.git.side_effect = [
|
||||
"#", # git config --get core.commentchar
|
||||
"test åuthor\n", # git config --get user.name
|
||||
ErrorReturnCode('git config --get user.name', b"", b""),
|
||||
"#", # git config --get core.commentchar
|
||||
ErrorReturnCode("git config --get user.email", b"", b""),
|
||||
]
|
||||
|
||||
expected_msg = "Missing git configuration: please set user.email"
|
||||
with self.assertRaisesMessage(GitContextError, expected_msg):
|
||||
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):
|
||||
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})
|
||||
|
||||
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")
|
||||
def test_gitcommit_equality(self, git):
|
||||
# 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()
|
||||
context1 = GitContext()
|
||||
commit_message1 = GitCommitMessage(context1, "tëst\n\nfoo", "tëst\n\nfoo", "tēst", ["", "föo"])
|
||||
commit1 = GitCommit(context1, commit_message1, "shä", now, "Jöhn Smith", "jöhn.smith@test.com", None,
|
||||
["föo/bar"], ["brånch1", "brånch2"])
|
||||
commit1 = GitCommit(
|
||||
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]
|
||||
|
||||
context2 = GitContext()
|
||||
commit_message2 = GitCommitMessage(context2, "tëst\n\nfoo", "tëst\n\nfoo", "tēst", ["", "föo"])
|
||||
commit2 = GitCommit(context2, commit_message1, "shä", now, "Jöhn Smith", "jöhn.smith@test.com", None,
|
||||
["föo/bar"], ["brånch1", "brånch2"])
|
||||
commit2 = GitCommit(
|
||||
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]
|
||||
|
||||
self.assertEqual(context1, context2)
|
||||
|
@ -588,15 +780,29 @@ class GitCommitTests(BaseTestCase):
|
|||
self.assertEqual(commit1, commit2)
|
||||
|
||||
# Check that objects are unequal when changing a single attribute
|
||||
kwargs = {'message': commit1.message, 'sha': commit1.sha, 'date': commit1.date,
|
||||
'author_name': commit1.author_name, 'author_email': commit1.author_email, 'parents': commit1.parents,
|
||||
'changed_files': commit1.changed_files, 'branches': commit1.branches}
|
||||
kwargs = {
|
||||
"message": commit1.message,
|
||||
"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
|
||||
special_messages = {'is_merge_commit': "Merge: foöbar", 'is_fixup_commit': "fixup! foöbar",
|
||||
'is_squash_commit': "squash! foöbar", 'is_revert_commit': "Revert: foöbar"}
|
||||
special_messages = {
|
||||
"is_merge_commit": "Merge: foöbar",
|
||||
"is_fixup_commit": "fixup! foöbar",
|
||||
"is_squash_commit": "squash! foöbar",
|
||||
"is_revert_commit": "Revert: foöbar",
|
||||
}
|
||||
for key in special_messages:
|
||||
kwargs_copy = copy.deepcopy(kwargs)
|
||||
clone1 = GitCommit(context=commit1.context, **kwargs_copy)
|
||||
|
@ -607,6 +813,10 @@ class GitCommitTests(BaseTestCase):
|
|||
clone2.message = GitCommitMessage.from_full_message(context1, "foöbar")
|
||||
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")
|
||||
def test_commit_msg_custom_commentchar(self, patched):
|
||||
patched.return_value = "ä"
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from unittest.mock import patch, call
|
||||
|
||||
from gitlint.tests.base import BaseTestCase
|
||||
|
@ -7,24 +5,16 @@ from gitlint.git import GitContext
|
|||
|
||||
|
||||
class GitContextTests(BaseTestCase):
|
||||
|
||||
# Expected special_args passed to 'sh'
|
||||
expected_sh_special_args = {
|
||||
'_tty_out': False,
|
||||
'_cwd': "fåke/path"
|
||||
}
|
||||
expected_sh_special_args = {"_tty_out": False, "_cwd": "fåke/path"}
|
||||
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.git.sh")
|
||||
def test_gitcontext(self, sh):
|
||||
|
||||
sh.git.side_effect = [
|
||||
"#", # git config --get core.commentchar
|
||||
"\nfoöbar\n"
|
||||
]
|
||||
sh.git.side_effect = ["#", "\nfoöbar\n"] # git config --get core.commentchar
|
||||
|
||||
expected_calls = [
|
||||
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")
|
||||
|
@ -38,12 +28,11 @@ class GitContextTests(BaseTestCase):
|
|||
self.assertEqual(context.current_branch, "foöbar")
|
||||
self.assertEqual(sh.git.mock_calls, expected_calls)
|
||||
|
||||
@patch('gitlint.git.sh')
|
||||
@patch("gitlint.git.sh")
|
||||
def test_gitcontext_equality(self, sh):
|
||||
|
||||
sh.git.side_effect = [
|
||||
"û\n", # context1: git config --get core.commentchar
|
||||
"û\n", # context2: git config --get core.commentchar
|
||||
"û\n", # context1: git config --get core.commentchar
|
||||
"û\n", # context2: git config --get core.commentchar
|
||||
"my-brånch\n", # context1: git rev-parse --abbrev-ref HEAD
|
||||
"my-brånch\n", # context2: git rev-parse --abbrev-ref HEAD
|
||||
]
|
||||
|
@ -68,17 +57,17 @@ class GitContextTests(BaseTestCase):
|
|||
# Different comment_char
|
||||
context3 = GitContext("fåke/path")
|
||||
context3.commits = ["fōo", "bår"]
|
||||
sh.git.side_effect = ([
|
||||
"ç\n", # context3: git config --get core.commentchar
|
||||
"my-brånch\n" # context3: git rev-parse --abbrev-ref HEAD
|
||||
])
|
||||
sh.git.side_effect = [
|
||||
"ç\n", # context3: git config --get core.commentchar
|
||||
"my-brånch\n", # context3: git rev-parse --abbrev-ref HEAD
|
||||
]
|
||||
self.assertNotEqual(context1, context3)
|
||||
|
||||
# Different current_branch
|
||||
context4 = GitContext("fåke/path")
|
||||
context4.commits = ["fōo", "bår"]
|
||||
sh.git.side_effect = ([
|
||||
"û\n", # context4: git config --get core.commentchar
|
||||
"different-brånch\n" # context4: git rev-parse --abbrev-ref HEAD
|
||||
])
|
||||
sh.git.side_effect = [
|
||||
"û\n", # context4: git config --get core.commentchar
|
||||
"different-brånch\n", # context4: git rev-parse --abbrev-ref HEAD
|
||||
]
|
||||
self.assertNotEqual(context1, context4)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from gitlint.tests.base import BaseTestCase
|
||||
from gitlint import rules
|
||||
|
||||
|
@ -17,7 +16,7 @@ class BodyRuleTests(BaseTestCase):
|
|||
self.assertListEqual(violations, [expected_violation])
|
||||
|
||||
# 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)
|
||||
self.assertIsNone(violations)
|
||||
|
||||
|
@ -100,14 +99,14 @@ class BodyRuleTests(BaseTestCase):
|
|||
# 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)
|
||||
|
||||
rule = rules.BodyMinLength({'min-length': 120})
|
||||
commit = self.gitcommit("Title\n\n{0}\n".format("å" * 21)) # pylint: disable=consider-using-f-string
|
||||
rule = rules.BodyMinLength({"min-length": 120})
|
||||
commit = self.gitcommit("Title\n\n{}\n".format("å" * 21)) # pylint: disable=consider-using-f-string
|
||||
violations = rule.validate(commit)
|
||||
self.assertListEqual(violations, [expected_violation])
|
||||
|
||||
# Make sure we don't get the error if the body-length is exactly the min-length
|
||||
rule = rules.BodyMinLength({'min-length': 8})
|
||||
commit = self.gitcommit("Tïtle\n\n{0}\n".format("å" * 8)) # pylint: disable=consider-using-f-string
|
||||
rule = rules.BodyMinLength({"min-length": 8})
|
||||
commit = self.gitcommit("Tïtle\n\n{}\n".format("å" * 8)) # pylint: disable=consider-using-f-string
|
||||
violations = rule.validate(commit)
|
||||
self.assertIsNone(violations)
|
||||
|
||||
|
@ -145,7 +144,7 @@ class BodyRuleTests(BaseTestCase):
|
|||
self.assertIsNone(violations)
|
||||
|
||||
# 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)
|
||||
expected_violation = rules.RuleViolation("B6", "Body message is missing", None, 3)
|
||||
self.assertListEqual(violations, [expected_violation])
|
||||
|
@ -159,7 +158,7 @@ class BodyRuleTests(BaseTestCase):
|
|||
self.assertIsNone(violations)
|
||||
|
||||
# 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")
|
||||
violations = rule.validate(commit)
|
||||
self.assertIsNone(violations)
|
||||
|
@ -201,29 +200,29 @@ class BodyRuleTests(BaseTestCase):
|
|||
|
||||
# assert no violation on matching regex
|
||||
# (also note that first body line - in between title and rest of body - is ignored)
|
||||
rule = rules.BodyRegexMatches({'regex': "^Bödy(.*)"})
|
||||
rule = rules.BodyRegexMatches({"regex": "^Bödy(.*)"})
|
||||
violations = rule.validate(commit)
|
||||
self.assertIsNone(violations)
|
||||
|
||||
# assert we can do end matching (and last empty line is ignored)
|
||||
# (also note that first body line - in between title and rest of body - is ignored)
|
||||
rule = rules.BodyRegexMatches({'regex': "My-Commit-Tag: föo$"})
|
||||
rule = rules.BodyRegexMatches({"regex": "My-Commit-Tag: föo$"})
|
||||
violations = rule.validate(commit)
|
||||
self.assertIsNone(violations)
|
||||
|
||||
# common use-case: matching that a given line is present
|
||||
rule = rules.BodyRegexMatches({'regex': "(.*)Föo(.*)"})
|
||||
rule = rules.BodyRegexMatches({"regex": "(.*)Föo(.*)"})
|
||||
violations = rule.validate(commit)
|
||||
self.assertIsNone(violations)
|
||||
|
||||
# assert violation on non-matching body
|
||||
rule = rules.BodyRegexMatches({'regex': "^Tëst(.*)Foo"})
|
||||
rule = rules.BodyRegexMatches({"regex": "^Tëst(.*)Foo"})
|
||||
violations = rule.validate(commit)
|
||||
expected_violation = rules.RuleViolation("B8", "Body does not match regex (^Tëst(.*)Foo)", None, 6)
|
||||
self.assertListEqual(violations, [expected_violation])
|
||||
|
||||
# assert no violation on None regex
|
||||
rule = rules.BodyRegexMatches({'regex': None})
|
||||
rule = rules.BodyRegexMatches({"regex": None})
|
||||
violations = rule.validate(commit)
|
||||
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"]
|
||||
for body in bodies:
|
||||
commit = self.gitcommit(body)
|
||||
rule = rules.BodyRegexMatches({'regex': ".*"})
|
||||
rule = rules.BodyRegexMatches({"regex": ".*"})
|
||||
violations = rule.validate(commit)
|
||||
self.assertIsNone(violations)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from gitlint.tests.base import BaseTestCase
|
||||
from gitlint.tests.base import BaseTestCase, EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING
|
||||
from gitlint import rules
|
||||
from gitlint.config import LintConfig
|
||||
|
||||
|
@ -22,20 +21,25 @@ class ConfigurationRuleTests(BaseTestCase):
|
|||
rule.apply(config, commit)
|
||||
self.assertEqual(config, expected_config)
|
||||
|
||||
expected_log_message = "DEBUG: gitlint.rules Ignoring commit because of rule 'I1': " + \
|
||||
"Commit title 'Releäse' matches the regex '^Releäse(.*)', ignoring rules: all"
|
||||
self.assert_log_contains(expected_log_message)
|
||||
expected_log_messages = [
|
||||
EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING.format("I1", "ignore-by-title"),
|
||||
"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
|
||||
rule = rules.IgnoreByTitle({"regex": "^Releäse(.*)",
|
||||
"ignore": "T1,B2"})
|
||||
rule = rules.IgnoreByTitle({"regex": "^Releäse(.*)", "ignore": "T1,B2"})
|
||||
expected_config = LintConfig()
|
||||
expected_config.ignore = "T1,B2"
|
||||
rule.apply(config, commit)
|
||||
self.assertEqual(config, expected_config)
|
||||
|
||||
expected_log_message = "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"
|
||||
]
|
||||
self.assert_logged(expected_log_messages)
|
||||
|
||||
def test_ignore_by_body(self):
|
||||
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)
|
||||
self.assertEqual(config, expected_config)
|
||||
|
||||
expected_log_message = "DEBUG: gitlint.rules Ignoring commit because of rule 'I2': " + \
|
||||
"Commit message line ' a relëase body' matches the regex '(.*)relëase(.*)'," + \
|
||||
" ignoring rules: all"
|
||||
self.assert_log_contains(expected_log_message)
|
||||
expected_log_messages = [
|
||||
EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING.format("I2", "ignore-by-body"),
|
||||
"DEBUG: gitlint.rules Ignoring commit because of rule 'I2': "
|
||||
"Commit message line ' a relëase body' matches the regex '(.*)relëase(.*)',"
|
||||
" ignoring rules: all",
|
||||
]
|
||||
self.assert_logged(expected_log_messages)
|
||||
|
||||
# Matching regex with specific ignore
|
||||
rule = rules.IgnoreByBody({"regex": "(.*)relëase(.*)",
|
||||
"ignore": "T1,B2"})
|
||||
rule = rules.IgnoreByBody({"regex": "(.*)relëase(.*)", "ignore": "T1,B2"})
|
||||
expected_config = LintConfig()
|
||||
expected_config.ignore = "T1,B2"
|
||||
rule.apply(config, commit)
|
||||
self.assertEqual(config, expected_config)
|
||||
|
||||
expected_log_message = "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"
|
||||
self.assert_log_contains(expected_log_message)
|
||||
]
|
||||
self.assert_logged(expected_log_messages)
|
||||
|
||||
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")
|
||||
|
@ -88,10 +96,13 @@ class ConfigurationRuleTests(BaseTestCase):
|
|||
rule.apply(config, commit)
|
||||
self.assertEqual(config, expected_config)
|
||||
|
||||
expected_log_message = ("DEBUG: gitlint.rules Ignoring commit because of rule 'I4': "
|
||||
"Commit Author Name 'Tëst nåme' matches the regex '(.*)ëst(.*)',"
|
||||
" ignoring rules: all")
|
||||
self.assert_log_contains(expected_log_message)
|
||||
expected_log_messages = [
|
||||
EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING.format("I4", "ignore-by-author-name"),
|
||||
"DEBUG: gitlint.rules Ignoring commit because of rule 'I4': "
|
||||
"Commit Author Name 'Tëst nåme' matches the regex '(.*)ëst(.*)',"
|
||||
" ignoring rules: all",
|
||||
]
|
||||
self.assert_logged(expected_log_messages)
|
||||
|
||||
# Matching regex with specific ignore
|
||||
rule = rules.IgnoreByAuthorName({"regex": "(.*)nåme", "ignore": "T1,B2"})
|
||||
|
@ -100,9 +111,11 @@ class ConfigurationRuleTests(BaseTestCase):
|
|||
rule.apply(config, commit)
|
||||
self.assertEqual(config, expected_config)
|
||||
|
||||
expected_log_message = ("DEBUG: gitlint.rules Ignoring commit because of rule 'I4': "
|
||||
"Commit Author Name 'Tëst nåme' matches the regex '(.*)nåme', ignoring rules: T1,B2")
|
||||
self.assert_log_contains(expected_log_message)
|
||||
expected_log_messages += [
|
||||
"DEBUG: gitlint.rules Ignoring commit because of rule 'I4': "
|
||||
"Commit Author Name 'Tëst nåme' matches the regex '(.*)nåme', ignoring rules: T1,B2"
|
||||
]
|
||||
self.assert_logged(expected_log_messages)
|
||||
|
||||
def test_ignore_body_lines(self):
|
||||
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
|
||||
self.assertEqual(commit1, expected_commit)
|
||||
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 " +
|
||||
"matches '(.*)relëase(.*)'")
|
||||
expected_log_messages = [
|
||||
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
|
||||
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
|
||||
from gitlint.tests.base import BaseTestCase, EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING
|
||||
from gitlint.rules import AuthorValidEmail, RuleViolation
|
||||
|
||||
|
||||
|
@ -8,8 +7,13 @@ class MetaRuleTests(BaseTestCase):
|
|||
rule = AuthorValidEmail()
|
||||
|
||||
# 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",
|
||||
"jöhn.doe@subdomain.bar.com"]
|
||||
valid_email_addresses = [
|
||||
"föo@bar.com",
|
||||
"Jöhn.Doe@bar.com",
|
||||
"jöhn+doe@bar.com",
|
||||
"jöhn/doe@bar.com",
|
||||
"jöhn.doe@subdomain.bar.com",
|
||||
]
|
||||
for email in valid_email_addresses:
|
||||
commit = self.gitcommit("", author_email=email)
|
||||
violations = rule.validate(commit)
|
||||
|
@ -22,19 +26,32 @@ class MetaRuleTests(BaseTestCase):
|
|||
self.assertIsNone(violations)
|
||||
|
||||
# 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",
|
||||
"JöhnDoe@ foo.com", "JöhnDoe@foo. com", "JöhnDoe@foo. com", "@bår.com",
|
||||
"föo@.com"]
|
||||
invalid_email_addresses = [
|
||||
"föo@bar",
|
||||
"JöhnDoe",
|
||||
"Jöhn Doe",
|
||||
"Jöhn Doe@foo.com",
|
||||
" JöhnDoe@foo.com",
|
||||
"JöhnDoe@ foo.com",
|
||||
"JöhnDoe@foo. com",
|
||||
"JöhnDoe@foo. com",
|
||||
"@bår.com",
|
||||
"föo@.com",
|
||||
]
|
||||
for email in invalid_email_addresses:
|
||||
commit = self.gitcommit("", author_email=email)
|
||||
violations = rule.validate(commit)
|
||||
self.assertListEqual(violations,
|
||||
[RuleViolation("M1", "Author email for commit is invalid", email)])
|
||||
self.assertListEqual(violations, [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):
|
||||
# regex=None -> the rule isn't applied
|
||||
rule = AuthorValidEmail()
|
||||
rule.options['regex'].set(None)
|
||||
rule.options["regex"].set(None)
|
||||
emailadresses = ["föo", None, "hür dür"]
|
||||
for email in emailadresses:
|
||||
commit = self.gitcommit("", author_email=email)
|
||||
|
@ -42,9 +59,8 @@ class MetaRuleTests(BaseTestCase):
|
|||
self.assertIsNone(violations)
|
||||
|
||||
# Custom domain
|
||||
rule = AuthorValidEmail({'regex': "[^@]+@bår.com"})
|
||||
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"]
|
||||
rule = AuthorValidEmail({"regex": "[^@]+@bår.com"})
|
||||
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"]
|
||||
for email in valid_email_addresses:
|
||||
commit = self.gitcommit("", author_email=email)
|
||||
violations = rule.validate(commit)
|
||||
|
@ -55,5 +71,7 @@ class MetaRuleTests(BaseTestCase):
|
|||
for email in invalid_email_addresses:
|
||||
commit = self.gitcommit("", author_email=email)
|
||||
violations = rule.validate(commit)
|
||||
self.assertListEqual(violations,
|
||||
[RuleViolation("M1", "Author email for commit is invalid", email)])
|
||||
self.assertListEqual(violations, [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.rules import Rule, RuleViolation
|
||||
|
||||
|
||||
class RuleTests(BaseTestCase):
|
||||
|
||||
def test_rule_equality(self):
|
||||
self.assertEqual(Rule(), Rule())
|
||||
# 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.rules import TitleMaxLength, TitleTrailingWhitespace, TitleHardTab, TitleMustNotContainWord, \
|
||||
TitleTrailingPunctuation, TitleLeadingWhitespace, TitleRegexMatches, RuleViolation, TitleMinLength
|
||||
from gitlint.rules import (
|
||||
TitleMaxLength,
|
||||
TitleTrailingWhitespace,
|
||||
TitleHardTab,
|
||||
TitleMustNotContainWord,
|
||||
TitleTrailingPunctuation,
|
||||
TitleLeadingWhitespace,
|
||||
TitleRegexMatches,
|
||||
RuleViolation,
|
||||
TitleMinLength,
|
||||
)
|
||||
|
||||
|
||||
class TitleRuleTests(BaseTestCase):
|
||||
|
@ -18,7 +26,7 @@ class TitleRuleTests(BaseTestCase):
|
|||
self.assertListEqual(violations, [expected_violation])
|
||||
|
||||
# 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)
|
||||
self.assertIsNone(violations)
|
||||
|
||||
|
@ -85,31 +93,37 @@ class TitleRuleTests(BaseTestCase):
|
|||
|
||||
# match literally
|
||||
violations = rule.validate("WIP This is å test", None)
|
||||
expected_violation = RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
||||
"WIP This is å test")
|
||||
expected_violation = RuleViolation(
|
||||
"T5", "Title contains the word 'WIP' (case-insensitive)", "WIP This is å test"
|
||||
)
|
||||
self.assertListEqual(violations, [expected_violation])
|
||||
|
||||
# match case insensitive
|
||||
violations = rule.validate("wip This is å test", None)
|
||||
expected_violation = RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
||||
"wip This is å test")
|
||||
expected_violation = RuleViolation(
|
||||
"T5", "Title contains the word 'WIP' (case-insensitive)", "wip This is å test"
|
||||
)
|
||||
self.assertListEqual(violations, [expected_violation])
|
||||
|
||||
# match if there is a colon after the word
|
||||
violations = rule.validate("WIP:This is å test", None)
|
||||
expected_violation = RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
||||
"WIP:This is å test")
|
||||
expected_violation = RuleViolation(
|
||||
"T5", "Title contains the word 'WIP' (case-insensitive)", "WIP:This is å test"
|
||||
)
|
||||
self.assertListEqual(violations, [expected_violation])
|
||||
|
||||
# match multiple words
|
||||
rule = TitleMustNotContainWord({'words': "wip,test,å"})
|
||||
rule = TitleMustNotContainWord({"words": "wip,test,å"})
|
||||
violations = rule.validate("WIP:This is å test", None)
|
||||
expected_violation = RuleViolation("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_violation3 = RuleViolation("T5", "Title contains the word 'å' (case-insensitive)",
|
||||
"WIP:This is å test")
|
||||
expected_violation = RuleViolation(
|
||||
"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_violation3 = RuleViolation(
|
||||
"T5", "Title contains the word 'å' (case-insensitive)", "WIP:This is å test"
|
||||
)
|
||||
self.assertListEqual(violations, [expected_violation, expected_violation2, expected_violation3])
|
||||
|
||||
def test_leading_whitespace(self):
|
||||
|
@ -143,12 +157,12 @@ class TitleRuleTests(BaseTestCase):
|
|||
self.assertIsNone(violations)
|
||||
|
||||
# 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)
|
||||
self.assertIsNone(violations)
|
||||
|
||||
# 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)
|
||||
expected_violation = RuleViolation("T7", "Title does not match regex (^UÅ[0-9]*)", "US1234: åbc")
|
||||
self.assertListEqual(violations, [expected_violation])
|
||||
|
@ -166,12 +180,12 @@ class TitleRuleTests(BaseTestCase):
|
|||
self.assertListEqual(violations, [expected_violation])
|
||||
|
||||
# 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)
|
||||
self.assertIsNone(violations)
|
||||
|
||||
# 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)
|
||||
self.assertIsNone(violations)
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
@ -33,7 +31,7 @@ class UserRuleTests(BaseTestCase):
|
|||
# Do some basic asserts on our user rule
|
||||
self.assertEqual(classes[0].id, "UC1")
|
||||
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.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)])
|
||||
|
||||
# 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)")
|
||||
self.assertListEqual(violations, [rules.RuleViolation("UC1", "Commit violåtion 1", "Contënt 1", 1),
|
||||
rules.RuleViolation("UC1", "Commit violåtion 2", "Contënt 2", 2)])
|
||||
self.assertListEqual(
|
||||
violations,
|
||||
[
|
||||
rules.RuleViolation("UC1", "Commit violåtion 1", "Contënt 1", 1),
|
||||
rules.RuleViolation("UC1", "Commit violåtion 2", "Contënt 2", 2),
|
||||
],
|
||||
)
|
||||
|
||||
def test_extra_path_specified_by_file(self):
|
||||
# Test that find_rule_classes can handle an extra path given as a file name instead of a directory
|
||||
|
@ -67,7 +70,7 @@ class UserRuleTests(BaseTestCase):
|
|||
classes = find_rule_classes(user_rule_path)
|
||||
|
||||
# convert classes to strings and sort them so we can compare them
|
||||
class_strings = sorted([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'>"]
|
||||
self.assertListEqual(class_strings, expected)
|
||||
|
||||
|
@ -96,23 +99,23 @@ class UserRuleTests(BaseTestCase):
|
|||
|
||||
def test_assert_valid_rule_class(self):
|
||||
class MyLineRuleClass(rules.LineRule):
|
||||
id = 'UC1'
|
||||
name = 'my-lïne-rule'
|
||||
id = "UC1"
|
||||
name = "my-lïne-rule"
|
||||
target = rules.CommitMessageTitle
|
||||
|
||||
def validate(self):
|
||||
pass
|
||||
|
||||
class MyCommitRuleClass(rules.CommitRule):
|
||||
id = 'UC2'
|
||||
name = 'my-cömmit-rule'
|
||||
id = "UC2"
|
||||
name = "my-cömmit-rule"
|
||||
|
||||
def validate(self):
|
||||
pass
|
||||
|
||||
class MyConfigurationRuleClass(rules.ConfigurationRule):
|
||||
id = 'UC3'
|
||||
name = 'my-cönfiguration-rule'
|
||||
id = "UC3"
|
||||
name = "my-cönfiguration-rule"
|
||||
|
||||
def apply(self):
|
||||
pass
|
||||
|
@ -125,8 +128,9 @@ class UserRuleTests(BaseTestCase):
|
|||
def test_assert_valid_rule_class_negative(self):
|
||||
# general test to make sure that incorrect rules will raise an exception
|
||||
user_rule_path = self.get_sample_path("user_rules/incorrect_linerule")
|
||||
with self.assertRaisesMessage(UserRuleError,
|
||||
"User-defined rule class 'MyUserLineRule' must have a 'validate' method"):
|
||||
with self.assertRaisesMessage(
|
||||
UserRuleError, "User-defined rule class 'MyUserLineRule' must have a 'validate' method"
|
||||
):
|
||||
find_rule_classes(user_rule_path)
|
||||
|
||||
def test_assert_valid_rule_class_negative_parent(self):
|
||||
|
@ -134,13 +138,14 @@ class UserRuleTests(BaseTestCase):
|
|||
class MyRuleClass:
|
||||
pass
|
||||
|
||||
expected_msg = "User-defined rule class 'MyRuleClass' must extend from gitlint.rules.LineRule, " + \
|
||||
"gitlint.rules.CommitRule or gitlint.rules.ConfigurationRule"
|
||||
expected_msg = (
|
||||
"User-defined rule class 'MyRuleClass' must extend from gitlint.rules.LineRule, "
|
||||
"gitlint.rules.CommitRule or gitlint.rules.ConfigurationRule"
|
||||
)
|
||||
with self.assertRaisesMessage(UserRuleError, expected_msg):
|
||||
assert_valid_rule_class(MyRuleClass)
|
||||
|
||||
def test_assert_valid_rule_class_negative_id(self):
|
||||
|
||||
for parent_class in [rules.LineRule, rules.CommitRule]:
|
||||
|
||||
class MyRuleClass(parent_class):
|
||||
|
@ -159,8 +164,9 @@ class UserRuleTests(BaseTestCase):
|
|||
# Rule ids must not start with one of the reserved id letters
|
||||
for letter in ["T", "R", "B", "M", "I"]:
|
||||
MyRuleClass.id = letter + "1"
|
||||
expected_msg = f"The id '{letter}' of 'MyRuleClass' is invalid. " + \
|
||||
"Gitlint reserves ids starting with R,T,B,M,I"
|
||||
expected_msg = (
|
||||
f"The id '{letter}' of 'MyRuleClass' is invalid. Gitlint reserves ids starting with R,T,B,M,I"
|
||||
)
|
||||
with self.assertRaisesMessage(UserRuleError, expected_msg):
|
||||
assert_valid_rule_class(MyRuleClass)
|
||||
|
||||
|
@ -181,7 +187,6 @@ class UserRuleTests(BaseTestCase):
|
|||
assert_valid_rule_class(MyRuleClass)
|
||||
|
||||
def test_assert_valid_rule_class_negative_option_spec(self):
|
||||
|
||||
for parent_class in [rules.LineRule, rules.CommitRule]:
|
||||
|
||||
class MyRuleClass(parent_class):
|
||||
|
@ -190,8 +195,10 @@ class UserRuleTests(BaseTestCase):
|
|||
|
||||
# if set, option_spec must be a list of gitlint options
|
||||
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"
|
||||
)
|
||||
with self.assertRaisesMessage(UserRuleError, expected_msg):
|
||||
assert_valid_rule_class(MyRuleClass)
|
||||
|
||||
|
@ -201,21 +208,23 @@ class UserRuleTests(BaseTestCase):
|
|||
assert_valid_rule_class(MyRuleClass)
|
||||
|
||||
def test_assert_valid_rule_class_negative_validate(self):
|
||||
|
||||
baseclasses = [rules.LineRule, rules.CommitRule]
|
||||
for clazz in baseclasses:
|
||||
|
||||
class MyRuleClass(clazz):
|
||||
id = "UC1"
|
||||
name = "my-rüle-class"
|
||||
|
||||
with self.assertRaisesMessage(UserRuleError,
|
||||
"User-defined rule class 'MyRuleClass' must have a 'validate' method"):
|
||||
with self.assertRaisesMessage(
|
||||
UserRuleError, "User-defined rule class 'MyRuleClass' must have a 'validate' method"
|
||||
):
|
||||
assert_valid_rule_class(MyRuleClass)
|
||||
|
||||
# validate attribute - not a method
|
||||
MyRuleClass.validate = "föo"
|
||||
with self.assertRaisesMessage(UserRuleError,
|
||||
"User-defined rule class 'MyRuleClass' must have a 'validate' method"):
|
||||
with self.assertRaisesMessage(
|
||||
UserRuleError, "User-defined rule class 'MyRuleClass' must have a 'validate' method"
|
||||
):
|
||||
assert_valid_rule_class(MyRuleClass)
|
||||
|
||||
def test_assert_valid_rule_class_negative_apply(self):
|
||||
|
@ -241,8 +250,10 @@ class UserRuleTests(BaseTestCase):
|
|||
pass
|
||||
|
||||
# no target
|
||||
expected_msg = "The target attribute of the user-defined LineRule class 'MyRuleClass' must be either " + \
|
||||
"gitlint.rules.CommitMessageTitle or gitlint.rules.CommitMessageBody"
|
||||
expected_msg = (
|
||||
"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):
|
||||
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
|
||||
class MyObject:
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from gitlint.rules import LineRule
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from gitlint.rules import CommitRule, RuleViolation
|
||||
from gitlint.options import IntOption
|
||||
|
||||
|
@ -7,11 +5,11 @@ from gitlint.options import IntOption
|
|||
class MyUserCommitRule(CommitRule):
|
||||
name = "my-üser-commit-rule"
|
||||
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):
|
||||
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))
|
||||
|
||||
return violations
|
||||
|
@ -19,6 +17,7 @@ class MyUserCommitRule(CommitRule):
|
|||
|
||||
# The below code is present so that we can test that we actually ignore it
|
||||
|
||||
|
||||
def func_should_be_ignored():
|
||||
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.
|
||||
|
||||
from gitlint.rules import CommitRule
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from gitlint.rules import CommitRule
|
||||
|
||||
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from gitlint.tests.base import BaseTestCase
|
||||
from gitlint.cache import PropertyCache, cache
|
||||
|
||||
|
||||
class CacheTests(BaseTestCase):
|
||||
|
||||
class MyClass(PropertyCache):
|
||||
""" Simple class that has cached properties, used for testing. """
|
||||
"""Simple class that has cached properties, used for testing."""
|
||||
|
||||
def __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 unittest.mock import patch # pylint: disable=no-name-in-module, import-error
|
||||
|
@ -14,9 +12,9 @@ class DisplayTests(BaseTestCase):
|
|||
display = Display(LintConfig())
|
||||
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
|
||||
with patch('gitlint.display.stdout', new=StringIO()) as stdout:
|
||||
with patch("gitlint.display.stdout", new=StringIO()) as stdout:
|
||||
display.v("tëst")
|
||||
display.vv("tëst2")
|
||||
# vvvv should be ignored regardless
|
||||
|
@ -25,7 +23,7 @@ class DisplayTests(BaseTestCase):
|
|||
self.assertEqual("tëst\ntëst2\n", stdout.getvalue())
|
||||
|
||||
# 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.vv("tëst2", exact=True)
|
||||
# vvvv should be ignored regardless
|
||||
|
@ -33,16 +31,16 @@ class DisplayTests(BaseTestCase):
|
|||
display.vvv("tëst3.2", exact=True)
|
||||
self.assertEqual("tëst2\n", stdout.getvalue())
|
||||
|
||||
# standard error should be empty throughtout all of this
|
||||
self.assertEqual('', stderr.getvalue())
|
||||
# standard error should be empty throughout all of this
|
||||
self.assertEqual("", stderr.getvalue())
|
||||
|
||||
def test_e(self):
|
||||
display = Display(LintConfig())
|
||||
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
|
||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
||||
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||
display.e("tëst")
|
||||
display.ee("tëst2")
|
||||
# vvvv should be ignored regardless
|
||||
|
@ -51,7 +49,7 @@ class DisplayTests(BaseTestCase):
|
|||
self.assertEqual("tëst\ntëst2\n", stderr.getvalue())
|
||||
|
||||
# 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.ee("tëst2", exact=True)
|
||||
# vvvv should be ignored regardless
|
||||
|
@ -59,5 +57,5 @@ class DisplayTests(BaseTestCase):
|
|||
display.eee("tëst3.2", exact=True)
|
||||
self.assertEqual("tëst2\n", stderr.getvalue())
|
||||
|
||||
# standard output should be empty throughtout all of this
|
||||
self.assertEqual('', stdout.getvalue())
|
||||
# standard output should be empty throughout all of this
|
||||
self.assertEqual("", stdout.getvalue())
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
|
||||
from unittest.mock import patch, ANY, mock_open
|
||||
|
||||
from gitlint.tests.base import BaseTestCase
|
||||
from gitlint.config import LintConfig
|
||||
from gitlint.hooks import GitHookInstaller, GitHookInstallerError, COMMIT_MSG_HOOK_SRC_PATH, COMMIT_MSG_HOOK_DST_PATH, \
|
||||
GITLINT_HOOK_IDENTIFIER
|
||||
from gitlint.hooks import (
|
||||
GitHookInstaller,
|
||||
GitHookInstallerError,
|
||||
COMMIT_MSG_HOOK_SRC_PATH,
|
||||
COMMIT_MSG_HOOK_DST_PATH,
|
||||
GITLINT_HOOK_IDENTIFIER,
|
||||
)
|
||||
|
||||
|
||||
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):
|
||||
git_hooks_dir.return_value = os.path.join("/föo", "bar")
|
||||
lint_config = LintConfig()
|
||||
|
@ -24,12 +26,12 @@ class HookTests(BaseTestCase):
|
|||
self.assertEqual(path, expected_path)
|
||||
|
||||
@staticmethod
|
||||
@patch('os.chmod')
|
||||
@patch('os.stat')
|
||||
@patch('gitlint.hooks.shutil.copy')
|
||||
@patch('os.path.exists', return_value=False)
|
||||
@patch('os.path.isdir', return_value=True)
|
||||
@patch('gitlint.hooks.git_hooks_dir')
|
||||
@patch("os.chmod")
|
||||
@patch("os.stat")
|
||||
@patch("gitlint.hooks.shutil.copy")
|
||||
@patch("os.path.exists", return_value=False)
|
||||
@patch("os.path.isdir", return_value=True)
|
||||
@patch("gitlint.hooks.git_hooks_dir")
|
||||
def test_install_commit_msg_hook(git_hooks_dir, isdir, path_exists, copy, stat, chmod):
|
||||
lint_config = LintConfig()
|
||||
lint_config.target = os.path.join("/hür", "dur")
|
||||
|
@ -43,10 +45,10 @@ class HookTests(BaseTestCase):
|
|||
chmod.assert_called_once_with(expected_dst, ANY)
|
||||
git_hooks_dir.assert_called_with(lint_config.target)
|
||||
|
||||
@patch('gitlint.hooks.shutil.copy')
|
||||
@patch('os.path.exists', return_value=False)
|
||||
@patch('os.path.isdir', return_value=True)
|
||||
@patch('gitlint.hooks.git_hooks_dir')
|
||||
@patch("gitlint.hooks.shutil.copy")
|
||||
@patch("os.path.exists", return_value=False)
|
||||
@patch("os.path.isdir", return_value=True)
|
||||
@patch("gitlint.hooks.git_hooks_dir")
|
||||
def test_install_commit_msg_hook_negative(self, git_hooks_dir, isdir, path_exists, copy):
|
||||
lint_config = LintConfig()
|
||||
lint_config.target = os.path.join("/hür", "dur")
|
||||
|
@ -64,22 +66,24 @@ class HookTests(BaseTestCase):
|
|||
isdir.return_value = True
|
||||
path_exists.return_value = True
|
||||
expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH)
|
||||
expected_msg = 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."
|
||||
expected_msg = (
|
||||
f"There is already a commit-msg hook file present in {expected_dst}.\n"
|
||||
"gitlint currently does not support appending to an existing commit-msg file."
|
||||
)
|
||||
with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
|
||||
GitHookInstaller.install_commit_msg_hook(lint_config)
|
||||
|
||||
@staticmethod
|
||||
@patch('os.remove')
|
||||
@patch('os.path.exists', return_value=True)
|
||||
@patch('os.path.isdir', return_value=True)
|
||||
@patch('gitlint.hooks.git_hooks_dir')
|
||||
@patch("os.remove")
|
||||
@patch("os.path.exists", return_value=True)
|
||||
@patch("os.path.isdir", return_value=True)
|
||||
@patch("gitlint.hooks.git_hooks_dir")
|
||||
def test_uninstall_commit_msg_hook(git_hooks_dir, isdir, path_exists, remove):
|
||||
lint_config = LintConfig()
|
||||
git_hooks_dir.return_value = os.path.join("/föo", "bar", ".git", "hooks")
|
||||
lint_config.target = os.path.join("/hür", "dur")
|
||||
read_data = "#!/bin/sh\n" + GITLINT_HOOK_IDENTIFIER
|
||||
with patch('gitlint.hooks.io.open', mock_open(read_data=read_data), create=True):
|
||||
with patch("builtins.open", mock_open(read_data=read_data), create=True):
|
||||
GitHookInstaller.uninstall_commit_msg_hook(lint_config)
|
||||
|
||||
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)
|
||||
git_hooks_dir.assert_called_with(lint_config.target)
|
||||
|
||||
@patch('os.remove')
|
||||
@patch('os.path.exists', return_value=True)
|
||||
@patch('os.path.isdir', return_value=True)
|
||||
@patch('gitlint.hooks.git_hooks_dir')
|
||||
@patch("os.remove")
|
||||
@patch("os.path.exists", return_value=True)
|
||||
@patch("os.path.isdir", return_value=True)
|
||||
@patch("gitlint.hooks.git_hooks_dir")
|
||||
def test_uninstall_commit_msg_hook_negative(self, git_hooks_dir, isdir, path_exists, remove):
|
||||
lint_config = LintConfig()
|
||||
lint_config.target = os.path.join("/hür", "dur")
|
||||
|
@ -122,10 +126,12 @@ class HookTests(BaseTestCase):
|
|||
path_exists.return_value = True
|
||||
read_data = "#!/bin/sh\nfoo"
|
||||
expected_dst = os.path.join(git_hooks_dir.return_value, COMMIT_MSG_HOOK_DST_PATH)
|
||||
expected_msg = f"The commit-msg hook in {expected_dst} was not installed by gitlint " + \
|
||||
"(or it was modified).\nUninstallation of 3th party or modified gitlint hooks " + \
|
||||
"is not supported."
|
||||
with patch('gitlint.hooks.io.open', mock_open(read_data=read_data), create=True):
|
||||
expected_msg = (
|
||||
f"The commit-msg hook in {expected_dst} was not installed by gitlint "
|
||||
"(or it was modified).\nUninstallation of 3th party or modified gitlint hooks "
|
||||
"is not supported."
|
||||
)
|
||||
with patch("builtins.open", mock_open(read_data=read_data), create=True):
|
||||
with self.assertRaisesMessage(GitHookInstallerError, expected_msg):
|
||||
GitHookInstaller.uninstall_commit_msg_hook(lint_config)
|
||||
remove.assert_not_called()
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from io import StringIO
|
||||
|
||||
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):
|
||||
|
||||
def test_lint_sample1(self):
|
||||
linter = GitLinter(LintConfig())
|
||||
gitcontext = self.gitcontext(self.get_sample("commit_message/sample1"))
|
||||
violations = linter.lint(gitcontext.commits[-1])
|
||||
expected_errors = [RuleViolation("T3", "Title has trailing punctuation (.)",
|
||||
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
|
||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
||||
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
|
||||
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
|
||||
RuleViolation("B1", "Line exceeds max length (135>80)",
|
||||
"This is the first line of the commit message body and it is meant to test " +
|
||||
"a line that exceeds the maximum line length of 80 characters.", 3),
|
||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a tråiling space. ", 4),
|
||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing tab.\t", 5),
|
||||
RuleViolation("B3", "Line contains hard tab characters (\\t)",
|
||||
"This line has a trailing tab.\t", 5)]
|
||||
# fmt: off
|
||||
expected_errors = [
|
||||
RuleViolation("T3", "Title has trailing punctuation (.)",
|
||||
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
|
||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
||||
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
|
||||
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
|
||||
RuleViolation("B1", "Line exceeds max length (135>80)",
|
||||
"This is the first line of the commit message body and it is meant to test " +
|
||||
"a line that exceeds the maximum line length of 80 characters.", 3),
|
||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a tråiling space. ", 4),
|
||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing tab.\t", 5),
|
||||
RuleViolation("B3", "Line contains hard tab characters (\\t)",
|
||||
"This line has a trailing tab.\t", 5)
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
self.assertListEqual(violations, expected_errors)
|
||||
|
||||
|
@ -35,9 +36,10 @@ class LintTests(BaseTestCase):
|
|||
linter = GitLinter(LintConfig())
|
||||
gitcontext = self.gitcontext(self.get_sample("commit_message/sample2"))
|
||||
violations = linter.lint(gitcontext.commits[-1])
|
||||
expected = [RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
||||
"Just a title contåining WIP", 1),
|
||||
RuleViolation("B6", "Body message is missing", None, 3)]
|
||||
expected = [
|
||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", "Just a title contåining WIP", 1),
|
||||
RuleViolation("B6", "Body message is missing", None, 3),
|
||||
]
|
||||
|
||||
self.assertListEqual(violations, expected)
|
||||
|
||||
|
@ -46,20 +48,24 @@ class LintTests(BaseTestCase):
|
|||
gitcontext = self.gitcontext(self.get_sample("commit_message/sample3"))
|
||||
violations = linter.lint(gitcontext.commits[-1])
|
||||
|
||||
# fmt: off
|
||||
title = " Commit title containing 'WIP', \tleading and tråiling whitespace and longer than 72 characters."
|
||||
expected = [RuleViolation("T1", "Title exceeds max length (95>72)", title, 1),
|
||||
RuleViolation("T3", "Title has trailing punctuation (.)", title, 1),
|
||||
RuleViolation("T4", "Title contains hard tab characters (\\t)", title, 1),
|
||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", title, 1),
|
||||
RuleViolation("T6", "Title has leading whitespace", title, 1),
|
||||
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
|
||||
RuleViolation("B1", "Line exceeds max length (101>80)",
|
||||
"This is the first line is meånt to test a line that exceeds the maximum line " +
|
||||
"length of 80 characters.", 3),
|
||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing space. ", 4),
|
||||
RuleViolation("B2", "Line has trailing whitespace", "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)]
|
||||
expected = [
|
||||
RuleViolation("T1", "Title exceeds max length (95>72)", title, 1),
|
||||
RuleViolation("T3", "Title has trailing punctuation (.)", title, 1),
|
||||
RuleViolation("T4", "Title contains hard tab characters (\\t)", title, 1),
|
||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", title, 1),
|
||||
RuleViolation("T6", "Title has leading whitespace", title, 1),
|
||||
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
|
||||
RuleViolation("B1", "Line exceeds max length (101>80)",
|
||||
"This is the first line is meånt to test a line that exceeds the maximum line " +
|
||||
"length of 80 characters.", 3),
|
||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing space. ", 4),
|
||||
RuleViolation("B2", "Line has trailing whitespace", "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)
|
||||
|
||||
|
@ -82,26 +88,28 @@ class LintTests(BaseTestCase):
|
|||
|
||||
title = " Commit title containing 'WIP', \tleading and tråiling whitespace and longer than 72 characters."
|
||||
# expect only certain violations because sample5 has a 'gitlint-ignore: T3, T6, body-max-line-length'
|
||||
expected = [RuleViolation("T1", "Title exceeds max length (95>72)", title, 1),
|
||||
RuleViolation("T4", "Title contains hard tab characters (\\t)", title, 1),
|
||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", title, 1),
|
||||
RuleViolation("B4", "Second line is not empty", "This line should be ëmpty", 2),
|
||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a tråiling space. ", 4),
|
||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing tab.\t", 5),
|
||||
RuleViolation("B3", "Line contains hard tab characters (\\t)",
|
||||
"This line has a trailing tab.\t", 5)]
|
||||
expected = [
|
||||
RuleViolation("T1", "Title exceeds max length (95>72)", title, 1),
|
||||
RuleViolation("T4", "Title contains hard tab characters (\\t)", title, 1),
|
||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", title, 1),
|
||||
RuleViolation("B4", "Second line is not empty", "This line should be ëmpty", 2),
|
||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a tråiling space. ", 4),
|
||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing tab.\t", 5),
|
||||
RuleViolation("B3", "Line contains hard tab characters (\\t)", "This line has a trailing tab.\t", 5),
|
||||
]
|
||||
self.assertListEqual(violations, expected)
|
||||
|
||||
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())
|
||||
gitcontext = self.gitcontext(self.get_sample("commit_message/sample2"))
|
||||
gitcontext.commits[0].author_email = "foo bår"
|
||||
violations = linter.lint(gitcontext.commits[-1])
|
||||
expected = [RuleViolation("M1", "Author email for commit is invalid", "foo bår", None),
|
||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
||||
"Just a title contåining WIP", 1),
|
||||
RuleViolation("B6", "Body message is missing", None, 3)]
|
||||
expected = [
|
||||
RuleViolation("M1", "Author email for commit is invalid", "foo bår", None),
|
||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", "Just a title contåining WIP", 1),
|
||||
RuleViolation("B6", "Body message is missing", None, 3),
|
||||
]
|
||||
|
||||
self.assertListEqual(violations, expected)
|
||||
|
||||
|
@ -111,9 +119,10 @@ class LintTests(BaseTestCase):
|
|||
linter = GitLinter(lint_config)
|
||||
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),
|
||||
RuleViolation("B3", "Line contains hard tab characters (\\t)",
|
||||
"This line has a tråiling tab.\t", 5)]
|
||||
expected = [
|
||||
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
|
||||
RuleViolation("B3", "Line contains hard tab characters (\\t)", "This line has a tråiling tab.\t", 5),
|
||||
]
|
||||
|
||||
self.assertListEqual(violations, expected)
|
||||
|
||||
|
@ -135,8 +144,9 @@ class LintTests(BaseTestCase):
|
|||
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
|
||||
expected = [RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
||||
"Just a title contåining WIP", 1)]
|
||||
expected = [
|
||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", "Just a title contåining WIP", 1)
|
||||
]
|
||||
|
||||
self.assertListEqual(violations, expected)
|
||||
|
||||
|
@ -145,22 +155,25 @@ class LintTests(BaseTestCase):
|
|||
linter = GitLinter(lint_config)
|
||||
lint_config.set_rule_option("I3", "regex", "(.*)tråiling(.*)")
|
||||
violations = linter.lint(self.gitcommit(self.get_sample("commit_message/sample1")))
|
||||
expected_errors = [RuleViolation("T3", "Title has trailing punctuation (.)",
|
||||
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
|
||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
||||
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
|
||||
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
|
||||
RuleViolation("B1", "Line exceeds max length (135>80)",
|
||||
"This is the first line of the commit message body and it is meant to test " +
|
||||
"a line that exceeds the maximum line length of 80 characters.", 3),
|
||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing tab.\t", 4),
|
||||
RuleViolation("B3", "Line contains hard tab characters (\\t)",
|
||||
"This line has a trailing tab.\t", 4)]
|
||||
|
||||
# fmt: off
|
||||
expected_errors = [
|
||||
RuleViolation("T3", "Title has trailing punctuation (.)",
|
||||
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
|
||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)",
|
||||
"Commit title contåining 'WIP', as well as trailing punctuation.", 1),
|
||||
RuleViolation("B4", "Second line is not empty", "This line should be empty", 2),
|
||||
RuleViolation("B1", "Line exceeds max length (135>80)",
|
||||
"This is the first line of the commit message body and it is meant to test " +
|
||||
"a line that exceeds the maximum line length of 80 characters.", 3),
|
||||
RuleViolation("B2", "Line has trailing whitespace", "This line has a trailing tab.\t", 4),
|
||||
RuleViolation("B3", "Line contains hard tab characters (\\t)",
|
||||
"This line has a trailing tab.\t", 4)
|
||||
]
|
||||
# fmt: on
|
||||
self.assertListEqual(violations, expected_errors)
|
||||
|
||||
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}"))
|
||||
lintconfig = LintConfig()
|
||||
linter = GitLinter(lintconfig)
|
||||
|
@ -176,7 +189,7 @@ class LintTests(BaseTestCase):
|
|||
self.assertTrue(len(violations) > 0)
|
||||
|
||||
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"))
|
||||
lintconfig = LintConfig()
|
||||
linter = GitLinter(lintconfig)
|
||||
|
@ -192,46 +205,52 @@ class LintTests(BaseTestCase):
|
|||
self.assertListEqual(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("body-match-regex", "regex", "Sügned-Off-By: (.*)$")
|
||||
expected_violations = [RuleViolation("T7", "Title does not match regex (^Tïtle)", "Normal Commit Tïtle", 1),
|
||||
RuleViolation("B8", "Body does not match regex (Sügned-Off-By: (.*)$)", None, 6)]
|
||||
expected_violations = [
|
||||
RuleViolation("T7", "Title does not match regex (^Tïtle)", "Normal Commit Tïtle", 1),
|
||||
RuleViolation("B8", "Body does not match regex (Sügned-Off-By: (.*)$)", None, 6),
|
||||
]
|
||||
violations = linter.lint(commit)
|
||||
self.assertListEqual(violations, expected_violations)
|
||||
|
||||
def test_print_violations(self):
|
||||
violations = [RuleViolation("RULE_ID_1", "Error Messåge 1", "Violating Content 1", None),
|
||||
RuleViolation("RULE_ID_2", "Error Message 2", "Violåting Content 2", 2)]
|
||||
violations = [
|
||||
RuleViolation("RULE_ID_1", "Error Messåge 1", "Violating Content 1", None),
|
||||
RuleViolation("RULE_ID_2", "Error Message 2", "Violåting Content 2", 2),
|
||||
]
|
||||
linter = GitLinter(LintConfig())
|
||||
|
||||
# test output with increasing verbosity
|
||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
||||
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||
linter.config.verbosity = 0
|
||||
linter.print_violations(violations)
|
||||
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.print_violations(violations)
|
||||
expected = "-: RULE_ID_1\n2: RULE_ID_2\n"
|
||||
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.print_violations(violations)
|
||||
expected = "-: RULE_ID_1 Error Messåge 1\n2: RULE_ID_2 Error Message 2\n"
|
||||
self.assertEqual(expected, stderr.getvalue())
|
||||
|
||||
with patch('gitlint.display.stderr', new=StringIO()) as stderr:
|
||||
with patch("gitlint.display.stderr", new=StringIO()) as stderr:
|
||||
linter.config.verbosity = 3
|
||||
linter.print_violations(violations)
|
||||
expected = "-: RULE_ID_1 Error Messåge 1: \"Violating Content 1\"\n" + \
|
||||
"2: RULE_ID_2 Error Message 2: \"Violåting Content 2\"\n"
|
||||
expected = (
|
||||
'-: RULE_ID_1 Error Messåge 1: "Violating Content 1"\n'
|
||||
+ '2: RULE_ID_2 Error Message 2: "Violåting Content 2"\n'
|
||||
)
|
||||
self.assertEqual(expected, stderr.getvalue())
|
||||
|
||||
def test_named_rules(self):
|
||||
""" Test that when named rules are present, both them and the original (non-named) rules executed """
|
||||
"""Test that when named rules are present, both them and the original (non-named) rules executed"""
|
||||
|
||||
lint_config = LintConfig()
|
||||
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"])
|
||||
linter = GitLinter(lint_config)
|
||||
|
||||
violations = [RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", "WIP: Föo bar", 1),
|
||||
RuleViolation("T5:another-rule-ïd", "Title contains the word 'Föo' (case-insensitive)",
|
||||
"WIP: Föo bar", 1),
|
||||
RuleViolation("T5:my-ïd", "Title contains the word 'Föo' (case-insensitive)",
|
||||
"WIP: Föo bar", 1)]
|
||||
violations = [
|
||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", "WIP: Föo bar", 1),
|
||||
RuleViolation("T5:another-rule-ïd", "Title contains the word 'Föo' (case-insensitive)", "WIP: Föo bar", 1),
|
||||
RuleViolation("T5:my-ïd", "Title contains the word 'Föo' (case-insensitive)", "WIP: Föo bar", 1),
|
||||
]
|
||||
self.assertListEqual(violations, linter.lint(self.gitcommit("WIP: Föo bar\n\nFoo bår hur dur bla bla")))
|
||||
|
||||
def test_ignore_named_rules(self):
|
||||
""" Test that named rules can be ignored """
|
||||
"""Test that named rules can be ignored"""
|
||||
|
||||
# Add named rule to lint config
|
||||
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")
|
||||
|
||||
# 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),
|
||||
RuleViolation("T5:my-ïd", "Title contains the word 'Föo' (case-insensitive)",
|
||||
"WIP: Föo bar", 1)]
|
||||
violations = [
|
||||
RuleViolation("T5", "Title contains the word 'WIP' (case-insensitive)", "WIP: Föo bar", 1),
|
||||
RuleViolation("T5:my-ïd", "Title contains the word 'Föo' (case-insensitive)", "WIP: Föo bar", 1),
|
||||
]
|
||||
self.assertListEqual(violations, linter.lint(commit))
|
||||
|
||||
# ignore regular rule: only named rule violations show up
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import re
|
||||
|
||||
|
@ -9,8 +8,14 @@ from gitlint.options import IntOption, BoolOption, StrOption, ListOption, PathOp
|
|||
|
||||
class RuleOptionTests(BaseTestCase):
|
||||
def test_option_equality(self):
|
||||
options = {IntOption: 123, StrOption: "foöbar", BoolOption: False, ListOption: ["a", "b"],
|
||||
PathOption: ".", RegexOption: "^foöbar(.*)"}
|
||||
options = {
|
||||
IntOption: 123,
|
||||
StrOption: "foöbar",
|
||||
BoolOption: False,
|
||||
ListOption: ["a", "b"],
|
||||
PathOption: ".",
|
||||
RegexOption: "^foöbar(.*)",
|
||||
}
|
||||
for clazz, val in options.items():
|
||||
# 2 options are equal if their name, value and description match
|
||||
option1 = clazz("test-öption", val, "Test Dëscription")
|
||||
|
@ -97,7 +102,7 @@ class RuleOptionTests(BaseTestCase):
|
|||
self.assertEqual(option.value, True)
|
||||
|
||||
# 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:
|
||||
with self.assertRaisesMessage(RuleOptionError, "Option 'tëst-name' must be either 'true' or 'false'"):
|
||||
option.set(value)
|
||||
|
@ -197,7 +202,7 @@ class RuleOptionTests(BaseTestCase):
|
|||
self.assertEqual(option.value, self.get_sample_path())
|
||||
|
||||
# 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')"
|
||||
with self.assertRaisesMessage(RuleOptionError, expected):
|
||||
option.set("haha")
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from gitlint import utils
|
||||
|
@ -7,13 +5,12 @@ from gitlint.tests.base import BaseTestCase
|
|||
|
||||
|
||||
class UtilsTests(BaseTestCase):
|
||||
|
||||
def tearDown(self):
|
||||
# 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
|
||||
utils.PLATFORM_IS_WINDOWS = utils.platform_is_windows()
|
||||
|
||||
@patch('os.environ')
|
||||
@patch("os.environ")
|
||||
def test_use_sh_library(self, patched_env):
|
||||
patched_env.get.return_value = "1"
|
||||
self.assertEqual(utils.use_sh_library(), True)
|
||||
|
@ -25,15 +22,11 @@ class UtilsTests(BaseTestCase):
|
|||
self.assertEqual(utils.use_sh_library(), False, invalid_val)
|
||||
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
|
||||
utils.PLATFORM_IS_WINDOWS = True
|
||||
# Assert that when GITLINT_USE_SH_LIB is not set, we fallback to False (not using)
|
||||
patched_env.get.return_value = None
|
||||
self.assertEqual(utils.use_sh_library(), False)
|
||||
|
||||
utils.PLATFORM_IS_WINDOWS = False
|
||||
self.assertEqual(utils.use_sh_library(), True)
|
||||
|
||||
@patch('gitlint.utils.locale')
|
||||
@patch("gitlint.utils.locale")
|
||||
def test_default_encoding_non_windows(self, mocked_locale):
|
||||
utils.PLATFORM_IS_WINDOWS = False
|
||||
mocked_locale.getpreferredencoding.return_value = "foöbar"
|
||||
|
@ -43,7 +36,7 @@ class UtilsTests(BaseTestCase):
|
|||
mocked_locale.getpreferredencoding.return_value = False
|
||||
self.assertEqual(utils.getpreferredencoding(), "UTF-8")
|
||||
|
||||
@patch('os.environ')
|
||||
@patch("os.environ")
|
||||
def test_default_encoding_windows(self, patched_env):
|
||||
utils.PLATFORM_IS_WINDOWS = True
|
||||
# Mock out os.environ
|
||||
|
|
|
@ -11,7 +11,7 @@ import locale
|
|||
# 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
|
||||
|
@ -31,10 +31,10 @@ PLATFORM_IS_WINDOWS = platform_is_windows()
|
|||
|
||||
|
||||
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:
|
||||
return gitlint_use_sh_lib_env == "1"
|
||||
return not PLATFORM_IS_WINDOWS
|
||||
return False
|
||||
|
||||
|
||||
USE_SH_LIB = use_sh_library()
|
||||
|
@ -44,8 +44,8 @@ USE_SH_LIB = use_sh_library()
|
|||
|
||||
|
||||
def getpreferredencoding():
|
||||
""" Modified version of local.getpreferredencoding() that takes into account LC_ALL, LC_CTYPE, LANG env vars
|
||||
on windows and falls back to UTF-8. """
|
||||
"""Modified version of local.getpreferredencoding() that takes into account LC_ALL, LC_CTYPE, LANG env vars
|
||||
on windows and falls back to UTF-8."""
|
||||
fallback_encoding = "UTF-8"
|
||||
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
|
||||
dot_index = encoding.find(".")
|
||||
if dot_index != -1:
|
||||
default_encoding = encoding[dot_index + 1:]
|
||||
default_encoding = encoding[dot_index + 1 :]
|
||||
else:
|
||||
default_encoding = encoding
|
||||
break
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
from __future__ import print_function
|
||||
from setuptools import setup, find_packages
|
||||
import io
|
||||
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
|
||||
def get_version(package):
|
||||
"""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)
|
||||
|
||||
|
||||
|
@ -50,38 +49,37 @@ setup(
|
|||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Software Development :: Quality Assurance",
|
||||
"Topic :: Software Development :: Testing",
|
||||
"License :: OSI Approved :: MIT License"
|
||||
"License :: OSI Approved :: MIT License",
|
||||
],
|
||||
python_requires=">=3.6",
|
||||
install_requires=[
|
||||
'Click>=8',
|
||||
'arrow>=1',
|
||||
"Click>=8",
|
||||
"arrow>=1",
|
||||
'sh>=1.13.0 ; sys_platform != "win32"',
|
||||
],
|
||||
extras_require={
|
||||
'trusted-deps': [
|
||||
'Click==8.0.3',
|
||||
'arrow==1.2.1',
|
||||
"trusted-deps": [
|
||||
"Click==8.0.3",
|
||||
"arrow==1.2.1",
|
||||
'sh==1.14.2 ; sys_platform != "win32"',
|
||||
],
|
||||
},
|
||||
keywords='gitlint git lint',
|
||||
author='Joris Roovers',
|
||||
url='https://jorisroovers.github.io/gitlint',
|
||||
keywords="gitlint git lint",
|
||||
author="Joris Roovers",
|
||||
url="https://jorisroovers.github.io/gitlint",
|
||||
project_urls={
|
||||
'Documentation': 'https://jorisroovers.github.io/gitlint',
|
||||
'Source': 'https://github.com/jorisroovers/gitlint',
|
||||
},
|
||||
license='MIT',
|
||||
package_data={
|
||||
'gitlint': ['files/*']
|
||||
"Documentation": "https://jorisroovers.github.io/gitlint",
|
||||
"Source": "https://github.com/jorisroovers/gitlint",
|
||||
},
|
||||
license="MIT",
|
||||
package_data={"gitlint": ["files/*"]},
|
||||
packages=find_packages(exclude=["examples"]),
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
|
@ -92,16 +90,20 @@ setup(
|
|||
|
||||
# Print a red deprecation warning for python < 3.6 users
|
||||
if sys.version_info[:2] < (3, 6):
|
||||
msg = "\033[31mDEPRECATION: You're using a python version that has reached end-of-life. " + \
|
||||
"Gitlint does not support Python < 3.6" + \
|
||||
"Please upgrade your Python to 3.6 or above.\033[0m"
|
||||
msg = (
|
||||
"\033[31mDEPRECATION: You're using a python version that has reached end-of-life. "
|
||||
+ "Gitlint does not support Python < 3.6"
|
||||
+ "Please upgrade your Python to 3.6 or above.\033[0m"
|
||||
)
|
||||
print(msg)
|
||||
|
||||
# Print a warning message for Windows users
|
||||
PLATFORM_IS_WINDOWS = "windows" in platform.system().lower()
|
||||
if PLATFORM_IS_WINDOWS:
|
||||
msg = "\n\n\n\n\n****************\n" + \
|
||||
"WARNING: Gitlint support for Windows is still experimental and there are some known issues: " + \
|
||||
"https://github.com/jorisroovers/gitlint/issues?q=is%3Aissue+is%3Aopen+label%3Awindows " + \
|
||||
"\n*******************"
|
||||
msg = (
|
||||
"\n\n\n\n\n****************\n"
|
||||
+ "WARNING: Gitlint support for Windows is still experimental and there are some known issues: "
|
||||
+ "https://github.com/jorisroovers/gitlint/issues?q=is%3Aissue+is%3Aopen+label%3Awindows "
|
||||
+ "\n*******************"
|
||||
)
|
||||
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=too-many-function-args,unexpected-keyword-arg
|
||||
|
||||
import io
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
|
@ -20,8 +18,8 @@ from qa.utils import DEFAULT_ENCODING
|
|||
|
||||
|
||||
class BaseTestCase(TestCase):
|
||||
""" Base class of which all gitlint integration test classes are derived.
|
||||
Provides a number of convenience methods. """
|
||||
"""Base class of which all gitlint integration test classes are derived.
|
||||
Provides a number of convenience methods."""
|
||||
|
||||
# In case of assert failures, print the full error message
|
||||
maxDiff = None
|
||||
|
@ -32,7 +30,7 @@ class BaseTestCase(TestCase):
|
|||
GITLINT_USAGE_ERROR = 253
|
||||
|
||||
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.tmp_git_repos = []
|
||||
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
|
||||
self.assertIsInstance(output, RunningCommand)
|
||||
output = output.stdout.decode(DEFAULT_ENCODING)
|
||||
output = output.replace('\r', '')
|
||||
output = output.replace("\r", "")
|
||||
self.assertMultiLineEqual(output, expected)
|
||||
|
||||
@staticmethod
|
||||
|
@ -56,11 +54,11 @@ class BaseTestCase(TestCase):
|
|||
return os.path.realpath(f"/tmp/gitlint-test-{timestamp}")
|
||||
|
||||
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()
|
||||
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
|
||||
git("config", "user.name", "gitlint-test-user", _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
|
||||
|
||||
@staticmethod
|
||||
def create_file(parent_dir):
|
||||
""" Creates a file inside a passed directory. Returns filename."""
|
||||
def create_file(parent_dir, content=None):
|
||||
"""Creates a file inside a passed directory. Returns filename."""
|
||||
test_filename = "test-fïle-" + str(uuid4())
|
||||
# pylint: disable=consider-using-with
|
||||
io.open(os.path.join(parent_dir, test_filename), 'a', encoding=DEFAULT_ENCODING).close()
|
||||
full_path = os.path.join(parent_dir, test_filename)
|
||||
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
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()
|
||||
if envvars:
|
||||
environment.update(envvars)
|
||||
return environment
|
||||
|
||||
def create_tmp_git_config(self, contents):
|
||||
""" Creates an environment with the GIT_CONFIG variable set to a file with the given contents. """
|
||||
"""Creates an environment with the GIT_CONFIG variable set to a file with the given contents."""
|
||||
tmp_config = self.create_tmpfile(contents)
|
||||
return self.create_environment({"GIT_CONFIG": tmp_config})
|
||||
|
||||
def create_simple_commit(self, message, out=None, ok_code=None, env=None, git_repo=None, tty_in=False):
|
||||
""" Creates a simple commit with an empty test file.
|
||||
:param message: Commit message for the commit. """
|
||||
def create_simple_commit(
|
||||
self, message, *, file_contents=None, out=None, ok_code=None, env=None, git_repo=None, tty_in=False
|
||||
):
|
||||
"""Creates a simple commit with an empty test file.
|
||||
:param message: Commit message for the commit."""
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
# https://amoffat.github.io/sh/#interactive-callbacks
|
||||
if not ok_code:
|
||||
ok_code = [0]
|
||||
|
||||
git("commit", "-m", message, _cwd=git_repo, _err_to_out=True, _out=out, _tty_in=tty_in,
|
||||
_ok_code=ok_code, _env=environment)
|
||||
git(
|
||||
"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
|
||||
|
||||
def create_tmpfile(self, content):
|
||||
""" 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
|
||||
"""Utility method to create temp files. These are cleaned at the end of the test"""
|
||||
# Not using a context manager to avoid unnecessary indentation in test code
|
||||
tmpfile, tmpfilepath = tempfile.mkstemp()
|
||||
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)
|
||||
|
||||
return tmpfilepath
|
||||
|
||||
@staticmethod
|
||||
|
@ -149,11 +177,11 @@ class BaseTestCase(TestCase):
|
|||
|
||||
@staticmethod
|
||||
def get_expected(filename="", variable_dict=None):
|
||||
""" Utility method to read an 'expected' file and return it as a string. Optionally replace template variables
|
||||
specified by variable_dict. """
|
||||
"""Utility method to read an 'expected' file and return it as a string. Optionally replace template variables
|
||||
specified by variable_dict."""
|
||||
expected_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "expected")
|
||||
expected_path = os.path.join(expected_dir, filename)
|
||||
with io.open(expected_path, encoding=DEFAULT_ENCODING) as file:
|
||||
with open(expected_path, encoding=DEFAULT_ENCODING) as file:
|
||||
expected = file.read()
|
||||
|
||||
if variable_dict:
|
||||
|
@ -162,20 +190,25 @@ class BaseTestCase(TestCase):
|
|||
|
||||
@staticmethod
|
||||
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_git_version = git("--version").strip()
|
||||
return {'platform': platform.platform(), 'python_version': sys.version,
|
||||
'git_version': expected_git_version, 'gitlint_version': expected_gitlint_version,
|
||||
'GITLINT_USE_SH_LIB': BaseTestCase.GITLINT_USE_SH_LIB, 'DEFAULT_ENCODING': DEFAULT_ENCODING}
|
||||
return {
|
||||
"platform": platform.platform(),
|
||||
"python_version": sys.version,
|
||||
"git_version": expected_git_version,
|
||||
"gitlint_version": expected_gitlint_version,
|
||||
"GITLINT_USE_SH_LIB": BaseTestCase.GITLINT_USE_SH_LIB,
|
||||
"DEFAULT_ENCODING": DEFAULT_ENCODING,
|
||||
}
|
||||
|
||||
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
|
||||
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 = 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.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
|
||||
|
|
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}:
|
||||
1: T3 Title has trailing punctuation (.): "Sïmple title4."
|
||||
|
||||
|
|
|
@ -14,11 +14,13 @@ 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: True
|
||||
fail-without-commits: False
|
||||
regex-style-search: False
|
||||
verbosity: 3
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -60,17 +62,17 @@ target: {target}
|
|||
B8: body-match-regex
|
||||
regex=None
|
||||
M1: author-valid-email
|
||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
||||
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||
|
||||
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
|
||||
DEBUG: gitlint.cli Using --msg-filename.
|
||||
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
||||
DEBUG: gitlint.cli Linting 1 commit(s)
|
||||
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
|
||||
DEBUG: gitlint.git ('diff', '--staged', '--numstat', '-r')
|
||||
DEBUG: gitlint.git ('config', '--get', 'user.name')
|
||||
DEBUG: gitlint.git ('config', '--get', 'user.email')
|
||||
DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
|
||||
DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r')
|
||||
DEBUG: gitlint.lint Commit Object
|
||||
--- Commit Message ----
|
||||
WIP: from fïle test.
|
||||
|
@ -79,10 +81,14 @@ Author: gitlint-test-user <gitlint@test.com>
|
|||
Date: {staged_date}
|
||||
is-merge-commit: False
|
||||
is-fixup-commit: False
|
||||
is-fixup-amend-commit: False
|
||||
is-squash-commit: False
|
||||
is-revert-commit: False
|
||||
Branches: ['master']
|
||||
Parents: []
|
||||
Branches: ['main']
|
||||
Changed Files: {changed_files}
|
||||
Changed Files Stats:
|
||||
{changed_files_stats}
|
||||
-----------------------
|
||||
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."
|
||||
|
|
|
@ -14,11 +14,13 @@ 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: True
|
||||
fail-without-commits: False
|
||||
regex-style-search: False
|
||||
verbosity: 3
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -60,7 +62,7 @@ target: {target}
|
|||
B8: body-match-regex
|
||||
regex=None
|
||||
M1: author-valid-email
|
||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
||||
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||
|
||||
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
|
||||
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.cli Linting 1 commit(s)
|
||||
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.email')
|
||||
DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
|
||||
DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r')
|
||||
DEBUG: gitlint.lint Commit Object
|
||||
--- Commit Message ----
|
||||
WIP: Pïpe test.
|
||||
|
@ -81,10 +83,14 @@ Author: gitlint-test-user <gitlint@test.com>
|
|||
Date: {staged_date}
|
||||
is-merge-commit: False
|
||||
is-fixup-commit: False
|
||||
is-fixup-amend-commit: False
|
||||
is-squash-commit: False
|
||||
is-revert-commit: False
|
||||
Branches: ['master']
|
||||
Parents: []
|
||||
Branches: ['main']
|
||||
Changed Files: {changed_files}
|
||||
Changed Files Stats:
|
||||
{changed_files_stats}
|
||||
-----------------------
|
||||
1: T3 Title has trailing punctuation (.): "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-merge-commits: True
|
||||
ignore-fixup-commits: True
|
||||
ignore-fixup-amend-commits: True
|
||||
ignore-squash-commits: True
|
||||
ignore-revert-commits: True
|
||||
ignore-stdin: True
|
||||
staged: False
|
||||
fail-without-commits: True
|
||||
regex-style-search: False
|
||||
verbosity: 2
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -60,7 +62,7 @@ target: {target}
|
|||
B8: body-match-regex
|
||||
regex=None
|
||||
M1: author-valid-email
|
||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
||||
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||
CC1: contrib-body-requires-signed-off-by
|
||||
CT1: contrib-title-conventional-commits
|
||||
types=fix,feat,chore,docs,style,refactor,perf,test,revert,ci,build
|
||||
|
@ -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 ('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.git ('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', '{commit_sha}')
|
||||
DEBUG: gitlint.lint Commit Object
|
||||
--- Commit Message ----
|
||||
WIP: Thïs is a title thåt is a bit longer.
|
||||
|
@ -84,10 +86,14 @@ 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
|
||||
Branches: ['master']
|
||||
Parents: []
|
||||
Branches: ['main']
|
||||
Changed Files: {changed_files}
|
||||
Changed Files Stats:
|
||||
{changed_files_stats}
|
||||
-----------------------
|
||||
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
|
||||
|
|
|
@ -14,11 +14,13 @@ 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: True
|
||||
fail-without-commits: False
|
||||
regex-style-search: False
|
||||
verbosity: 0
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -60,17 +62,17 @@ target: {target}
|
|||
B8: body-match-regex
|
||||
regex=None
|
||||
M1: author-valid-email
|
||||
regex=[^@ ]+@[^@ ]+\.[^@ ]+
|
||||
regex=^[^@ ]+@[^@ ]+\.[^@ ]+
|
||||
|
||||
DEBUG: gitlint.cli Fetching additional meta-data from staged commit
|
||||
DEBUG: gitlint.cli Using --msg-filename.
|
||||
DEBUG: gitlint.git ('config', '--get', 'core.commentchar')
|
||||
DEBUG: gitlint.cli Linting 1 commit(s)
|
||||
DEBUG: gitlint.lint Linting commit [SHA UNKNOWN]
|
||||
DEBUG: gitlint.git ('diff', '--staged', '--numstat', '-r')
|
||||
DEBUG: gitlint.git ('config', '--get', 'user.name')
|
||||
DEBUG: gitlint.git ('config', '--get', 'user.email')
|
||||
DEBUG: gitlint.git ('rev-parse', '--abbrev-ref', 'HEAD')
|
||||
DEBUG: gitlint.git ('diff', '--staged', '--name-only', '-r')
|
||||
DEBUG: gitlint.lint Commit Object
|
||||
--- Commit Message ----
|
||||
WIP: msg-fïlename test.
|
||||
|
@ -79,9 +81,12 @@ Author: gitlint-test-user <gitlint@test.com>
|
|||
Date: {date}
|
||||
is-merge-commit: False
|
||||
is-fixup-commit: False
|
||||
is-fixup-amend-commit: False
|
||||
is-squash-commit: False
|
||||
is-revert-commit: False
|
||||
Branches: ['master']
|
||||
Parents: []
|
||||
Branches: ['main']
|
||||
Changed Files: []
|
||||
Changed Files Stats: {{}}
|
||||
-----------------------
|
||||
DEBUG: gitlint.cli Exit Code = 3
|
||||
|
|
|
@ -14,11 +14,13 @@ contrib: []
|
|||
ignore: title-trailing-punctuation,B2
|
||||
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: 2
|
||||
debug: True
|
||||
target: {target}
|
||||
|
@ -60,7 +62,7 @@ target: {target}
|
|||
B8: body-match-regex
|
||||
regex=None
|
||||
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.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 ('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.git ('diff-tree', '--no-commit-id', '--name-only', '-r', '--root', '{commit_sha}')
|
||||
DEBUG: gitlint.lint Commit Object
|
||||
--- Commit Message ----
|
||||
WIP: Thïs is a title thåt is a bit longer.
|
||||
|
@ -81,10 +83,14 @@ 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
|
||||
Branches: ['master']
|
||||
Parents: []
|
||||
Branches: ['main']
|
||||
Changed Files: {changed_files}
|
||||
Changed Files Stats:
|
||||
{changed_files_stats}
|
||||
-----------------------
|
||||
1: T1 Title exceeds max length (42>20)
|
||||
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: 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"
|
||||
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: 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 '$'
|
||||
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: UC1 Body contains too many lines (2 > 1)
|
||||
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"
|
||||
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: UC1 GitContext.current_branch: master
|
||||
1: UC1 GitContext.current_branch: main
|
||||
1: UC1 GitContext.commentchar: #
|
||||
1: UC2 GitCommit.branches: ['master']
|
||||
1: UC2 GitCommit.branches: ['main']
|
||||
1: UC2 GitCommit.custom_prop: foöbar
|
||||
1: UC4 int-öption: 2
|
||||
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