diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2023-03-11 08:03:07 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2023-03-11 08:03:07 +0000 |
commit | dcc28a9a987457acf9e2c8249a9df5e40143eba3 (patch) | |
tree | a3b44db00ff34f0dee0406875e7320c4dce3041e | |
parent | Releasing debian version 0.19.0~dev-1. (diff) | |
download | gitlint-dcc28a9a987457acf9e2c8249a9df5e40143eba3.tar.xz gitlint-dcc28a9a987457acf9e2c8249a9df5e40143eba3.zip |
Merging upstream version 0.19.1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
111 files changed, 2060 insertions, 1678 deletions
diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 9da615c..0000000 --- a/.coveragerc +++ /dev/null @@ -1,6 +0,0 @@ -[report] -fail_under = 97 - -[run] -branch = true -omit=*dist-packages*,*site-packages*,gitlint-core/gitlint/tests/*,.venv/*,*virtualenv* diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5889037..65fcc1c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -22,7 +22,11 @@ "settings": { "python.defaultInterpreterPath": "/usr/local/bin/python", "python.linting.enabled": true, - "python.linting.pylintEnabled": true, + "python.formatting.provider": "black", + "python.formatting.blackArgs": [ + "--config", + "./pyproject.toml" + ], "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", @@ -31,12 +35,13 @@ "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" + "ms-python.vscode-pylance", + "charliermarsh.ruff", + "tamasfe.even-better-toml" ] } }, diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh index 4bbaf05..e2f0f76 100755 --- a/.devcontainer/postCreateCommand.sh +++ b/.devcontainer/postCreateCommand.sh @@ -1,6 +1,7 @@ #!/bin/sh -x brew install asdf +brew install hatch source "$(brew --prefix asdf)/libexec/asdf.sh" # Install latest python diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7c33438..a781b54 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,3 +12,7 @@ updates: directory: / schedule: interval: daily + - package-ecosystem: pip + directory: /gitlint-core + schedule: + interval: daily diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml deleted file mode 100644 index 39b3782..0000000 --- a/.github/workflows/checks.yml +++ /dev/null @@ -1,155 +0,0 @@ -name: Tests and Checks - -on: [push, pull_request] - -jobs: - checks: - runs-on: "ubuntu-latest" - strategy: - matrix: - 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@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. - # 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@v4.2.0 - with: - python-version: ${{ matrix.python-version }} - - - name: Install requirements - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r test-requirements.txt - - - name: Unit Tests - run: ./run_tests.sh - - # Coveralls integration doesn't properly work at this point, also see below - # - name: Coveralls - # env: - # 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 - - # 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: 1 - run: ./run_tests.sh -i - - - name: Code formatting (black) - run: ./run_tests.sh -f - - - name: PyLint - run: ./run_tests.sh -l - - - name: Build tests - run: ./run_tests.sh --build - - # Coveralls GH Action currently doesn't support current non-LCOV reporting format - # For now, still using Travis for unit test coverage reporting - # https://github.com/coverallsapp/github-action/issues/30 - # - name: Coveralls - # uses: coverallsapp/github-action@master - # with: - # github-token: ${{ secrets.GITHUB_TOKEN }} - - # Re-add git version control so we can run gitlint on itself. - - name: Re-add git version control to code - run: mv ._git .git - - # Run gitlint. Skip during PR runs, since PR commit messages are transient and usually full of gitlint violations. - # PRs get squashed and get a proper commit message during merge. - - name: Gitlint check - run: ./run_tests.sh -g --debug - if: ${{ github.event_name != 'pull_request' }} - - windows-checks: - runs-on: windows-latest - strategy: - matrix: - python-version: ["3.10"] - steps: - - 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. - # 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@v4.2.0 - with: - python-version: ${{ matrix.python-version }} - - - name: "Upgrade pip on Python 3" - if: matrix.python-version == '3.10' - run: python -m pip install --upgrade pip - - - name: Install requirements - run: | - pip install -r requirements.txt - pip install -r test-requirements.txt - - - name: gitlint --version - run: gitlint --version - - - name: Tests (sanity) - run: tools\windows\run_tests.bat "gitlint-core\gitlint\tests\cli\test_cli.py::CLITests::test_lint" - - - name: Tests (ignore cli\*) - run: pytest --ignore gitlint-core\gitlint\tests\cli -rw -s gitlint-core - - - name: Tests (test_cli.py only - continue-on-error:true) - run: tools\windows\run_tests.bat "gitlint-core\gitlint\tests\cli\test_cli.py" - continue-on-error: true # Known to fail at this point - - - name: Tests (all - continue-on-error:true) - run: tools\windows\run_tests.bat - continue-on-error: true # Known to fail at this point - - - name: Integration tests (continue-on-error:true) - run: pytest -rw -s qa - continue-on-error: true # Known to fail at this point - - - name: Code formatting (black) - run: black . - - - name: PyLint - run: pylint gitlint-core\gitlint qa --rcfile=".pylintrc" -r n - - # Re-add git version control so we can run gitlint on itself. - - name: Re-add git version control to code - run: Rename-Item ._git .git - - # Run gitlint. Skip during PR runs, since PR commit messages are transient and usually full of gitlint violations. - # PRs get squashed and get a proper commit message during merge. - - name: Gitlint check - run: gitlint --debug - if: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..403dcc4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,142 @@ +name: Tests and Checks + +# Only run CI on pushes to main and pull requests +# We don't run CI on other branches, but those should be merged into main via a PR anyways which will trigger CI before the merge. +on: + push: + branches: + - main + pull_request: + branches: + - main + +concurrency: + group: ci-${{ github.ref }}-1 + cancel-in-progress: true + +jobs: + checks: + runs-on: "ubuntu-latest" + strategy: + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", pypy-3.9] + os: ["macos-latest", "ubuntu-latest", "windows-latest"] + steps: + - uses: actions/checkout@v3.3.0 + with: + ref: ${{ github.event.pull_request.head.sha }} # Checkout pull request HEAD commit instead of merge commit + fetch-depth: 0 # checkout all history, needed for hatch versioning + + - name: Setup python + uses: actions/setup-python@v4.5.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Install pypa/build + run: python -m pip install build==0.10.0 + + - name: Install Hatch + run: python -m pip install hatch==1.6.3 + + - name: Unit Tests + run: hatch run test:unit-tests + + - name: Code formatting (black) + run: hatch run test:format + + - name: Code linting (ruff) + run: hatch run test:lint + + - name: Install local gitlint for integration tests + run: | + hatch run qa:install-local + + - name: Integration tests (default -> GITLINT_USE_SH_LIB=1) + run: | + hatch run qa:integration-tests + if: matrix.os != 'windows-latest' + + - name: Integration tests (GITLINT_USE_SH_LIB=1) + run: | + hatch run qa:integration-tests + env: + GITLINT_USE_SH_LIB: 1 + if: matrix.os != 'windows-latest' + + - name: Integration tests (GITLINT_QA_USE_SH_LIB=0) + run: | + hatch run qa:integration-tests -k "not(test_commit_hook_continue or test_commit_hook_abort or test_commit_hook_edit)" qa + env: + GITLINT_QA_USE_SH_LIB: 0 + if: matrix.os != 'windows-latest' + + - name: Integration tests (Windows) + run: | + hatch run qa:integration-tests -k "not (test_commit_hook_continue or test_commit_hook_abort or test_commit_hook_edit or test_lint_staged_stdin or test_stdin_file or test_stdin_pipe_empty)" qa + if: matrix.os == 'windows-latest' + + - name: Build test (gitlint) + run: | + python -m build + hatch clean + + - name: Build test (gitlint-core) + run: | + python -m build + hatch clean + working-directory: ./gitlint-core + + - name: Docs build (mkdocs) + run: hatch run docs:build + + # Run gitlint. Skip during PR runs, since PR commit messages are transient and usually full of gitlint violations. + # PRs get squashed and get a proper commit message during merge. + - name: gitlint --debug + run: hatch run dev:gitlint --debug + continue-on-error: ${{ github.event_name == 'pull_request' }} # Don't enforce gitlint in PRs + + - name: Code Coverage (coveralls) + uses: coverallsapp/github-action@master + with: + path-to-lcov: ".coverage.lcov" + github-token: ${{ secrets.GITHUB_TOKEN }} + git-commit: ${{ github.event.pull_request.head.sha }} + flag-name: gitlint-${{ matrix.os }}-${{ matrix.python-version }} + parallel: true + + upload_coveralls: + needs: checks + runs-on: ubuntu-latest + steps: + - name: Upload coverage to coveralls + uses: coverallsapp/github-action@master + with: + path-to-lcov: ".coverage.lcov" + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true + + check: # This job does nothing and is only used for the branch protection + if: always() # Ref: https://github.com/marketplace/actions/alls-green#why + + needs: + - upload_coveralls + - checks + + runs-on: ubuntu-latest + + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + + # When on main, auto publish dev build + auto-publish-dev: + needs: + - check + if: github.ref == 'refs/heads/main' + uses: ./.github/workflows/publish-release.yml + secrets: inherit # pass all secrets (required to access secrets in a called workflow) + with: + pypi_target: "pypi.org" + repo_release_ref: "main" diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml new file mode 100644 index 0000000..e5e40c9 --- /dev/null +++ b/.github/workflows/github-release.yml @@ -0,0 +1,14 @@ +name: Github Release Publish +run-name: "Github Release Publish (tag=${{github.ref_name}})" + +on: + release: + types: [published] + +jobs: + publish-release: + uses: ./.github/workflows/publish-release.yml + secrets: inherit # pass all secrets (required to access secrets in a called workflow) + with: + pypi_target: "pypi.org" + repo_release_ref: ${{ github.ref_name }} diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml new file mode 100644 index 0000000..092b6b3 --- /dev/null +++ b/.github/workflows/publish-docker.yml @@ -0,0 +1,79 @@ +name: Publish Docker +run-name: "Publish Docker (gitlint_version=${{ inputs.gitlint_version }})" + +on: + workflow_call: + inputs: + gitlint_version: + description: "Gitlint version to build docker image for" + required: true + type: string + docker_image_tag: + description: "Docker image tag" + required: true + type: string + push_to_dockerhub: + description: "Push to dockerhub.com" + required: false + type: boolean + default: false + workflow_dispatch: + inputs: + gitlint_version: + description: "Gitlint version to build docker image for" + type: string + docker_image_tag: + description: "Docker image tag" + required: true + type: choice + options: + - "latest_dev" + - "latest" + - "Use $gitlint_version" + default: "Use $gitlint_version" + push_to_dockerhub: + description: "Push to dockerhub.com" + required: false + type: boolean + default: false + +jobs: + publish_docker: + runs-on: "ubuntu-latest" + steps: + - name: Determine docker tag + id: set_tag + run: | + if [[ "${{ inputs.docker_image_tag }}" == "Use $gitlint_version" ]]; then + echo "docker_image_tag=${{ inputs.gitlint_version }}" >> $GITHUB_OUTPUT + else + echo "docker_image_tag=${{ inputs.docker_image_tag }}" >> $GITHUB_OUTPUT + fi + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: jorisroovers + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build docker image + uses: docker/build-push-action@v4 + with: + build-args: GITLINT_VERSION=${{ inputs.gitlint_version }} + tags: jorisroovers/gitlint:${{ steps.set_tag.outputs.docker_image_tag }} + + - name: Test docker image + run: | + gitlint_version=$(docker run --ulimit nofile=1024 -v $(pwd):/repo jorisroovers/gitlint:${{ steps.set_tag.outputs.docker_image_tag }} --version) + [ "$gitlint_version" == "gitlint, version ${{ inputs.gitlint_version }}" ] + + + # This won't actually rebuild the docker image, but just push the previously built and cached image + - name: Push docker image + uses: docker/build-push-action@v4 + with: + push: ${{ inputs.push_to_dockerhub }} + build-args: GITLINT_VERSION=${{ inputs.gitlint_version }} + tags: jorisroovers/gitlint:${{ steps.set_tag.outputs.docker_image_tag }} + if: inputs.push_to_dockerhub +
\ No newline at end of file diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..22ac4be --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,176 @@ +name: Publish Release +run-name: "Publish Release (pypi_target=${{ inputs.pypi_target }}, repo_release_ref=${{ inputs.repo_release_ref }})" + +on: + # Trigger release workflow from other workflows (e.g. release dev build as part of CI) + workflow_call: + inputs: + pypi_target: + description: "PyPI repository to publish to" + required: true + type: string + default: "test.pypi.org" + repo_release_ref: + description: "Gitlint git reference to publish release for" + type: string + default: "main" + + # Manually trigger a release + workflow_dispatch: + inputs: + pypi_target: + description: "PyPI repository to publish to" + required: true + type: choice + options: + - "pypi.org" + - "test.pypi.org" + default: "test.pypi.org" + repo_release_ref: + description: "Gitlint git reference to publish release for" + type: string + default: "main" + +jobs: + publish: + timeout-minutes: 15 + runs-on: "ubuntu-latest" + outputs: + gitlint_version: ${{ steps.set_version.outputs.gitlint_version }} + steps: + - name: Setup python + uses: actions/setup-python@v4.5.0 + with: + python-version: "3.11" + + - name: Install pypa/build + run: python -m pip install build==0.10.0 + + - name: Install Hatch + run: python -m pip install hatch==1.6.3 + + - uses: actions/checkout@v3.3.0 + with: + ref: ${{ inputs.repo_release_ref }} + fetch-depth: 0 # checkout all history, needed for hatch versioning + + # Run hatch version once to avoid additional output ("Setting up build environment for missing dependencies") + # during the next step + - name: Hatch version + run: hatch version + + # Hatch versioning is based on git (using hatch-vcs). If there is no explicit tag for the commit we're trying to + # publish, hatch versioning strings will have this format: 0.19.0.dev52+g9f7dc7d + # With the string after '+' being the 'g<short-sha>' of the commit. + # + # However, PyPI doesn't allow '+' in version numbers (no PEP440 local versions allowed on PyPI). + # To work around this, we override the version string by setting the SETUPTOOLS_SCM_PRETEND_VERSION env var + # to the version string without the '+' and everything after it. + # We do this by setting the `gitlint_version` step output here and re-using it later to + # set SETUPTOOLS_SCM_PRETEND_VERSION. + # + # We only actually publish such releases on the main branch to guarantee the dev numbering scheme remains + # unique. + # Note that when a tag *is* present (i.e. v0.19.0), hatch versioning will return the tag name (i.e. 0.19.0) + # and this step has no effect, ie. SETUPTOOLS_SCM_PRETEND_VERSION will be the same as `hatch version`. + - name: Set SETUPTOOLS_SCM_PRETEND_VERSION + id: set_version + run: | + echo "gitlint_version=$(hatch version | cut -d+ -f1)" >> $GITHUB_OUTPUT + + - name: Build (gitlint-core) + run: python -m build + working-directory: ./gitlint-core + env: + SETUPTOOLS_SCM_PRETEND_VERSION: ${{ steps.set_version.outputs.gitlint_version }} + + - name: Build (gitlint) + run: python -m build + env: + SETUPTOOLS_SCM_PRETEND_VERSION: ${{ steps.set_version.outputs.gitlint_version }} + + - name: Publish gitlint-core (pypi.org) + run: hatch publish + working-directory: ./gitlint-core + env: + HATCH_INDEX_USER: ${{ secrets.PYPI_GITLINT_CORE_USERNAME }} + HATCH_INDEX_AUTH: ${{ secrets.PYPI_GITLINT_CORE_PASSWORD }} + if: inputs.pypi_target == 'pypi.org' + + - name: Publish gitlint (pypi.org) + run: hatch publish + env: + HATCH_INDEX_USER: ${{ secrets.PYPI_GITLINT_USERNAME }} + HATCH_INDEX_AUTH: ${{ secrets.PYPI_GITLINT_PASSWORD }} + if: inputs.pypi_target == 'pypi.org' + + - name: Publish gitlint-core (test.pypi.org) + run: hatch publish -r test + working-directory: ./gitlint-core + env: + HATCH_INDEX_USER: ${{ secrets.TEST_PYPI_GITLINT_CORE_USERNAME }} + HATCH_INDEX_AUTH: ${{ secrets.TEST_PYPI_GITLINT_CORE_PASSWORD }} + if: inputs.pypi_target == 'test.pypi.org' + + - name: Publish gitlint (test.pypi.org) + run: hatch publish -r test + env: + HATCH_INDEX_USER: ${{ secrets.TEST_PYPI_GITLINT_USERNAME }} + HATCH_INDEX_AUTH: ${{ secrets.TEST_PYPI_GITLINT_PASSWORD }} + if: inputs.pypi_target == 'test.pypi.org' + + # Wait for gitlint package to be available in PyPI for installation + wait-for-package: + needs: + - publish + runs-on: "ubuntu-latest" + steps: + - name: Install gitlint + uses: nick-fields/retry@v2.8.3 + with: + timeout_minutes: 1 + max_attempts: 10 + command: | + python -m pip install gitlint==${{ needs.publish.outputs.gitlint_version }} + if: inputs.pypi_target == 'pypi.org' + + - name: Install gitlint (test.pypi.org) + uses: nick-fields/retry@v2.8.3 + with: + timeout_minutes: 1 + max_attempts: 10 + command: | + pip install --no-cache-dir -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple gitlint==${{ needs.publish.outputs.gitlint_version }} + if: inputs.pypi_target == 'test.pypi.org' + + - name: gitlint --version + run: | + gitlint --version + [ "$(gitlint --version)" == "gitlint, version ${{ needs.publish.outputs.gitlint_version }}" ] + + # Unfortunately, it's not because the newly published package installation worked once that replication + # has finished amongst all PyPI servers (subsequent installations might still fail). We sleep for 10 min here + # to increase the odds that replication has finished. + - name: Sleep + run: sleep 600 + + test-release: + needs: + - publish + - wait-for-package + uses: ./.github/workflows/test-release.yml + with: + gitlint_version: ${{ needs.publish.outputs.gitlint_version }} + pypi_source: ${{ inputs.pypi_target }} + repo_test_ref: ${{ inputs.repo_release_ref }} + + publish-docker: + needs: + - publish + - test-release + uses: ./.github/workflows/publish-docker.yml + secrets: inherit # pass all secrets (required to access secrets in a called workflow) + with: + gitlint_version: ${{ needs.publish.outputs.gitlint_version }} + docker_image_tag: "latest_dev" + push_to_dockerhub: true diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml new file mode 100644 index 0000000..caf00dd --- /dev/null +++ b/.github/workflows/test-release.yml @@ -0,0 +1,95 @@ +name: Test Release +run-name: "Test Release (${{ inputs.gitlint_version }}, pypi_source=${{ inputs.pypi_source }}, repo_test_ref=${{ inputs.repo_test_ref }})" +on: + workflow_call: + inputs: + gitlint_version: + description: "Gitlint version to test" + required: true + default: "0.18.0" + type: string + pypi_source: + description: "PyPI repository to use" + required: true + type: string + repo_test_ref: + description: "Git reference to checkout for integration tests" + default: "main" + type: string + workflow_dispatch: + inputs: + gitlint_version: + description: "Gitlint version to test" + required: true + default: "0.18.0" + pypi_source: + description: "PyPI repository to use" + required: true + type: choice + options: + - "pypi.org" + - "test.pypi.org" + default: "pypi.org" + repo_test_ref: + description: "Git reference to checkout for integration tests" + default: "main" + +jobs: + test-release: + timeout-minutes: 10 + runs-on: "ubuntu-latest" + strategy: + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", pypy-3.9] + os: ["macos-latest", "ubuntu-latest", "windows-latest"] + steps: + - name: Setup python + uses: actions/setup-python@v4.5.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Hatch + run: python -m pip install hatch==1.6.3 + + - name: Install gitlint + run: | + python -m pip install gitlint==${{ inputs.gitlint_version }} + if: inputs.pypi_source == 'pypi.org' + + - name: Install gitlint (test.pypi.org) + run: | + pip install --no-cache-dir -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple gitlint==${{ inputs.gitlint_version }} + if: inputs.pypi_source == 'test.pypi.org' + + - name: gitlint --version + run: | + gitlint --version + [ "$(gitlint --version)" == "gitlint, version ${{ inputs.gitlint_version }}" ] + + - uses: actions/checkout@v3.3.0 + with: + ref: ${{ inputs.repo_test_ref }} + + - name: Integration tests (default -> GITLINT_USE_SH_LIB=1) + run: | + hatch run qa:integration-tests + if: matrix.os != 'windows-latest' + + - name: Integration tests (GITLINT_USE_SH_LIB=1) + run: | + hatch run qa:integration-tests + env: + GITLINT_USE_SH_LIB: 1 + if: matrix.os != 'windows-latest' + + - name: Integration tests (GITLINT_QA_USE_SH_LIB=0) + run: | + hatch run qa:integration-tests -k "not(test_commit_hook_continue or test_commit_hook_abort or test_commit_hook_edit)" qa + env: + GITLINT_QA_USE_SH_LIB: 0 + if: matrix.os != 'windows-latest' + + - name: Integration tests (Windows) + run: | + hatch run qa:integration-tests -k "not (test_commit_hook_continue or test_commit_hook_abort or test_commit_hook_edit or test_lint_staged_stdin or test_stdin_file or test_stdin_pipe_empty)" qa + if: matrix.os == 'windows-latest' diff --git a/.gitlint b/.gitlint new file mode 100644 index 0000000..011b0a3 --- /dev/null +++ b/.gitlint @@ -0,0 +1,9 @@ +[general] +# See https://jorisroovers.com/gitlint/configuration/#regex-style-search +regex-style-search=True + +# Dependabot tends to generate lines that exceed the default 80 char limit. +[ignore-by-author-name] +regex=dependabot +ignore=all + diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index dc54455..0000000 --- a/.pylintrc +++ /dev/null @@ -1,48 +0,0 @@ -# The format of this file isn't really documented; just use --generate-rcfile -[MASTER] - -[Messages Control] -# C0111: Don't require docstrings on every method -# W0511: TODOs in code comments are fine. -# W0142: *args and **kwargs are fine. -# W0223: abstract methods don't need to be overwritten (i.e. when overwriting a Django REST serializer) -# W0622: Redefining id is fine. -# R0901: Too many ancestors (i.e. when subclassing test classes) -# R0801: Similar lines in files -# I0011: Informational: locally disabled pylint -# I0013: Informational: Ignoring entire file -disable=bad-option-value,C0111,W0511,W0142,W0622,W0223,W0212,R0901,R0801,I0011,I0013,anomalous-backslash-in-string,useless-object-inheritance,unnecessary-pass - -[Format] -max-line-length=120 - -[Basic] -# Variable names can be 1 to 31 characters long, with lowercase and underscores -variable-rgx=[a-z_][a-z0-9_]{0,30}$ - -# Argument names can be 2 to 31 characters long, with lowercase and underscores -argument-rgx=[a-z_][a-z0-9_]{1,30}$ - -# Method names should be at least 3 characters long -# and be lower-cased with underscores -method-rgx=([a-z_][a-z0-9_]{2,50}|setUp|tearDown)$ - -# Allow 'id' as variable name everywhere -good-names=id,c,_ - -bad-names=__author__ - -# Ignore all variables that start with an underscore (e.g. unused _request variable in a view) -dummy-variables-rgx=_ - -[Design] -max-public-methods=100 -min-public-methods=0 -# Maximum number of attributes of a class -max-attributes=15 -max-args=10 -max-locals=20 - -[Typecheck] -# Allow the use of the Django 'objects' members -generated-members=sh.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 091b2a4..f26f670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,28 +1,80 @@ -# Changelog # +# Changelog +This file documents notable changes introduced in gitlint releases. -## v0.18.0 (2022-11-16) ## +# v0.20.0 (Unreleased) + +# v0.19.1 (2023-03-10) + +## Development +- Fix issue that prevented homebrew packages from being built ([#460](https://github.com/jorisroovers/gitlint/issues/460)) +- Switch to using pypa/build in CI ([#463](https://github.com/jorisroovers/gitlint/issues/463)) - thanks @webknjaz + + +# v0.19.0 (2023-03-07) + +This release was primarily focussed on modernizing gitlint's build and test tooling (details: [#378](https://github.com/jorisroovers/gitlint/issues/378)). + +## General + - Python 3.6 no longer supported ([EOL since 2021-12-23](https://endoflife.date/python)) ([#379](https://github.com/jorisroovers/gitlint/issues/379)) + - This is the last release to support the [sh](https://amoffat.github.io/sh/) library (used under-the-hood to execute git commands) by setting `GITLINT_USE_SH_LIB=1`. This is already disabled by default since v0.18.0. + +## Features + - Allow for a single commit in the `--commits` cmd-line param ([#412](https://github.com/jorisroovers/gitlint/issues/412)) - thanks [carlescufi](https://github.com/carlescufi) + - Gitlint now separates `FILE_ENCODING` (always UTF-8) from `TERMINAL_ENCODING` (terminal dependent), this should improve issues with unicode. Use `gitlint --debug` to inspect these values. ([#424](https://github.com/jorisroovers/gitlint/issues/424)) + +## Bugfixes + - `ignore-by-author-name` crashes without --staged ([#445](https://github.com/jorisroovers/gitlint/issues/445)) + - Various documentation fixes ([#401](https://github.com/jorisroovers/gitlint/issues/401), [#433](https://github.com/jorisroovers/gitlint/issues/433)) - Thanks [scop](https://github.com/scop) + +## Development + - Adopted [hatch](https://hatch.pypa.io/latest/) for project management ([#384](https://github.com/jorisroovers/gitlint/issues/384)). + This significantly improves the developer workflow, please read the updated [CONTRIBUTING](https://jorisroovers.com/gitlint/contributing/) page. + - Adopted [ruff](https://github.com/charliermarsh/ruff) for linting, replacing pylint ([#404](https://github.com/jorisroovers/gitlint/issues/404)) + - Gitlint now publishes [dev builds on every commit to main](https://jorisroovers.github.io/gitlint/contributing/#dev-builds) ([#429](https://github.com/jorisroovers/gitlint/issues/429)) + - Gitlint now publishes a [`latest_dev` docker image](https://hub.docker.com/r/jorisroovers/gitlint/tags?name=latest_dev) on every commit to main ([#451](https://github.com/jorisroovers/gitlint/issues/452)) ([#452](https://github.com/jorisroovers/gitlint/issues/451)) + - Dependencies updated + - Many improvements to the [CI/CD worfklows](https://github.com/jorisroovers/gitlint/tree/main/.github/workflows) + - Fixed coveralls integration: [coveralls.io/github/jorisroovers/gitlint](https://coveralls.io/github/jorisroovers/gitlint) + - Improve unit test coverage ([#453](https://github.com/jorisroovers/gitlint/issues/453)) + - Integration test fixes on windows ([#392](https://github.com/jorisroovers/gitlint/issues/392), [#397](https://github.com/jorisroovers/gitlint/issues/397)) + - Devcontainer improvements ([#428](https://github.com/jorisroovers/gitlint/issues/428)) + - Removal of Dockerfile.dev ([#390](https://github.com/jorisroovers/gitlint/issues/390)) + - Fix most integration tests on Windows + - Fix Windows unit tests ([#383](https://github.com/jorisroovers/gitlint/issues/383)) + - Introduce a gate/check GHA job ([#375](https://github.com/jorisroovers/gitlint/issues/375)) - Thanks [webknjaz](https://github.com/webknjaz) + - Thanks to [sigmavirus24](https://github.com/sigmavirus24) for continued overall help and support + + +# v0.18.0 (2022-11-16) Contributors: Special thanks to all contributors for this release - details inline! +## General - 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)) + +## Features - `--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: + +## 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: + +## 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: + +## 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: + +## Development - Dependencies updated - Moved to [black](https://github.com/psf/black) for formatting - Fixed nasty CI issue ([#298](https://github.com/jorisroovers/gitlint/issues/298)) @@ -32,63 +84,75 @@ Special thanks to all contributors for this release - details inline! - Moved [roadmap and project planning](https://github.com/users/jorisroovers/projects/1) to github projects - Thanks to [sigmavirus24](https://github.com/sigmavirus24) for continued overall help and support -## v0.17.0 (2021-11-28) ## +# v0.17.0 (2021-11-28) Contributors: Special thanks to all contributors for this release, in particular [andersk](https://github.com/andersk) and [sigmavirus24](https://github.com/sigmavirus24). +## General - Gitlint is now split in 2 packages: `gitlint` and `gitlint-core`. This allows users to install gitlint without pinned dependencies (which is the default) ([#162](https://github.com/jorisroovers/gitlint/issues/162)) - Under-the-hood: dependencies updated -## v0.16.0 (2021-10-08) ## + +# v0.16.0 (2021-10-08) Contributors: Special thanks to all contributors for this release, in particular [sigmavirus24](https://github.com/sigmavirus24), [l0b0](https://github.com/l0b0) and [rafaelbubach](https://github.com/rafaelbubach). +## General - 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 +- 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. + +## Features - `--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: + +## Rules +- **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 + +## 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 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) ## + +## Development +- Dependencies updated +- Test and github action improvements + +# v0.15.1 (2021-04-16) Contributors: Special thanks to all contributors for this release, in particular [PW999](https://github.com/PW999), [gsemet](https://github.com/gsemet) and [Lorac](https://github.com/Lorac). -Bugfixes: +## Bugfixes - Git commit message body with only new lines is not longer considered empty by `body-is-missing` ([#176](https://github.com/jorisroovers/gitlint/issues/176)) - Added compatibility with `git commit -s` for `contrib-requires-signed-off-by` rule ([#178](https://github.com/jorisroovers/gitlint/pull/178)) - Minor tweak to gitlint commit-hook output ([#173](https://github.com/jorisroovers/gitlint/pull/173)) - All dependencies have been upgraded to the latest available versions (`Click==7.1.2`, `arrow==1.0.3`, `sh==1.14.1`). - Minor doc fixes -## v0.15.0 (2020-11-27) ## + +# v0.15.0 (2020-11-27) Contributors: Special thanks to [BrunIF](https://github.com/BrunIF), [lukech](https://github.com/lukech), [Cielquan](https://github.com/Cielquan), [harens](https://github.com/harens) and [sigmavirus24](https://github.com/sigmavirus24). -**This release drops support for Python 2.7 and Python 3.5 ([both are EOL](https://endoflife.date/python)). Other than a few minor fixes, there are no functional differences from the 0.14.0 release.** +## General -Other call-outs: +- **This release drops support for Python 2.7 and Python 3.5 ([both are EOL](https://endoflife.date/python)). Other than a few minor fixes, there are no functional differences from the 0.14.0 release.** - **Mac users**: Gitlint can now be installed using both homebrew (upgraded to latest) and macports. Special thanks to [@harens](https://github.com/harens) for maintaining these packages (best-effort). -- Bugfix: Gitlint now properly handles exceptions when using its built-in commit-msg hook ([#166](https://github.com/jorisroovers/gitlint/issues/166)). + +## Bugfixes +- Gitlint now properly handles exceptions when using its built-in commit-msg hook ([#166](https://github.com/jorisroovers/gitlint/issues/166)). + +## Development - All dependencies have been upgraded to the latest available versions (`Click==7.1.2`, `arrow==0.17.0`, `sh==1.14.1`). - Much under-the-hood refactoring as a result of dropping Python 2.7 -## v0.14.0 (2020-10-24) ## +# v0.14.0 (2020-10-24) Contributors: Special thanks to all contributors for this release, in particular [mrshu](https://github.com/mrshu), [glasserc](https://github.com/glasserc), [strk](https://github.com/strk), [chgl](https://github.com/chgl), [melg8](https://github.com/melg8) and [sigmavirus24](https://github.com/sigmavirus24). - +## General - **IMPORTANT: Gitlint 0.14.x will be the last gitlint release to support Python 2.7 and Python 3.5, as [both are EOL](https://endoflife.date/python) which makes it difficult to keep supporting them.** - Python 3.9 support -- **New Rule**: [title-min-length](http://jorisroovers.github.io/gitlint/rules/#t8-title-min-length) enforces a minimum length on titles (default: 5 chars) ([#138](https://github.com/jorisroovers/gitlint/issues/138)) -- **New Rule**: [body-match-regex](http://jorisroovers.github.io/gitlint/rules/#b8-body-match-regex) allows users to enforce that the commit-msg body matches a given regex ([#130](https://github.com/jorisroovers/gitlint/issues/130)) -- **New Rule**: [ignore-body-lines](http://jorisroovers.github.io/gitlint/rules/#i3-ignore-body-lines) allows users to -[ignore parts of a commit](http://jorisroovers.github.io/gitlint/gitlint/#ignoring-commits) by matching a regex against -the lines in a commit message body ([#126](https://github.com/jorisroovers/gitlint/issues/126)) - [Named Rules](http://jorisroovers.github.io/gitlint/#named-rules) allow users to have multiple instances of the same rule active at the same time. This is useful when you want to enforce the same rule multiple times but with different options ([#113](https://github.com/jorisroovers/gitlint/issues/113), [#66](https://github.com/jorisroovers/gitlint/issues/66)) - [User-defined Configuration Rules](http://jorisroovers.github.io/gitlint/user_defined_rules/#configuration-rules) allow users to dynamically change gitlint's configuration and/or the commit *before* any other rules are applied. - The `commit-msg` hook has been re-written in Python (it contained a lot of Bash before), fixing a number of platform specific issues. Existing users will need to reinstall their hooks (`gitlint uninstall-hook; gitlint install-hook`) to make use of this. @@ -96,45 +160,75 @@ the lines in a commit message body ([#126](https://github.com/jorisroovers/gitli - Users can now use `self.log.debug("my message")` for debugging purposes in their user-defined rules. Debug messages will show up when running `gitlint --debug`. - **Breaking**: User-defined rule id's can no longer start with 'I', as those are reserved for [built-in gitlint ignore rules](http://jorisroovers.github.io/gitlint/rules/#i1-ignore-by-title). - New `RegexOption` rule [option type for use in user-defined rules](http://jorisroovers.github.io/gitlint/user_defined_rules/#options). By using the `RegexOption`, regular expressions are pre-validated at gitlint startup and compiled only once which is much more efficient when linting multiple commits. -- Bugfixes: - - Improved UTF-8 fallback on Windows (ongoing - [#96](https://github.com/jorisroovers/gitlint/issues/96)) - - Windows users can now use the 'edit' function of the `commit-msg` hook ([#94](https://github.com/jorisroovers/gitlint/issues/94)) - - Doc update: Users should use `--ulimit nofile=1024` when invoking gitlint using Docker ([#129](https://github.com/jorisroovers/gitlint/issues/129)) - - The `commit-msg` hook was broken in Ubuntu's gitlint package due to a python/python3 mismatch ([#127](https://github.com/jorisroovers/gitlint/issues/127)) - - Better error message when no git username is set ([#149](https://github.com/jorisroovers/gitlint/issues/149)) - - Options can now actually be set to `None` (from code) to make them optional. - - Ignore rules no longer have `"None"` as default regex, but an empty regex - effectively disabling them by default (as intended). -- Contrib Rules: - - Added 'ci' and 'build' to conventional commit types ([#135](https://github.com/jorisroovers/gitlint/issues/135)) -- Under-the-hood: minor performance improvements (removed some unnecessary regex matching), test improvements, improved debug logging, CI runs on pull requests, PR request template. - -## v0.13.1 (2020-02-26) +## Rules +- **New Rule**: [title-min-length](http://jorisroovers.github.io/gitlint/rules/#t8-title-min-length) enforces a minimum length on titles (default: 5 chars) ([#138](https://github.com/jorisroovers/gitlint/issues/138)) +- **New Rule**: [body-match-regex](http://jorisroovers.github.io/gitlint/rules/#b8-body-match-regex) allows users to enforce that the commit-msg body matches a given regex ([#130](https://github.com/jorisroovers/gitlint/issues/130)) +- **New Rule**: [ignore-body-lines](http://jorisroovers.github.io/gitlint/rules/#i3-ignore-body-lines) allows users to +[ignore parts of a commit](http://jorisroovers.github.io/gitlint/gitlint/#ignoring-commits) by matching a regex against +the lines in a commit message body ([#126](https://github.com/jorisroovers/gitlint/issues/126)) + +## Contrib Rules +- Added 'ci' and 'build' to conventional commit types ([#135](https://github.com/jorisroovers/gitlint/issues/135)) + +## Bugfixes +- Improved UTF-8 fallback on Windows (ongoing - [#96](https://github.com/jorisroovers/gitlint/issues/96)) +- Windows users can now use the 'edit' function of the `commit-msg` hook ([#94](https://github.com/jorisroovers/gitlint/issues/94)) +- Doc update: Users should use `--ulimit nofile=1024` when invoking gitlint using Docker ([#129](https://github.com/jorisroovers/gitlint/issues/129)) +- The `commit-msg` hook was broken in Ubuntu's gitlint package due to a python/python3 mismatch ([#127](https://github.com/jorisroovers/gitlint/issues/127)) +- Better error message when no git username is set ([#149](https://github.com/jorisroovers/gitlint/issues/149)) +- Options can now actually be set to `None` (from code) to make them optional. +- Ignore rules no longer have `"None"` as default regex, but an empty regex - effectively disabling them by default (as intended). + +## Development +- Minor performance improvements (removed some unnecessary regex matching), +- Test improvements, +- Improved debug logging, +- CI runs on pull requests +- PR request template + +# v0.13.1 (2020-02-26) + +## Bugfixes - Patch to enable `--staged` flag for pre-commit. - Minor doc updates ([#109](https://github.com/jorisroovers/gitlint/issues/109)) -## v0.13.0 (2020-02-25) +# v0.13.0 (2020-02-25) -- **Behavior Change**: Revert Commits are now recognized and ignored by default ([#99](https://github.com/jorisroovers/gitlint/issues/99)) -- `--staged` flag: gitlint can now detect meta-data (such as author details, changed files, etc) of staged/pre-commits. Useful when you use [gitlint's commit-msg hook](https://jorisroovers.github.io/gitlint/#using-gitlint-as-a-commit-msg-hook) or [precommit](https://jorisroovers.github.io/gitlint/#using-gitlint-through-pre-commit) ([#105](https://github.com/jorisroovers/gitlint/issues/105)) -- New branch properties on `GitCommit` and `GitContext`, useful when writing your own user-defined rules: `commit.branches` and `commit.context.current_branch` ([#108](https://github.com/jorisroovers/gitlint/issues/108)) +## General - Python 3.8 support - Python 3.4 no longer supported. Python 3.4 has [reached EOL](https://www.python.org/dev/peps/pep-0429/#id4) and an increasing of gitlint's dependencies have dropped support which makes it hard to maintain. - Improved Windows support: better unicode handling. [Issues remain](https://github.com/jorisroovers/gitlint/issues?q=is%3Aissue+is%3Aopen+label%3Awindows) but the basic functionality works. -- Bugfixes: +- **Behavior Change**: Revert Commits are now recognized and ignored by default ([#99](https://github.com/jorisroovers/gitlint/issues/99)) + +## Features +- `--staged` flag: gitlint can now detect meta-data (such as author details, changed files, etc) of staged/pre-commits. Useful when you use [gitlint's commit-msg hook](https://jorisroovers.github.io/gitlint/#using-gitlint-as-a-commit-msg-hook) or [precommit](https://jorisroovers.github.io/gitlint/#using-gitlint-through-pre-commit) ([#105](https://github.com/jorisroovers/gitlint/issues/105)) +- New branch properties on `GitCommit` and `GitContext`, useful when writing your own user-defined rules: `commit.branches` and `commit.context.current_branch` ([#108](https://github.com/jorisroovers/gitlint/issues/108)) + +## Bugfixes - Gitlint no longer crashes when acting on empty repositories (this only occurred in specific circumstances). - Changed files are now better detected in repos that only have a root commit - Improved performance and memory (gitlint now caches git properties) - Improved `--debug` output - Improved documentation -- Under-the-hood: dependencies updated, unit and integration test improvements, migrated from TravisCI to Github Actions. -## v0.12.0 (2019-07-15) ## +## Development +- Dependencies updated +- Unit and integration test improvements +- Migrated from TravisCI to Github Actions. + +# v0.12.0 (2019-07-15) Contributors: Special thanks to all contributors for this release, in particular [@rogalksi](https://github.com/rogalski) and [@byrney](https://github.com/byrney). +## General +- Python 3.3 no longer supported. Python 3.4 is likely to follow in a future release as it has [reached EOL](https://www.python.org/dev/peps/pep-0429/#id4) as well. +- PyPy 3.5 support + + +## Features - [Contrib Rules](http://jorisroovers.github.io/gitlint/contrib_rules): community-contributed rules that are disabled by default, but can be enabled through configuration. Contrib rules are meant to augment default gitlint behavior by providing users with rules for common use-cases without forcing these rules on all gitlint users. @@ -143,40 +237,40 @@ Special thanks to all contributors for this release, in particular [@rogalksi](h - If you're interested in adding new Contrib rules to gitlint, please start by reading the [Contributing](http://jorisroovers.github.io/gitlint/contributing/) page. Thanks for considering! - *Experimental (!)* Windows support: Basic functionality is working, but there are still caveats. For more details, please refer to [#20](https://github.com/jorisroovers/gitlint/issues/20) and the [open issues related to Windows](https://github.com/jorisroovers/gitlint/issues?q=is%3Aissue+is%3Aopen+label%3Awindows). -- Python 3.3 no longer supported. Python 3.4 is likely to follow in a future release as it has [reached EOL](https://www.python.org/dev/peps/pep-0429/#id4) as well. -- PyPy 3.5 support - Support for `--ignore-stdin` command-line flag to ignore any text send via stdin. ([#56](https://github.com/jorisroovers/gitlint/issues/56), [#89](https://github.com/jorisroovers/gitlint/issues/89)) -- Bugfixes: + +## Bugfixes - [#68: Can't use install-hooks in with git worktree](https://github.com/jorisroovers/gitlint/issues/68) - [#59: gitlint failed with configured commentchar](https://github.com/jorisroovers/gitlint/issues/59) -- Under-the-hood: dependencies updated, experimental Dockerfile, github issue template. -## v0.11.0 (2019-03-13) ## +## Development +- Dependencies updated +- Experimental Dockerfile +- Github issue template. +# v0.11.0 (2019-03-13) + +## General - Python 3.7 support - Python 2.6 no longer supported + +## Development - Various dependency updates and under the hood fixes (see [#76](https://github.com/jorisroovers/gitlint/pull/76) for details). Special thanks to @pbregener for his contributions related to python 3.7 support and test fixes. -## v0.10.0 (2018-04-15) ## +# v0.10.0 (2018-04-15) The 0.10.0 release adds the ability to ignore commits based on their contents, support for [pre-commit](https://pre-commit.com/), and important fix for running gitlint in CI environments (such as Jenkins, Gitlab, etc). Special thanks to [asottile](https://github.com/asottile), [bdrung](https://github.com/bdrung), [pbregener](https://github.com/pbregener), [torwald-sergesson](https://github.com/torwald-sergesson), [RykHawthorn](https://github.com/RykHawthorn), [SteffenKockel](https://github.com/SteffenKockel) and [tommyip](https://github.com/tommyip) for their contributions. -**Since it's becoming increasingly hard to support Python 2.6 and 3.3, we'd like to encourage our users to upgrade their -python version to 2.7 or 3.3+. Future versions of gitlint are likely to drop support for Python 2.6 and 3.3.** +## General +- **Since it's becoming increasingly hard to support Python 2.6 and 3.3, we'd like to encourage our users to upgrade their + python version to 2.7 or 3.3+. Future versions of gitlint are likely to drop support for Python 2.6 and 3.3.** -Full Changelog: - -- **New Rule**: `ignore-by-title` allows users to -[ignore certain commits](http://jorisroovers.github.io/gitlint/#ignoring-commits) by matching a regex against -a commit message title. ([#54](https://github.com/jorisroovers/gitlint/issues/54), [#57](https://github.com/jorisroovers/gitlint/issues/57)). -- **New Rule**: `ignore-by-body` allows users to -[ignore certain commits](http://jorisroovers.github.io/gitlint/#ignoring-commits) by matching a regex against -a line in a commit message body. +## Features - Gitlint now supports [pre-commit.com](https://pre-commit.com). [Details in our documentation](http://jorisroovers.github.io/gitlint/#using-gitlint-through-pre-commit) ([#62](https://github.com/jorisroovers/gitlint/issues/62)). @@ -186,18 +280,26 @@ a line in a commit message body. - Gitlint can now be installed on MacOS by brew via the [homebrew-devops](https://github.com/rockyluke/homebrew-devops) tap. To get the latest version of gitlint, always use pip for installation. - If all goes well, [gitlint will also be available as a package in the Ubuntu 18.04 repositories](https://launchpad.net/ubuntu/+source/gitlint). -- Bugfixes: - - We fixed a nasty and recurring issue with running gitlint in CI. Hopefully that's the end of it :-) ([#40](https://github.com/jorisroovers/gitlint/issues/40)). - - Fix for custom git comment characters ([#48](https://github.com/jorisroovers/gitlint/issues/48)). -## v0.9.0 (2017-12-03) ## +## Rules +- **New Rule**: `ignore-by-title` allows users to +[ignore certain commits](http://jorisroovers.github.io/gitlint/#ignoring-commits) by matching a regex against +a commit message title. ([#54](https://github.com/jorisroovers/gitlint/issues/54), [#57](https://github.com/jorisroovers/gitlint/issues/57)). +- **New Rule**: `ignore-by-body` allows users to +[ignore certain commits](http://jorisroovers.github.io/gitlint/#ignoring-commits) by matching a regex against +a line in a commit message body. + +## Bugfixes +- We fixed a nasty and recurring issue with running gitlint in CI. Hopefully that's the end of it :-) ([#40](https://github.com/jorisroovers/gitlint/issues/40)). +- Fix for custom git comment characters ([#48](https://github.com/jorisroovers/gitlint/issues/48)). + +# v0.9.0 (2017-12-03) The 0.9.0 release adds a new default `author-valid-email` rule, important bugfixes and special case handling. Special thanks to [joshholl](https://github.com/joshholl), [ron8mcr](https://github.com/ron8mcr), [omarkohl](https://github.com/omarkohl), [domo141](https://github.com/domo141), [nud](https://github.com/nud) and [AlexMooney](https://github.com/AlexMooney) for their contributions. -- New Rule: `author-valid-email` enforces a valid author email address. Details can be found in the - [Rules section of the documentation](http://jorisroovers.github.io/gitlint/rules/#m1-author-valid-email). +## General - **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 <ref>` won't work anymore. Instead, for single commits, @@ -212,68 +314,89 @@ and [AlexMooney](https://github.com/AlexMooney) for their contributions. - **Behavior Change**: Gitlint will now by default [ignore squash and fixup commits](http://jorisroovers.github.io/gitlint/#merge-fixup-and-squash-commits) (fix for [#33: fixup messages should not trigger a gitlint violation](https://github.com/jorisroovers/gitlint/issues/33)) + +## Features - Support for custom comment characters ([#34](https://github.com/jorisroovers/gitlint/issues/34)) - Support for [`git commit --cleanup=scissors`](https://git-scm.com/docs/git-commit#git-commit---cleanupltmodegt) ([#34](https://github.com/jorisroovers/gitlint/issues/34)) -- Bugfix: [#37: Prevent Commas in text fields from breaking git log printing](https://github.com/jorisroovers/gitlint/issues/37) + +## Rules +- New Rule: `author-valid-email` enforces a valid author email address. Details can be found in the + [Rules section of the documentation](http://jorisroovers.github.io/gitlint/rules/#m1-author-valid-email). + +## Bugfixes +- [#37: Prevent Commas in text fields from breaking git log printing](https://github.com/jorisroovers/gitlint/issues/37) + +## Development - Debug output improvements -## v0.8.2 (2017-04-25) ## +# v0.8.2 (2017-04-25) The 0.8.2 release brings minor improvements, bugfixes and some under-the-hood changes. Special thanks to [tommyip](https://github.com/tommyip) for his contributions. +## Features - `--extra-path` now also accepts a file path (in the past only directory paths where accepted). Thanks to [tommyip](https://github.com/tommyip) for implementing this! - gitlint will now show more information when using the `--debug` flag. This is initial work and will continue to be improved upon in later releases. -- Bugfixes: + +## Bugfixes - [#24: --commits doesn't take commit specific config into account](https://github.com/jorisroovers/gitlint/issues/24) - [#27: --commits returns the wrong exit code](https://github.com/jorisroovers/gitlint/issues/27) -- Development: better unit and integration test coverage for `--commits` +## Development +- Better unit and integration test coverage for `--commits` -## v0.8.1 (2017-03-16) ## +# v0.8.1 (2017-03-16) The 0.8.1 release brings minor tweaks and some experimental features. Special thanks to [tommyip](https://github.com/tommyip) for his contributions. +## General +- Experimental: Python 3.6 support +- Improved Windows error messaging: gitlint will now show a more descriptive error message when ran on windows. + See [#20](https://github.com/jorisroovers/gitlint/issues/20) for details on the lack of Windows support. + +## Features - Experimental: Linting a range of commits. [Documentation](http://jorisroovers.github.io/gitlint/#linting-a-range-of-commits). Known Caveats: [#23](https://github.com/jorisroovers/gitlint/issues/23), [#24](https://github.com/jorisroovers/gitlint/issues/24). Closes [#14](https://github.com/jorisroovers/gitlint/issues/14). Thanks to [tommyip](https://github.com/tommyip) for implementing this! -- Experimental: Python 3.6 support -- Improved Windows error messaging: gitlint will now show a more descriptive error message when ran on windows. - See [#20](https://github.com/jorisroovers/gitlint/issues/20) for details on the lack of Windows support. - -## v0.8.0 (2016-12-30) ## + +# v0.8.0 (2016-12-30) The 0.8.0 release is a significant release that has been in the works for a long time. Special thanks to [Claymore](https://github.com/Claymore), [gernd](https://github.com/gernd) and [ZhangYaxu](https://github.com/ZhangYaxu) for submitting bug reports and pull requests. +## General - Full unicode support: you can now lint messages in any language! This fixes [#16](https://github.com/jorisroovers/gitlint/issues/16) and [#18](https://github.com/jorisroovers/gitlint/pull/18). +- Pypy2 support! +- Various documentation improvements + +## Features - User-defined rules: you can now [define your own custom rules](http://jorisroovers.github.io/gitlint/user_defined_rules/) if you want to extend gitlint's functionality. -- Pypy2 support! - Debug output improvements: Gitlint will now print your active configuration when using `--debug` - The `general.target` option can now also be set via `-c` flags or a `.gitlint` file -- Bugfixes: - - Various important fixes related to configuration precedence - - [#17: Body MinLength is not working properly](https://github.com/jorisroovers/gitlint/issues/17). - **Behavior Change**: Gitlint now always applies this rule, even if the body has just a single line of content. - Also, gitlint now counts the body-length for the entire body, not just the length of the first line. -- Various documentation improvements -- Development: - - Pylint compliance for all supported python versions - - Updated dependencies to latest versions - - Various `run_tests.sh` improvements for developer convenience -## v0.7.1 (2016-06-18) ## -Bugfixes: +## Bugfixes +- Various important fixes related to configuration precedence +- [#17: Body MinLength is not working properly](https://github.com/jorisroovers/gitlint/issues/17). + **Behavior Change**: Gitlint now always applies this rule, even if the body has just a single line of content. + Also, gitlint now counts the body-length for the entire body, not just the length of the first line. + +## Development +- Pylint compliance for all supported python versions +- Updated dependencies to latest versions +- Various `run_tests.sh` improvements for developer convenience + +# v0.7.1 (2016-06-18) +## Bugfixes - **Behavior Change**: gitlint no longer prints the file path by default when using a `.gitlint` file. The path will still be printed when using the new `--debug` flag. Special thanks to [Slipcon](https://github.com/slipcon) @@ -283,53 +406,68 @@ for submitting this. - Gitlint is now better at parsing commit messages cross-platform by taking platform specific line endings into account - Minor documentation improvements -## v0.7.0 (2016-04-20) ## +# v0.7.0 (2016-04-20) This release contains mostly bugfix and internal code improvements. Special thanks to [William Turell](https://github.com/wturrell) and [Joe Grund](https://github.com/jgrund) for bug reports and pull requests. -- commit-msg hooks improvements: The new commit-msg hook now allows you to edit your message if it contains violations, - prints the commit message on aborting and is more compatible with GUI-based git clients such as SourceTree. - *You will need to uninstall and reinstall the commit-msg hook for these latest features*. +## General - Python 2.6 support - **Behavior change**: merge commits are now ignored by default. The rationale is that the original commits should already be linted and that many merge commits don't pass gitlint checks by default (e.g. exceeding title length or empty body is very common). This behavior can be overwritten by setting the general option `ignore-merge-commit=false`. -- Bugfixes and enhancements: - - [#7: Hook compatibility with SourceTree](https://github.com/jorisroovers/gitlint/issues/7) - - [#8: Illegal option -e](https://github.com/jorisroovers/gitlint/issues/8) - - [#9: print full commit msg to stdout if aborted](https://github.com/jorisroovers/gitlint/issues/9) - - [#11 merge commit titles exceeding the max title length by default](https://github.com/jorisroovers/gitlint/issues/11) - - Better error handling of invalid general options -- Development: internal refactoring to extract more info from git. This will allow for more complex rules in the future. -- Development: initial set of integration tests. Test gitlint end-to-end after it is installed. -- Development: pylint compliance for python 2.7 -## v0.6.1 (2015-11-22) ## +## Features +- commit-msg hooks improvements: The new commit-msg hook now allows you to edit your message if it contains violations, + prints the commit message on aborting and is more compatible with GUI-based git clients such as SourceTree. + *You will need to uninstall and reinstall the commit-msg hook for these latest features*. + +## Bugfixes +- [#7: Hook compatibility with SourceTree](https://github.com/jorisroovers/gitlint/issues/7) +- [#8: Illegal option -e](https://github.com/jorisroovers/gitlint/issues/8) +- [#9: print full commit msg to stdout if aborted](https://github.com/jorisroovers/gitlint/issues/9) +- [#11 merge commit titles exceeding the max title length by default](https://github.com/jorisroovers/gitlint/issues/11) +- Better error handling of invalid general options -- Fix: `install-hook` and `generate-config` commands not working when gitlint is installed from pypi. +## Development +- internal refactoring to extract more info from git. This will allow for more complex rules in the future. +- initial set of integration tests. Test gitlint end-to-end after it is installed. +- pylint compliance for python 2.7 -## v0.6.0 (2015-11-22) ## +# v0.6.1 (2015-11-22) +## Bugfixes + +- `install-hook` and `generate-config` commands not working when gitlint is installed from pypi. + +# v0.6.0 (2015-11-22) + +## General - Python 3 (3.3+) support! - All documentation is now hosted on [http://jorisroovers.github.io/gitlint/]() -- New `generate-config` command generates a sample gitlint config file -- New `--target` flag allows users to lint different directories than the current working directory - **Breaking change**: exit code behavior has changed. More details in the [Exit codes section of the documentation](http://jorisroovers.github.io/gitlint/#exit-codes). - **Breaking change**: `--install-hook` and `--uninstall-hook` have been renamed to `install-hook` and `uninstall-hook` respectively to better express that they are commands instead of options. + +## Features +- New `generate-config` command generates a sample gitlint config file +- New `--target` flag allows users to lint different directories than the current working directory - Better error handling when gitlint is executed in a directory that is not a git repository or when git is not installed. - The git commit message hook now uses pretty colored output -- Fix: `--config` option no longer accepts directories as value -- Development: unit tests are now ran using py.test -## v0.5.0 (2015-10-04) ## +## Bugfixes +- `--config` option no longer accepts directories as value + +## Development +- Unit tests are now ran using py.test + +# v0.5.0 (2015-10-04) + +## Features -- New Rule: `title-match-regex`. Details can be found in the - [Rules section of the documentation](http://jorisroovers.github.io/gitlint/rules/). - Uninstall previously installed gitlint git commit hooks using: `gitlint --uninstall-hook` - Ignore rules on a per commit basis by adding e.g.: `gitlint-ignore: T1, body-hard-tab` to your git commit message. Use `gitlint-ignore: all` to disable gitlint all together for a specific commit. @@ -338,38 +476,66 @@ requests. - Violations are now sorted by line number first and then by rule id (previously the order of violations on the same line was arbitrary). -## v0.4.1 (2015-09-19) ## +## Rules +- New Rule: `title-match-regex`. Details can be found in the + [Rules section of the documentation](http://jorisroovers.github.io/gitlint/rules/). -- Internal fix: added missing comma to setup.py which prevented pypi upload +# v0.4.1 (2015-09-19) -## v0.4.0 (2015-09-19) ## +## Bugfixes +- Added missing comma to setup.py which prevented pypi upload + +# v0.4.0 (2015-09-19) + +## General +- gitlint is now also released as a [python wheel](http://pythonwheels.com/) on pypi. + +## Features +- The git `commit-msg` hook now allows you to keep or discard the commit when it fails gitlint validation + +## Rules - New rules: `body-is-missing`, `body-min-length`, `title-leading-whitespace`, `body-changed-file-mention`. Details can be found in the [Rules section of the documentation](http://jorisroovers.github.io/gitlint/rules/). -- The git `commit-msg` hook now allows you to keep or discard the commit when it fails gitlint validation -- gitlint is now also released as a [python wheel](http://pythonwheels.com/) on pypi. + +## Development - Internal: rule classes now have access to a gitcontext containing body the commit message and the files changed in the last commit. -## v0.3.0 (2015-09-11) ## -- `title-must-not-contain-word` now has a `words` option that can be used to specify which words should not - occur in the title +# v0.3.0 (2015-09-11) +## Features - gitlint violations are now printed to the stderr instead of stdout -- Various minor bugfixes - gitlint now ignores commented out lines (i.e. starting with #) in your commit messages - Experimental: git commit-msg hook support + +## Rules +- `title-must-not-contain-word` now has a `words` option that can be used to specify which words should not + occur in the title + +## Bugfixes +- Various minor bugfixes + +## Development - Under-the-hood: better test coverage :-) -## v0.2.0 (2015-09-10) ## - - Rules can now have their behavior configured through options. +# v0.2.0 (2015-09-10) + +## Features +- Rules can now have their behavior configured through options. For example, the `title-max-length` rule now has a `line-length` option. - - Under-the-hood: The codebase now has a basic level of unit test coverage, increasing overall quality assurance + +## Development + - The codebase now has a basic level of unit test coverage, increasing overall quality assurance -## v0.1.1 (2015-09-08) ## -- Bugfix: added missing `sh` dependency +# v0.1.1 (2015-09-08) + +## Bugfixes +- Added missing `sh` dependency + +# v0.1.0 (2015-09-08) -## v0.1.0 (2015-09-08) ## +## General - Initial gitlint release - Initial set of rules: title-max-length, title-trailing-whitespace, title-trailing-punctuation , title-hard-tab, title-must-not-contain-word, body-max-line-length, body-trailing-whitespace, body-hard-tab @@ -1,5 +1,3 @@ -# User-facing Dockerfile. For development, see Dockerfile.dev and ./run_tests.sh -h - # To lint your current working directory: # docker run --ulimit nofile=1024 -v $(pwd):/repo jorisroovers/gitlint @@ -9,7 +7,7 @@ # NOTE: --ulimit is required to work around a limitation in Docker # Details: https://github.com/jorisroovers/gitlint/issues/129 -FROM python:3.11.0-alpine +FROM python:3.11.2-alpine ARG GITLINT_VERSION RUN apk add git diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index 5cd1739..0000000 --- a/Dockerfile.dev +++ /dev/null @@ -1,17 +0,0 @@ -# Note: development using the local Dockerfile is still work-in-progress -# Getting started: http://jorisroovers.github.io/gitlint/contributing/ -ARG python_version_dotted - -FROM python:${python_version_dotted}-stretch - -RUN apt-get update -# software-properties-common contains 'add-apt-repository' -RUN apt-get install -y git silversearcher-ag jq curl - -ADD . /gitlint -WORKDIR /gitlint - -RUN pip install --ignore-requires-python -r requirements.txt -RUN pip install --ignore-requires-python -r test-requirements.txt - -CMD ["/bin/bash"] diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index ad3da8a..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -include README.md -include LICENSE -exclude Vagrantfile -exclude *.yml *.sh *.txt -recursive-exclude examples * -recursive-exclude gitlint-core * -recursive-exclude qa * @@ -1,15 +1,16 @@ # gitlint: [jorisroovers.github.io/gitlint](http://jorisroovers.github.io/gitlint/) # [![Tests](https://github.com/jorisroovers/gitlint/workflows/Tests%20and%20Checks/badge.svg)](https://github.com/jorisroovers/gitlint/actions?query=workflow%3A%22Tests+and+Checks%22) +[![Coverage Status](https://coveralls.io/repos/github/jorisroovers/gitlint/badge.svg?branch=fix-coveralls)](https://coveralls.io/github/jorisroovers/gitlint?branch=fix-coveralls) [![PyPi Package](https://img.shields.io/pypi/v/gitlint.png)](https://pypi.python.org/pypi/gitlint) ![Supported Python Versions](https://img.shields.io/pypi/pyversions/gitlint.svg) -Git commit message linter written in python (for Linux and Mac, experimental on Windows), checks your commit messages for style. +Git commit message linter written in python, checks your commit messages for style. **See [jorisroovers.github.io/gitlint](http://jorisroovers.github.io/gitlint/) for full documentation.** <a href="http://jorisroovers.github.io/gitlint/" target="_blank"> -<img src="docs/images/readme-gitlint.png" /> +<img src="https://raw.githubusercontent.com/jorisroovers/gitlint/main/docs/images/readme-gitlint.png" /> </a> ## Contributing ## diff --git a/doc-requirements.txt b/doc-requirements.txt deleted file mode 100644 index 40febbe..0000000 --- a/doc-requirements.txt +++ /dev/null @@ -1 +0,0 @@ -mkdocs==1.4.1
\ No newline at end of file diff --git a/docs/contributing.md b/docs/contributing.md index 254e856..d111bc6 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -3,13 +3,14 @@ We'd love for you to contribute to gitlint. Thanks for your interest! The [source-code and issue tracker](https://github.com/jorisroovers/gitlint) are hosted on Github. -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 project plan on github projects](https://github.com/users/jorisroovers/projects/1/), but -that's open to a lot of change and input. +!!! note + 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 project plan on github projects](https://github.com/users/jorisroovers/projects/1/), but + that's open to a lot of change and input. -## Guidelines +## Overall Guidelines When contributing code, please consider all the parts that are typically required: @@ -26,28 +27,46 @@ can make it as part of a release. **Gitlint commits and pull requests are gated 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 + It's 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 - to make it worthwhile or when there's a critical fix for a bug that fundamentally breaks gitlint. While the amount - of overhead of doing a release isn't huge, it's also not zero. In practice this means that it might take weeks - or months before merged code actually gets released - we know that can be frustrating but please understand it's - a well-considered trade-off based on available time. +## Releases +Gitlint releases typically go out when there's either enough new features and fixes +to make it worthwhile or when there's a critical fix for a bug that fundamentally breaks gitlint. -## Local setup +While the amount of overhead of doing a release isn't huge, it's also not zero. In practice this means that it might +take weeks or months before merged code actually gets released - we know that can be frustrating but please +understand it's a well-considered trade-off based on available time. -To install gitlint for local development: +### Dev Builds +While final releases are usually months apart, we do dev builds on every commit to `main`: +- **gitlint**: [https://pypi.org/project/gitlint/#history](https://pypi.org/project/gitlint/#history) +- **gitlint-core**: [https://pypi.org/project/gitlint-core/#history](https://pypi.org/project/gitlint-core/#history) + +It usually takes about 5 min after merging a PR to `main` for new dev builds to show up. Note that the installation +of a recently published version can still fail for a few minutes after a new version shows up on PyPI while the package +is replicated to all download mirrors. + +To install a dev build of gitlint: ```sh -python -m venv .venv -. .venv/bin/activate -pip install -r requirements.txt -r test-requirements.txt -r doc-requirements.txt -python setup.py develop +# Find latest dev build on https://pypi.org/project/gitlint/#history +pip install gitlint=="0.19.0.dev68" ``` -## Github Devcontainer + +## Environment setup +### Local setup + +Gitlint uses [hatch](https://hatch.pypa.io/latest/) for project management. +You do not need to setup a `virtualenv`, hatch will take care of that for you. + +```sh +pip install hatch +``` + +### Github Devcontainer We provide a devcontainer on github to make it easier to get started with gitlint development using VSCode. @@ -59,12 +78,6 @@ To start one, click the plus button under the *Code* dropdown on ![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: @@ -83,27 +96,43 @@ asdf list python ## Running tests ```sh -./run_tests.sh # run unit tests and print test coverage -./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 --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, format and gitlint checks +# Gitlint +hatch run dev:gitlint # run the local source copy of gitlint +hatch run dev:gitlint --version # This is just the gitlint binary, any flag will work +hatch run dev:gitlint --debug + +# Unit tests +hatch run test:unit-tests # run unit tests +hatch run test:unit-tests gitlint-core/gitlint/tests/rules/test_body_rules.py::BodyRuleTests::test_body_missing # run a single test +hatch run test:unit-tests -k test_body_missing_merge_commit # Run a specific tests using a pytest keyword expression +hatch run test:unit-tests-no-cov # run unit tests without test coverage + +# Integration tests +hatch run qa:install-local # One-time install: install the local gitlint source copy for integration testing +hatch run qa:integration-tests # Run integration tests + +# Formatting check (black) +hatch run test:format # Run formatting checks + +# Linting (ruff) +hatch run test:lint # Run Ruff + +# Project stats +hatch run test:stats ``` -## Formatting +## Autoformatting and autofixing We use [black](https://black.readthedocs.io/en/stable/) for code formatting. -To use it, just run black against the code you modified: ```sh -black . # format all python code -black gitlint-core/gitlint/lint.py # format a specific file +hatch run test:autoformat # format all python code +hatch run test:autoformat gitlint-core/gitlint/lint.py # format a specific file +``` + +We use [ruff](https://github.com/charliermarsh/ruff) for linting, it can autofix many of the issue it finds +(although not always perfect). +```sh +hatch run test:autofix # Attempt to fix linting issues ``` ## Documentation @@ -111,8 +140,7 @@ We use [mkdocs](https://www.mkdocs.org/) for generating our documentation from m To use it: ```sh -pip install -r doc-requirements.txt # install doc requirements -mkdocs serve +hatch run docs:serve ``` Then access the documentation website on [http://localhost:8000](). @@ -133,17 +161,18 @@ to split gitlint in 2 packages. ![Gitlint package structure](images/gitlint-packages.png) -### Packaging description - -To see the package description in HTML format +To build the packages locally: ```sh -pip install docutils -export LC_ALL=en_US.UTF-8 -export LANG=en_US.UTF-8 -python setup.py --long-description | rst2html.py > output.html +# gitlint +hatch build +hatch clean # cleanup + +# gitlint-core +cd gitlint-core +hatch build +hatch clean # cleanup ``` - ## Tools We keep a small set of scripts in the `tools/` directory: diff --git a/docs/index.md b/docs/index.md index 801a16e..b735b6b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,7 +7,7 @@ Great for use as a [commit-msg git hook](#using-gitlint-as-a-commit-msg-hook) or <script type="text/javascript" src="https://asciinema.org/a/30477.js" id="asciicast-30477" async></script> !!! note - **Gitlint support for Windows is experimental**, and [there are some known issues](https://github.com/jorisroovers/gitlint/issues?q=is%3Aissue+is%3Aopen+label%3Awindows). + **Gitlint works on Windows**, but [there are some known issues](https://github.com/jorisroovers/gitlint/issues?q=is%3Aissue+is%3Aopen+label%3Awindows). Also, gitlint is not the only git commit message linter out there, if you are looking for an alternative written in a different language, have a look at [fit-commit](https://github.com/m1foley/fit-commit) (Ruby), @@ -15,7 +15,7 @@ Great for use as a [commit-msg git hook](#using-gitlint-as-a-commit-msg-hook) or !!! important - **Gitlint no longer supports Python 2.7 and Python 3.5 as they [have reached End-Of-Life](https://endoflife.date/python). The last gitlint version to support Python 2.7 and Python 3.5 is `0.14.0` (released on October 24th, 2020).** + **Gitlint requires Python 3.7 (or above). For Python 2.7 and Python 3.5 use `gitlint==0.14.0` (released 2020-10-24), for Python 3.6 `gitlint==0.18.0` (released 2022-11-16).** ## Features - **Commit message hook**: [Auto-trigger validations against new commit message right when you're committing](#using-gitlint-as-a-commit-msg-hook). Also [works with pre-commit](#using-gitlint-through-pre-commit). @@ -30,7 +30,8 @@ useful throughout the years. - **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, - python code standards (pep8, pylint), good documentation, widely used, proven track record. + python code standards ([black](https://github.com/psf/black), [ruff](https://github.com/charliermarsh/ruff)), + good documentation, widely used, proven track record. ## Getting Started ### Installation @@ -271,7 +272,7 @@ 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 + rev: # insert ref, e.g. v0.18.0 hooks: - id: gitlint - id: gitlint-ci @@ -317,12 +318,14 @@ gitlint --commits mybranch # Lint all commits that are different between a branch and your main branch gitlint --commits "main..mybranch" # Use git's special references -gitlint --commits "origin..HEAD" +gitlint --commits "origin/main..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 +# You can also lint a single commit with --commits: +gitling --commits 019cf40, ``` The `--commits` flag takes a **single** refspec argument or commit range. Basically, any range that is understood @@ -331,6 +334,7 @@ by [git rev-list](https://git-scm.com/docs/git-rev-list) as a single argument wi 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. +`--commits` also accepts a single commit SHA with a trailing comma. 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. diff --git a/gitlint-core/LICENSE b/gitlint-core/LICENSE index ea5b606..122bd28 120000..100644 --- a/gitlint-core/LICENSE +++ b/gitlint-core/LICENSE @@ -1 +1,22 @@ -../LICENSE
\ No newline at end of file +The MIT License (MIT) + +Copyright (c) 2015 Joris Roovers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/gitlint-core/MANIFEST.in b/gitlint-core/MANIFEST.in deleted file mode 100644 index 375cec1..0000000 --- a/gitlint-core/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include README.md -include LICENSE -recursive-exclude gitlint/tests * diff --git a/gitlint-core/README.md b/gitlint-core/README.md index 32d46ee..dfbbe7f 120000..100644 --- a/gitlint-core/README.md +++ b/gitlint-core/README.md @@ -1 +1,26 @@ -../README.md
\ No newline at end of file +# Gitlint-core + +# gitlint: [jorisroovers.github.io/gitlint](http://jorisroovers.github.io/gitlint/) # + +[![Tests](https://github.com/jorisroovers/gitlint/workflows/Tests%20and%20Checks/badge.svg)](https://github.com/jorisroovers/gitlint/actions?query=workflow%3A%22Tests+and+Checks%22) +[![Coverage Status](https://coveralls.io/repos/github/jorisroovers/gitlint/badge.svg?branch=fix-coveralls)](https://coveralls.io/github/jorisroovers/gitlint?branch=fix-coveralls) +[![PyPi Package](https://img.shields.io/pypi/v/gitlint.png)](https://pypi.python.org/pypi/gitlint) +![Supported Python Versions](https://img.shields.io/pypi/pyversions/gitlint.svg) + +Git commit message linter written in python, checks your commit messages for style. + +**See [jorisroovers.github.io/gitlint](http://jorisroovers.github.io/gitlint/) for full documentation.** + +<a href="http://jorisroovers.github.io/gitlint/" target="_blank"> +<img src="https://raw.githubusercontent.com/jorisroovers/gitlint/main/docs/images/readme-gitlint.png" /> +</a> + +## Contributing ## +All contributions are welcome and very much appreciated! + +**I'm [looking for contributors](https://github.com/jorisroovers/gitlint/issues/134) that are interested in taking a more active co-maintainer role as it's becoming increasingly difficult for me to find time to maintain gitlint. Please leave a comment in [#134](https://github.com/jorisroovers/gitlint/issues/134) if you're interested!** + +See [jorisroovers.github.io/gitlint/contributing](http://jorisroovers.github.io/gitlint/contributing) for details on +how to get started - it's easy! + +We maintain a [loose project plan on Github Projects](https://github.com/users/jorisroovers/projects/1/views/1). diff --git a/gitlint-core/gitlint/__init__.py b/gitlint-core/gitlint/__init__.py index ad6b570..a2339fd 100644 --- a/gitlint-core/gitlint/__init__.py +++ b/gitlint-core/gitlint/__init__.py @@ -1 +1,8 @@ -__version__ = "0.19.0dev" +import sys + +if sys.version_info >= (3, 8): + from importlib import metadata # pragma: nocover +else: + import importlib_metadata as metadata # pragma: nocover + +__version__ = metadata.version("gitlint-core") diff --git a/gitlint-core/gitlint/cache.py b/gitlint-core/gitlint/cache.py index b84c904..a3dd0c8 100644 --- a/gitlint-core/gitlint/cache.py +++ b/gitlint-core/gitlint/cache.py @@ -13,7 +13,7 @@ class PropertyCache: return self._cache[cache_key] -def cache(original_func=None, cachekey=None): # pylint: disable=unused-argument +def cache(original_func=None, cachekey=None): """Cache decorator. Caches function return values. Requires the parent class to extend and initialize PropertyCache. Usage: diff --git a/gitlint-core/gitlint/cli.py b/gitlint-core/gitlint/cli.py index 387072e..619f006 100644 --- a/gitlint-core/gitlint/cli.py +++ b/gitlint-core/gitlint/cli.py @@ -1,22 +1,22 @@ -# pylint: disable=bad-option-value,wrong-import-position -# We need to disable the import position checks because of the windows check that we need to do below import copy import logging import os import platform import stat import sys + import click import gitlint -from gitlint.lint import GitLinter +from gitlint import hooks from gitlint.config import LintConfigBuilder, LintConfigError, LintConfigGenerator -from gitlint.deprecation import LOG as DEPRECATED_LOG, DEPRECATED_LOG_FORMAT +from gitlint.deprecation import DEPRECATED_LOG_FORMAT +from gitlint.deprecation import LOG as DEPRECATED_LOG +from gitlint.exception import GitlintError from gitlint.git import GitContext, GitContextError, git_version -from gitlint import hooks +from gitlint.lint import GitLinter from gitlint.shell import shell from gitlint.utils import LOG_FORMAT -from gitlint.exception import GitlintError # Error codes GITLINT_SUCCESS = 0 @@ -40,8 +40,6 @@ LOG = logging.getLogger("gitlint.cli") class GitLintUsageError(GitlintError): """Exception indicating there is an issue with how gitlint is used.""" - pass - def setup_logging(): """Setup gitlint logging""" @@ -49,7 +47,7 @@ def setup_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) + root_log.setLevel(logging.WARN) handler = logging.StreamHandler() formatter = logging.Formatter(LOG_FORMAT) handler.setFormatter(formatter) @@ -69,10 +67,11 @@ def log_system_info(): LOG.debug("Git version: %s", git_version()) LOG.debug("Gitlint version: %s", gitlint.__version__) LOG.debug("GITLINT_USE_SH_LIB: %s", os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]")) - LOG.debug("DEFAULT_ENCODING: %s", gitlint.utils.DEFAULT_ENCODING) + LOG.debug("TERMINAL_ENCODING: %s", gitlint.utils.TERMINAL_ENCODING) + LOG.debug("FILE_ENCODING: %s", gitlint.utils.FILE_ENCODING) -def build_config( # pylint: disable=too-many-arguments +def build_config( target, config_path, c, @@ -172,11 +171,9 @@ def build_git_context(lint_config, msg_filename, commit_hash, refspec): 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( # pylint: disable=unnecessary-lambda-assignment - message, lint_config.target - ) - ) + + def from_commit_msg(message): + return GitContext.from_staged_commit(message, lint_config.target) # Order of precedence: # 1. Any data specified via --msg-filename @@ -208,7 +205,7 @@ def build_git_context(lint_config, msg_filename, commit_hash, refspec): 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(",")] + commit_hashes = [hash.strip() for hash in refspec.split(",") if hash] 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) @@ -247,43 +244,43 @@ class ContextObj: # fmt: off -@click.group(invoke_without_command=True, context_settings={'max_content_width': 120}, +@click.group(invoke_without_command=True, context_settings={"max_content_width": 120}, epilog="When no COMMAND is specified, gitlint defaults to 'gitlint lint'.") -@click.option('--target', envvar='GITLINT_TARGET', +@click.option("--target", envvar="GITLINT_TARGET", type=click.Path(exists=True, resolve_path=True, file_okay=False, readable=True), help="Path of the target git repository. [default: current working directory]") -@click.option('-C', '--config', envvar='GITLINT_CONFIG', +@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, +@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, + "Flag can be used multiple times to set multiple config values.") +@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 (refspec or comma-separated hashes) to lint. [default: HEAD]") -@click.option('-e', '--extra-path', envvar='GITLINT_EXTRA_PATH', +@click.option("-e", "--extra-path", envvar="GITLINT_EXTRA_PATH", help="Path to a directory or python module with extra user-defined rules", 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="", +@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(encoding=gitlint.utils.DEFAULT_ENCODING), +@click.option("--msg-filename", type=click.File(encoding=gitlint.utils.FILE_ENCODING), help="Path to a file containing a commit-msg.") -@click.option('--ignore-stdin', envvar='GITLINT_IGNORE_STDIN', is_flag=True, +@click.option("--ignore-stdin", envvar="GITLINT_IGNORE_STDIN", is_flag=True, help="Ignore any stdin data. Useful for running in CI server.") -@click.option('--staged', envvar='GITLINT_STAGED', is_flag=True, +@click.option("--staged", envvar="GITLINT_STAGED", is_flag=True, help="Attempt smart guesses about meta info (like author name, email, branch, changed files, etc) " + "for staged commits.") -@click.option('--fail-without-commits', envvar='GITLINT_FAIL_WITHOUT_COMMITS', is_flag=True, +@click.option("--fail-without-commits", envvar="GITLINT_FAIL_WITHOUT_COMMITS", is_flag=True, help="Hard fail when the target commit range is empty.") -@click.option('-v', '--verbose', envvar='GITLINT_VERBOSITY', count=True, default=0, - help="Verbosity, more v's for more verbose output (e.g.: -v, -vv, -vvv). [default: -vvv]", ) -@click.option('-s', '--silent', envvar='GITLINT_SILENT', is_flag=True, +@click.option("-v", "--verbose", envvar="GITLINT_VERBOSITY", count=True, default=0, + help="Verbosity, use multiple times for more verbose output (e.g.: -v, -vv, -vvv). [default: -vvv]", ) +@click.option("-s", "--silent", envvar="GITLINT_SILENT", is_flag=True, help="Silent mode (no output). Takes precedence over -v, -vv, -vvv.") -@click.option('-d', '--debug', envvar='GITLINT_DEBUG', help="Enable debugging output.", is_flag=True) +@click.option("-d", "--debug", envvar="GITLINT_DEBUG", help="Enable debugging output.", is_flag=True) @click.version_option(version=gitlint.__version__) @click.pass_context -def cli( # pylint: disable=too-many-arguments +def cli( ctx, target, config, c, commit, commits, extra_path, ignore, contrib, msg_filename, ignore_stdin, staged, fail_without_commits, verbose, silent, debug, @@ -499,5 +496,4 @@ def generate_config(ctx): # Let's Party! setup_logging() if __name__ == "__main__": - # pylint: disable=no-value-for-parameter cli() # pragma: no cover diff --git a/gitlint-core/gitlint/config.py b/gitlint-core/gitlint/config.py index f038d4a..4b38d90 100644 --- a/gitlint-core/gitlint/config.py +++ b/gitlint-core/gitlint/config.py @@ -1,17 +1,19 @@ -from configparser import ConfigParser, Error as ConfigParserError - import copy -import re import os +import re import shutil - from collections import OrderedDict -from gitlint.utils import DEFAULT_ENCODING -from gitlint import rules # For some weird reason pylint complains about this, pylint: disable=unused-import -from gitlint import options -from gitlint import rule_finder +from configparser import ConfigParser +from configparser import Error as ConfigParserError + +from gitlint import ( + options, + rule_finder, + rules, +) from gitlint.contrib import rules as contrib_rules from gitlint.exception import GitlintError +from gitlint.utils import FILE_ENCODING def handle_option_error(func): @@ -31,7 +33,7 @@ class LintConfigError(GitlintError): pass -class LintConfig: # pylint: disable=too-many-instance-attributes +class LintConfig: """Class representing gitlint configuration. Contains active config as well as number of methods to easily get/set the config. """ @@ -105,7 +107,7 @@ class LintConfig: # pylint: disable=too-many-instance-attributes @handle_option_error def verbosity(self, value): self._verbosity.set(value) - if self.verbosity < 0 or self.verbosity > 3: + if self.verbosity < 0 or self.verbosity > 3: # noqa: PLR2004 (Magic value used in comparison) raise LintConfigError("Option 'verbosity' must be set between 0 and 3") @property @@ -294,7 +296,7 @@ class LintConfig: # pylint: disable=too-many-instance-attributes if not hasattr(self, attr_name) or attr_name[0] == "_": raise LintConfigError(f"'{option_name}' is not a valid gitlint option") - # else: + # else setattr(self, attr_name, option_value) def __eq__(self, other): @@ -384,7 +386,7 @@ class RuleCollection: """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 + for rule in list(self._rules.values()): if hasattr(rule, attr_name) and (getattr(rule, attr_name) == attr_val): del self._rules[rule.id] @@ -466,7 +468,7 @@ class LintConfigBuilder: try: parser = ConfigParser() - with open(filename, encoding=DEFAULT_ENCODING) as config_file: + with open(filename, encoding=FILE_ENCODING) as config_file: parser.read_file(config_file, filename) for section_name in parser.sections(): @@ -528,14 +530,15 @@ class LintConfigBuilder: for section_name, section_dict in self._config_blueprint.items(): for option_name, option_value in section_dict.items(): + qualified_section_name = section_name # Skip over the general section, as we've already done that above - if section_name != "general": + if qualified_section_name != "general": # If the section name contains a colon (:), then this section is defining a Named Rule # Which means we need to instantiate that Named Rule in the config. if self.RULE_QUALIFIER_SYMBOL in section_name: - section_name = self._add_named_rule(config, section_name) + qualified_section_name = self._add_named_rule(config, qualified_section_name) - config.set_rule_option(section_name, option_name, option_value) + config.set_rule_option(qualified_section_name, option_name, option_value) return config diff --git a/gitlint-core/gitlint/contrib/rules/authors_commit.py b/gitlint-core/gitlint/contrib/rules/authors_commit.py index ce11663..5c4a150 100644 --- a/gitlint-core/gitlint/contrib/rules/authors_commit.py +++ b/gitlint-core/gitlint/contrib/rules/authors_commit.py @@ -2,7 +2,6 @@ import re from pathlib import Path from typing import Tuple - from gitlint.rules import CommitRule, RuleViolation diff --git a/gitlint-core/gitlint/deprecation.py b/gitlint-core/gitlint/deprecation.py index bf13460..b7c2f42 100644 --- a/gitlint-core/gitlint/deprecation.py +++ b/gitlint-core/gitlint/deprecation.py @@ -1,6 +1,5 @@ import logging - LOG = logging.getLogger("gitlint.deprecated") DEPRECATED_LOG_FORMAT = "%(levelname)s: %(message)s" diff --git a/gitlint-core/gitlint/display.py b/gitlint-core/gitlint/display.py index d21b6c3..1de8d08 100644 --- a/gitlint-core/gitlint/display.py +++ b/gitlint-core/gitlint/display.py @@ -1,4 +1,4 @@ -from sys import stdout, stderr +from sys import stderr, stdout class Display: @@ -17,20 +17,20 @@ class Display: if self.config.verbosity >= verbosity: stream.write(message + "\n") - def v(self, message, exact=False): # pylint: disable=invalid-name + def v(self, message, exact=False): self._output(message, 1, exact, stdout) - def vv(self, message, exact=False): # pylint: disable=invalid-name + def vv(self, message, exact=False): self._output(message, 2, exact, stdout) - def vvv(self, message, exact=False): # pylint: disable=invalid-name + def vvv(self, message, exact=False): self._output(message, 3, exact, stdout) - def e(self, message, exact=False): # pylint: disable=invalid-name + def e(self, message, exact=False): self._output(message, 1, exact, stderr) - def ee(self, message, exact=False): # pylint: disable=invalid-name + def ee(self, message, exact=False): self._output(message, 2, exact, stderr) - def eee(self, message, exact=False): # pylint: disable=invalid-name + def eee(self, message, exact=False): self._output(message, 3, exact, stderr) diff --git a/gitlint-core/gitlint/exception.py b/gitlint-core/gitlint/exception.py index bcba54e..d1e8c9c 100644 --- a/gitlint-core/gitlint/exception.py +++ b/gitlint-core/gitlint/exception.py @@ -1,4 +1,2 @@ class GitlintError(Exception): """Based Exception class for all gitlint exceptions""" - - pass diff --git a/gitlint-core/gitlint/git.py b/gitlint-core/gitlint/git.py index 4b292f0..6612a7d 100644 --- a/gitlint-core/gitlint/git.py +++ b/gitlint-core/gitlint/git.py @@ -5,13 +5,12 @@ from pathlib import Path import arrow from gitlint import shell as sh +from gitlint.cache import PropertyCache, cache +from gitlint.exception import GitlintError # import exceptions separately, this makes it a little easier to mock them out in the unit tests from gitlint.shell import CommandNotFound, ErrorReturnCode -from gitlint.cache import PropertyCache, cache -from gitlint.exception import GitlintError - # For now, the git date format we use is fixed, but technically this format is determined by `git config log.date` # We should fix this at some point :-) GIT_TIMEFORMAT = "YYYY-MM-DD HH:mm:ss Z" @@ -22,8 +21,6 @@ LOG = logging.getLogger(__name__) class GitContextError(GitlintError): """Exception indicating there is an issue with the git context""" - pass - class GitNotInstalledError(GitContextError): def __init__(self): @@ -46,7 +43,7 @@ def _git(*command_parts, **kwargs): git_kwargs.update(kwargs) try: LOG.debug(command_parts) - result = sh.git(*command_parts, **git_kwargs) # pylint: disable=unexpected-keyword-arg + result = sh.git(*command_parts, **git_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 @@ -80,7 +77,7 @@ def git_commentchar(repository_path=None): """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: commentchar = "#" return commentchar.replace("\n", "") @@ -174,11 +171,6 @@ class GitChangedFileStats: 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. @@ -193,7 +185,7 @@ class GitCommit: message, sha=None, date=None, - author_name=None, # pylint: disable=too-many-arguments + author_name=None, author_email=None, parents=None, changed_files_stats=None, @@ -289,7 +281,7 @@ class LocalGitCommit(GitCommit, PropertyCache): startup time and reduces gitlint's memory footprint. """ - def __init__(self, context, sha): # pylint: disable=super-init-not-called + def __init__(self, context, sha): PropertyCache.__init__(self) self.context = context self.sha = sha @@ -382,7 +374,7 @@ class StagedLocalGitCommit(GitCommit, PropertyCache): information. """ - def __init__(self, context, commit_message): # pylint: disable=super-init-not-called + def __init__(self, context, commit_message): PropertyCache.__init__(self) self.context = context self.message = commit_message diff --git a/gitlint-core/gitlint/hooks.py b/gitlint-core/gitlint/hooks.py index 78c5e46..98ded18 100644 --- a/gitlint-core/gitlint/hooks.py +++ b/gitlint-core/gitlint/hooks.py @@ -1,10 +1,10 @@ -import shutil import os +import shutil import stat -from gitlint.utils import DEFAULT_ENCODING -from gitlint.git import git_hooks_dir from gitlint.exception import GitlintError +from gitlint.git import git_hooks_dir +from gitlint.utils import FILE_ENCODING COMMIT_MSG_HOOK_SRC_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "files", "commit-msg") COMMIT_MSG_HOOK_DST_PATH = "commit-msg" @@ -52,9 +52,9 @@ class GitHookInstaller: if not os.path.exists(dest_path): raise GitHookInstallerError(f"There is no commit-msg hook present in {dest_path}.") - with open(dest_path, encoding=DEFAULT_ENCODING) as fp: + with open(dest_path, encoding=FILE_ENCODING) as fp: lines = fp.readlines() - if len(lines) < 2 or lines[1] != GITLINT_HOOK_IDENTIFIER: + if len(lines) < 2 or lines[1] != GITLINT_HOOK_IDENTIFIER: # noqa: PLR2004 (Magic value used in comparison) 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." diff --git a/gitlint-core/gitlint/lint.py b/gitlint-core/gitlint/lint.py index 3bc1945..420d3ad 100644 --- a/gitlint-core/gitlint/lint.py +++ b/gitlint-core/gitlint/lint.py @@ -1,7 +1,7 @@ -# pylint: disable=logging-not-lazy import logging -from gitlint import rules as gitlint_rules + from gitlint import display +from gitlint import rules as gitlint_rules from gitlint.deprecation import Deprecation LOG = logging.getLogger(__name__) diff --git a/gitlint-core/gitlint/options.py b/gitlint-core/gitlint/options.py index 50565ea..ff7d9f1 100644 --- a/gitlint-core/gitlint/options.py +++ b/gitlint-core/gitlint/options.py @@ -1,6 +1,6 @@ -from abc import abstractmethod import os import re +from abc import abstractmethod from gitlint.exception import GitlintError @@ -37,7 +37,6 @@ class RuleOption: @abstractmethod def set(self, value): """Validates and sets the option's value""" - pass # pragma: no cover def __str__(self): return f"({self.name}: {self.value} ({self.description}))" diff --git a/gitlint-core/gitlint/rule_finder.py b/gitlint-core/gitlint/rule_finder.py index 11665cf..810faa9 100644 --- a/gitlint-core/gitlint/rule_finder.py +++ b/gitlint-core/gitlint/rule_finder.py @@ -1,10 +1,10 @@ import fnmatch +import importlib import inspect import os import sys -import importlib -from gitlint import rules, options +from gitlint import options, rules def find_rule_classes(extra_path): @@ -55,7 +55,7 @@ def find_rule_classes(extra_path): importlib.import_module(module) except Exception as e: - raise rules.UserRuleError(f"Error while importing extra-path module '{module}': {e}") + raise rules.UserRuleError(f"Error while importing extra-path module '{module}': {e}") from e # Find all rule classes in the module. We do this my inspecting all members of the module and checking # 1) is it a class, if not, skip @@ -67,11 +67,7 @@ def find_rule_classes(extra_path): 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) - ) + and (issubclass(clazz, (rules.LineRule, rules.CommitRule, rules.ConfigurationRule))) ] ) @@ -82,7 +78,7 @@ def find_rule_classes(extra_path): return rule_classes -def assert_valid_rule_class(clazz, rule_type="User-defined"): # pylint: disable=too-many-branches +def assert_valid_rule_class(clazz, rule_type="User-defined"): # noqa: PLR0912 (too many branches) """ Asserts that a given rule clazz is valid by checking a number of its properties: - Rules must extend from LineRule, CommitRule or ConfigurationRule @@ -97,11 +93,7 @@ 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) - ): + if not issubclass(clazz, (rules.LineRule, rules.CommitRule, rules.ConfigurationRule)): msg = ( f"{rule_type} rule class '{clazz.__name__}' " f"must extend from {rules.CommitRule.__module__}.{rules.LineRule.__name__}, " @@ -142,17 +134,18 @@ def assert_valid_rule_class(clazz, rule_type="User-defined"): # pylint: disable # 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 issubclass(clazz, (rules.LineRule, 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): + elif issubclass(clazz, rules.ConfigurationRule): # noqa: SIM102 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 issubclass(clazz, rules.LineRule): # noqa: SIM102 if clazz.target not in [rules.CommitMessageTitle, rules.CommitMessageBody]: msg = ( f"The target attribute of the {rule_type.lower()} LineRule class '{clazz.__name__}' " diff --git a/gitlint-core/gitlint/rules.py b/gitlint-core/gitlint/rules.py index 6d486a5..ca4a05b 100644 --- a/gitlint-core/gitlint/rules.py +++ b/gitlint-core/gitlint/rules.py @@ -1,11 +1,10 @@ -# pylint: disable=inconsistent-return-statements import copy import logging import re -from gitlint.options import IntOption, BoolOption, StrOption, ListOption, RegexOption -from gitlint.exception import GitlintError from gitlint.deprecation import Deprecation +from gitlint.exception import GitlintError +from gitlint.options import BoolOption, IntOption, ListOption, RegexOption, StrOption class Rule: @@ -50,40 +49,28 @@ class Rule: class ConfigurationRule(Rule): """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""" - pass - class LineRule(Rule): """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 (e.g. commit message title, commit message body). Each LineRule MUST have a target specified.""" - pass - class CommitMessageTitle(LineRuleTarget): """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""" - pass - class RuleViolation: """Class representing a violation of a rule. I.e.: When a rule is broken, the rule will instantiate this class @@ -107,8 +94,6 @@ class RuleViolation: class UserRuleError(GitlintError): """Error used to indicate that an error occurred while trying to load a user rule""" - pass - class MaxLineLength(LineRule): name = "max-line-length" @@ -305,7 +290,7 @@ class BodyMissing(CommitRule): # ignore merges when option tells us to, which may have no body 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(): # noqa: PLR2004 (Magic value) return [RuleViolation(self.id, "Body message is missing", None, 3)] @@ -319,7 +304,7 @@ class BodyChangedFileMention(CommitRule): 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: + if needs_mentioned_file in commit.changed_files: # noqa: SIM102 if needs_mentioned_file not in " ".join(commit.message.body): violation_message = f"Body does not mention changed file '{needs_mentioned_file}'" violations.append(RuleViolation(self.id, violation_message, None, len(commit.message.body) + 1)) @@ -370,7 +355,7 @@ class AuthorValidEmail(CommitRule): # 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: + if self.options["regex"].value.pattern == self.DEFAULT_AUTHOR_VALID_EMAIL_REGEX: regex_method = self.options["regex"].value.search else: regex_method = Deprecation.get_regex_method(self, self.options["regex"]) @@ -458,7 +443,7 @@ class IgnoreBodyLines(ConfigurationRule): new_body.append(line) commit.message.body = new_body - commit.message.full = "\n".join([commit.message.title] + new_body) + commit.message.full = "\n".join([commit.message.title, *new_body]) class IgnoreByAuthorName(ConfigurationRule): @@ -474,6 +459,17 @@ class IgnoreByAuthorName(ConfigurationRule): if not self.options["regex"].value: return + # If commit.author_name is not available, log warning and return + if commit.author_name is None: + warning_msg = ( + "%s - %s: skipping - commit.author_name unknown. " + "Suggested fix: Use the --staged flag (or set general.staged=True in .gitlint). " + "More details: https://jorisroovers.com/gitlint/configuration/#staged" + ) + + self.log.warning(warning_msg, self.name, self.id) + return + regex_method = Deprecation.get_regex_method(self, self.options["regex"]) if regex_method(commit.author_name): diff --git a/gitlint-core/gitlint/shell.py b/gitlint-core/gitlint/shell.py index c378c1c..fddece0 100644 --- a/gitlint-core/gitlint/shell.py +++ b/gitlint-core/gitlint/shell.py @@ -5,7 +5,8 @@ capabilities wrt dealing with more edge-case environments on *nix systems that a """ import subprocess -from gitlint.utils import USE_SH_LIB, DEFAULT_ENCODING + +from gitlint.utils import TERMINAL_ENCODING, USE_SH_LIB def shell(cmd): @@ -15,17 +16,17 @@ def shell(cmd): 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 + from sh import ( + CommandNotFound, + ErrorReturnCode, + git, + ) else: class CommandNotFound(Exception): """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""" @@ -42,14 +43,12 @@ else: class ErrorReturnCode(ShResult, Exception): """ShResult subclass for unexpected results (acts as an exception).""" - pass - def git(*command_parts, **kwargs): """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): @@ -65,7 +64,7 @@ else: raise CommandNotFound from e exit_code = p.returncode - stdout = result[0].decode(DEFAULT_ENCODING) + stdout = result[0].decode(TERMINAL_ENCODING) stderr = result[1] # 'sh' does not decode the stderr bytes to unicode full_cmd = "" if args is None else " ".join(args) diff --git a/gitlint-core/gitlint/tests/base.py b/gitlint-core/gitlint/tests/base.py index 710efe2..3899a5f 100644 --- a/gitlint-core/gitlint/tests/base.py +++ b/gitlint-core/gitlint/tests/base.py @@ -5,15 +5,15 @@ import os import re import shutil import tempfile - import unittest - +from pathlib import Path from unittest.mock import patch from gitlint.config import LintConfig -from gitlint.deprecation import Deprecation, LOG as DEPRECATION_LOG -from gitlint.git import GitContext, GitChangedFileStats -from gitlint.utils import LOG_FORMAT, DEFAULT_ENCODING +from gitlint.deprecation import LOG as DEPRECATION_LOG +from gitlint.deprecation import Deprecation +from gitlint.git import GitChangedFileStats, GitContext +from gitlint.utils import FILE_ENCODING, LOG_FORMAT EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING = ( "WARNING: gitlint.deprecated.regex_style_search {0} - {1}: gitlint will be switching from using " @@ -30,10 +30,28 @@ class BaseTestCase(unittest.TestCase): # In case of assert failures, print the full error message maxDiff = None + # Working directory in which tests in this class are executed + working_dir = None + # Originally working dir when the test was started + original_working_dir = None + SAMPLES_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "samples") EXPECTED_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "expected") GITLINT_USE_SH_LIB = os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]") + @classmethod + def setUpClass(cls): + # Run tests a temporary directory to shield them from any local git config + cls.original_working_dir = os.getcwd() + cls.working_dir = tempfile.mkdtemp() + os.chdir(cls.working_dir) + + @classmethod + def tearDownClass(cls): + # Go back to original working dir and remove our temp working dir + os.chdir(cls.original_working_dir) + shutil.rmtree(cls.working_dir) + def setUp(self): self.logcapture = LogCapture() self.logcapture.setFormatter(logging.Formatter(LOG_FORMAT)) @@ -77,9 +95,7 @@ class BaseTestCase(unittest.TestCase): def get_sample(filename=""): """Read and return the contents of a file in gitlint/tests/samples""" sample_path = BaseTestCase.get_sample_path(filename) - with open(sample_path, encoding=DEFAULT_ENCODING) as content: - sample = content.read() - return sample + return Path(sample_path).read_text(encoding=FILE_ENCODING) @staticmethod def patch_input(side_effect): @@ -93,8 +109,7 @@ class BaseTestCase(unittest.TestCase): """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 open(expected_path, encoding=DEFAULT_ENCODING) as content: - expected = content.read() + expected = Path(expected_path).read_text(encoding=FILE_ENCODING) if variable_dict: expected = expected.format(**variable_dict) @@ -150,22 +165,24 @@ class BaseTestCase(unittest.TestCase): self.logcapture.clear() @contextlib.contextmanager - def assertRaisesMessage(self, expected_exception, expected_msg): # pylint: disable=invalid-name + def assertRaisesMessage(self, expected_exception, expected_msg): """Asserts an exception has occurred with a given error message""" try: yield except expected_exception as exc: exception_msg = str(exc) - if exception_msg != expected_msg: + if exception_msg != expected_msg: # pragma: nocover error = f"Right exception, wrong message:\n got: {exception_msg}\n expected: {expected_msg}" - raise self.fail(error) + raise self.fail(error) from exc # else: everything is fine, just return return - except Exception as exc: - raise self.fail(f"Expected '{expected_exception.__name__}' got '{exc.__class__.__name__}'") + except Exception as exc: # pragma: nocover + raise self.fail(f"Expected '{expected_exception.__name__}' got '{exc.__class__.__name__}'") from exc # No exception raised while we expected one - raise self.fail(f"Expected to raise {expected_exception.__name__}, didn't get an exception at all") + raise self.fail( + f"Expected to raise {expected_exception.__name__}, didn't get an exception at all" + ) # pragma: nocover def object_equality_test(self, obj, attr_list, ctor_kwargs=None): """Helper function to easily implement object equality tests. diff --git a/gitlint-core/gitlint/tests/cli/test_cli.py b/gitlint-core/gitlint/tests/cli/test_cli.py index d18efe9..c006375 100644 --- a/gitlint-core/gitlint/tests/cli/test_cli.py +++ b/gitlint-core/gitlint/tests/cli/test_cli.py @@ -1,22 +1,15 @@ -import io import os -import sys import platform - -import arrow - +import sys from io import StringIO - -from click.testing import CliRunner - from unittest.mock import patch +import arrow +from click.testing import CliRunner +from gitlint import __version__, cli from gitlint.shell import CommandNotFound - from gitlint.tests.base import BaseTestCase -from gitlint import cli -from gitlint import __version__ -from gitlint.utils import DEFAULT_ENCODING +from gitlint.utils import FILE_ENCODING, TERMINAL_ENCODING class CLITests(BaseTestCase): @@ -46,7 +39,8 @@ class CLITests(BaseTestCase): "gitlint_version": __version__, "GITLINT_USE_SH_LIB": BaseTestCase.GITLINT_USE_SH_LIB, "target": os.path.realpath(os.getcwd()), - "DEFAULT_ENCODING": DEFAULT_ENCODING, + "TERMINAL_ENCODING": TERMINAL_ENCODING, + "FILE_ENCODING": FILE_ENCODING, } def test_version(self): @@ -107,6 +101,40 @@ class CLITests(BaseTestCase): @patch("gitlint.cli.get_stdin_data", return_value=False) @patch("gitlint.git.sh") + def test_lint_multiple_commits_csv(self, sh, _): + """Test for --commits 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\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> + # 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> + # 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> + ] + # fmt: on + + with patch("gitlint.display.stderr", new=StringIO()) as stderr: + result = self.cli.invoke(cli.cli, ["--commits", "6f29bf81,25053cce,4da2656b"]) + self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli/test_lint_multiple_commits_csv_1")) + self.assertEqual(result.exit_code, 3) + + @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""" @@ -225,8 +253,7 @@ class CLITests(BaseTestCase): self.assertEqual(result.exit_code, 2) @patch("gitlint.cli.get_stdin_data", return_value=False) - @patch("gitlint.git.sh") - def test_lint_commit_negative(self, sh, _): + def test_lint_commit_negative(self, _): """Negative test for --commit option""" # Try using --commit and --commits at the same time (not allowed) @@ -298,6 +325,11 @@ class CLITests(BaseTestCase): self.assertEqual(result.output, "") expected_kwargs = self.get_system_info_dict() + changed_files_stats = ( + f" {os.path.join('commit-1', 'file-1')}: 1 additions, 5 deletions\n" + f" {os.path.join('commit-1', 'file-2')}: 8 additions, 9 deletions" + ) + expected_kwargs.update({"changed_files_stats": changed_files_stats}) expected_logs = self.get_expected("cli/test_cli/test_lint_staged_stdin_2", expected_kwargs) self.assert_logged(expected_logs) @@ -318,7 +350,7 @@ class CLITests(BaseTestCase): with self.tempdir() as tmpdir: msg_filename = os.path.join(tmpdir, "msg") - with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f: + with open(msg_filename, "w", encoding=FILE_ENCODING) as f: f.write("WIP: msg-filename tïtle\n") with patch("gitlint.display.stderr", new=StringIO()) as stderr: @@ -328,6 +360,11 @@ class CLITests(BaseTestCase): self.assertEqual(result.output, "") expected_kwargs = self.get_system_info_dict() + changed_files_stats = ( + f" {os.path.join('commit-1', 'file-1')}: 3 additions, 4 deletions\n" + f" {os.path.join('commit-1', 'file-2')}: 4 additions, 7 deletions" + ) + expected_kwargs.update({"changed_files_stats": changed_files_stats}) expected_logs = self.get_expected("cli/test_cli/test_lint_staged_msg_filename_2", expected_kwargs) self.assert_logged(expected_logs) @@ -368,7 +405,7 @@ class CLITests(BaseTestCase): with self.tempdir() as tmpdir: msg_filename = os.path.join(tmpdir, "msg") - with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f: + with open(msg_filename, "w", encoding=FILE_ENCODING) as f: f.write("Commït title\n") with patch("gitlint.display.stderr", new=StringIO()) as stderr: @@ -458,6 +495,25 @@ class CLITests(BaseTestCase): self.assertEqual(result.exit_code, 6) expected_kwargs = self.get_system_info_dict() + changed_files_stats1 = ( + f" {os.path.join('commit-1', 'file-1')}: 5 additions, 8 deletions\n" + f" {os.path.join('commit-1', 'file-2')}: 2 additions, 9 deletions" + ) + changed_files_stats2 = ( + f" {os.path.join('commit-2', 'file-1')}: 5 additions, 8 deletions\n" + f" {os.path.join('commit-2', 'file-2')}: 7 additions, 9 deletions" + ) + changed_files_stats3 = ( + f" {os.path.join('commit-3', 'file-1')}: 1 additions, 4 deletions\n" + f" {os.path.join('commit-3', 'file-2')}: 3 additions, 4 deletions" + ) + expected_kwargs.update( + { + "changed_files_stats1": changed_files_stats1, + "changed_files_stats2": changed_files_stats2, + "changed_files_stats3": changed_files_stats3, + } + ) 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) @@ -548,7 +604,7 @@ class CLITests(BaseTestCase): # Non existing file config_path = self.get_sample_path("föo") result = self.cli.invoke(cli.cli, ["--config", config_path]) - expected_string = f"Error: Invalid value for '-C' / '--config': File '{config_path}' does not exist." + expected_string = f"Error: Invalid value for '-C' / '--config': File {config_path!r} does not exist." self.assertEqual(result.output.split("\n")[3], expected_string) self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE) @@ -569,7 +625,7 @@ class CLITests(BaseTestCase): # 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." + expected_string = f"Error: Invalid value for '-C' / '--config': File {config_path!r} does not exist." self.assertEqual(result.output.split("\n")[3], expected_string) self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE) @@ -578,6 +634,11 @@ class CLITests(BaseTestCase): result = self.cli.invoke(cli.cli, env={"GITLINT_CONFIG": config_path}) self.assertEqual(result.exit_code, self.CONFIG_ERROR_CODE) + def test_config_error(self): + result = self.cli.invoke(cli.cli, ["-c", "foo.bar=hur"]) + self.assertEqual(result.output, "Config Error: No such rule 'foo'\n") + 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""" @@ -602,7 +663,7 @@ class CLITests(BaseTestCase): target_path = self.get_sample_path(os.path.join("config", "gitlintconfig")) result = self.cli.invoke(cli.cli, ["--target", target_path]) self.assertEqual(result.exit_code, self.USAGE_ERROR_CODE) - expected_msg = f"Error: Invalid value for '--target': Directory '{target_path}' is a file." + expected_msg = f"Error: Invalid value for '--target': Directory {target_path!r} is a file." self.assertEqual(result.output.split("\n")[3], expected_msg) @patch("gitlint.config.LintConfigGenerator.generate_config") diff --git a/gitlint-core/gitlint/tests/cli/test_cli_hooks.py b/gitlint-core/gitlint/tests/cli/test_cli_hooks.py index d4311c6..c9e4eba 100644 --- a/gitlint-core/gitlint/tests/cli/test_cli_hooks.py +++ b/gitlint-core/gitlint/tests/cli/test_cli_hooks.py @@ -1,18 +1,12 @@ -import io -from io import StringIO import os - -from click.testing import CliRunner - +from io import StringIO from unittest.mock import patch -from gitlint.tests.base import BaseTestCase -from gitlint import cli -from gitlint import hooks -from gitlint import config +from click.testing import CliRunner +from gitlint import cli, config, hooks from gitlint.shell import ErrorReturnCode - -from gitlint.utils import DEFAULT_ENCODING +from gitlint.tests.base import BaseTestCase +from gitlint.utils import FILE_ENCODING class CLIHookTests(BaseTestCase): @@ -108,7 +102,7 @@ class CLIHookTests(BaseTestCase): with self.tempdir() as tmpdir: msg_filename = os.path.join(tmpdir, "hür") - with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f: + with open(msg_filename, "w", encoding=FILE_ENCODING) as f: f.write("WIP: tïtle\n") with patch("gitlint.display.stderr", new=StringIO()) as stderr: @@ -134,68 +128,65 @@ class CLIHookTests(BaseTestCase): # 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.tempdir() as tmpdir: - msg_filename = os.path.realpath(os.path.join(tmpdir, "hür")) - 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: - result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"]) - self.assertEqual( - result.output, - self.get_expected( - "cli/test_cli_hooks/test_hook_edit_1_stdout", {"commit_msg": commit_messages[i]} - ), - ) - expected = self.get_expected( - "cli/test_cli_hooks/test_hook_edit_1_stderr", {"commit_msg": commit_messages[i]} - ) - self.assertEqual(stderr.getvalue(), expected) - - # exit code = number of violations - self.assertEqual(result.exit_code, 2) - - shell.assert_called_with(expected_editors[i] + " " + msg_filename) - self.assert_log_contains("DEBUG: gitlint.cli run-hook: editing commit message") - self.assert_log_contains(f"DEBUG: gitlint.cli run-hook: {expected_editors[i]} {msg_filename}") + with self.patch_input(["e", "e", "n"]), self.tempdir() as tmpdir: + msg_filename = os.path.realpath(os.path.join(tmpdir, "hür")) + with open(msg_filename, "w", encoding=FILE_ENCODING) as f: + f.write(commit_messages[i] + "\n") + + with patch("gitlint.display.stderr", new=StringIO()) as stderr: + result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"]) + self.assertEqual( + result.output, + self.get_expected( + "cli/test_cli_hooks/test_hook_edit_1_stdout", {"commit_msg": commit_messages[i]} + ), + ) + expected = self.get_expected( + "cli/test_cli_hooks/test_hook_edit_1_stderr", {"commit_msg": commit_messages[i]} + ) + self.assertEqual(stderr.getvalue(), expected) + + # exit code = number of violations + self.assertEqual(result.exit_code, 2) + + shell.assert_called_with(expected_editors[i] + " " + msg_filename) + self.assert_log_contains("DEBUG: gitlint.cli run-hook: editing commit message") + self.assert_log_contains(f"DEBUG: gitlint.cli run-hook: {expected_editors[i]} {msg_filename}") def test_run_hook_no(self): """Test for run-hook subcommand, answering 'n(o)' after commit-hook""" - with self.patch_input(["n"]): - with self.tempdir() as tmpdir: - msg_filename = os.path.join(tmpdir, "hür") - with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f: - f.write("WIP: höok no\n") + with self.patch_input(["n"]), self.tempdir() as tmpdir: + msg_filename = os.path.join(tmpdir, "hür") + with open(msg_filename, "w", encoding=FILE_ENCODING) as f: + f.write("WIP: höok no\n") - with patch("gitlint.display.stderr", new=StringIO()) as stderr: - result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"]) - 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")) + with patch("gitlint.display.stderr", new=StringIO()) as stderr: + result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"]) + self.assertEqual(result.output, self.get_expected("cli/test_cli_hooks/test_hook_no_1_stdout")) + self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli_hooks/test_hook_no_1_stderr")) - # We decided not to keep the commit message: hook returns number of violations (>0) - # This will cause git to abort the commit - self.assertEqual(result.exit_code, 2) - self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message declined") + # We decided not to keep the commit message: hook returns number of violations (>0) + # This will cause git to abort the commit + self.assertEqual(result.exit_code, 2) + self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message declined") def test_run_hook_yes(self): """Test for run-hook subcommand, answering 'y(es)' after commit-hook""" - with self.patch_input(["y"]): - with self.tempdir() as tmpdir: - msg_filename = os.path.join(tmpdir, "hür") - with open(msg_filename, "w", encoding=DEFAULT_ENCODING) as f: - f.write("WIP: höok yes\n") + with self.patch_input(["y"]), self.tempdir() as tmpdir: + msg_filename = os.path.join(tmpdir, "hür") + with open(msg_filename, "w", encoding=FILE_ENCODING) as f: + f.write("WIP: höok yes\n") - with patch("gitlint.display.stderr", new=StringIO()) as stderr: - result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"]) - 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")) + with patch("gitlint.display.stderr", new=StringIO()) as stderr: + result = self.cli.invoke(cli.cli, ["--msg-filename", msg_filename, "run-hook"]) + self.assertEqual(result.output, self.get_expected("cli/test_cli_hooks/test_hook_yes_1_stdout")) + self.assertEqual(stderr.getvalue(), self.get_expected("cli/test_cli_hooks/test_hook_yes_1_stderr")) - # Exit code is 0 because we decide to keep the commit message - # This will cause git to keep the commit - self.assertEqual(result.exit_code, 0) - self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message accepted") + # Exit code is 0 because we decide to keep the commit message + # This will cause git to keep the commit + self.assertEqual(result.exit_code, 0) + self.assert_log_contains("DEBUG: gitlint.cli run-hook: commit message accepted") @patch("gitlint.cli.get_stdin_data", return_value=False) @patch("gitlint.git.sh") @@ -207,7 +198,8 @@ class CLIHookTests(BaseTestCase): 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_kwargs = {"git_repo": os.path.realpath(os.getcwd())} + expected = self.get_expected("cli/test_cli_hooks/test_run_hook_negative_1", expected_kwargs) self.assertEqual(result.output, expected) self.assertEqual(result.exit_code, self.GIT_CONTEXT_ERROR_CODE) @@ -276,11 +268,10 @@ class CLIHookTests(BaseTestCase): "commit-1-branch-1\ncommit-1-branch-2\n", ] - with self.patch_input(["e"]): - with patch("gitlint.display.stderr", new=StringIO()) as stderr: - result = self.cli.invoke(cli.cli, ["run-hook"]) - expected = self.get_expected("cli/test_cli_hooks/test_hook_local_commit_1_stderr") - self.assertEqual(stderr.getvalue(), expected) - self.assertEqual(result.output, self.get_expected("cli/test_cli_hooks/test_hook_local_commit_1_stdout")) - # If we can't edit the message, run-hook follows regular gitlint behavior and exit code = # violations - self.assertEqual(result.exit_code, 2) + with self.patch_input(["e"]), patch("gitlint.display.stderr", new=StringIO()) as stderr: + result = self.cli.invoke(cli.cli, ["run-hook"]) + expected = self.get_expected("cli/test_cli_hooks/test_hook_local_commit_1_stderr") + self.assertEqual(stderr.getvalue(), expected) + self.assertEqual(result.output, self.get_expected("cli/test_cli_hooks/test_hook_local_commit_1_stdout")) + # If we can't edit the message, run-hook follows regular gitlint behavior and exit code = # violations + self.assertEqual(result.exit_code, 2) diff --git a/gitlint-core/gitlint/tests/config/test_config.py b/gitlint-core/gitlint/tests/config/test_config.py index 852bf75..439fd93 100644 --- a/gitlint-core/gitlint/tests/config/test_config.py +++ b/gitlint-core/gitlint/tests/config/test_config.py @@ -1,8 +1,12 @@ from unittest.mock import patch -from gitlint import rules -from gitlint.config import LintConfig, LintConfigError, LintConfigGenerator, GITLINT_CONFIG_TEMPLATE_SRC_PATH -from gitlint import options +from gitlint import options, rules +from gitlint.config import ( + GITLINT_CONFIG_TEMPLATE_SRC_PATH, + LintConfig, + LintConfigError, + LintConfigGenerator, +) from gitlint.tests.base import BaseTestCase @@ -166,7 +170,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): # noqa: SIM117 with self.assertRaisesMessage(LintConfigError, str(side_effect)): config.contrib = "contrib-title-conventional-commits" diff --git a/gitlint-core/gitlint/tests/config/test_config_builder.py b/gitlint-core/gitlint/tests/config/test_config_builder.py index dfb77cd..ac2a896 100644 --- a/gitlint-core/gitlint/tests/config/test_config_builder.py +++ b/gitlint-core/gitlint/tests/config/test_config_builder.py @@ -1,10 +1,8 @@ import copy -from gitlint.tests.base import BaseTestCase - -from gitlint.config import LintConfig, LintConfigBuilder, LintConfigError - from gitlint import rules +from gitlint.config import LintConfig, LintConfigBuilder, LintConfigError +from gitlint.tests.base import BaseTestCase class LintConfigBuilderTests(BaseTestCase): @@ -256,8 +254,7 @@ class LintConfigBuilderTests(BaseTestCase): my_rule.options["regex"].set("wrong") def test_named_rules_negative(self): - # T7 = title-match-regex - # Invalid rule name + # Invalid rule name (T7 = title-match-regex) for invalid_name in ["", " ", " ", "\t", "\n", "å b", "å:b", "åb:", ":åb"]: config_builder = LintConfigBuilder() config_builder.set_option(f"T7:{invalid_name}", "regex", "tëst") diff --git a/gitlint-core/gitlint/tests/config/test_config_precedence.py b/gitlint-core/gitlint/tests/config/test_config_precedence.py index 22197e8..a7f94cf 100644 --- a/gitlint-core/gitlint/tests/config/test_config_precedence.py +++ b/gitlint-core/gitlint/tests/config/test_config_precedence.py @@ -1,12 +1,10 @@ from io import StringIO - -from click.testing import CliRunner - from unittest.mock import patch -from gitlint.tests.base import BaseTestCase +from click.testing import CliRunner from gitlint import cli from gitlint.config import LintConfigBuilder +from gitlint.tests.base import BaseTestCase class LintConfigPrecedenceTests(BaseTestCase): diff --git a/gitlint-core/gitlint/tests/config/test_rule_collection.py b/gitlint-core/gitlint/tests/config/test_rule_collection.py index ea7039f..2cb0e5c 100644 --- a/gitlint-core/gitlint/tests/config/test_rule_collection.py +++ b/gitlint-core/gitlint/tests/config/test_rule_collection.py @@ -1,4 +1,5 @@ from collections import OrderedDict + from gitlint import rules from gitlint.config import RuleCollection from gitlint.tests.base import BaseTestCase diff --git a/gitlint-core/gitlint/tests/contrib/rules/test_authors_commit.py b/gitlint-core/gitlint/tests/contrib/rules/test_authors_commit.py index 5ea9d8f..2bad2ed 100644 --- a/gitlint-core/gitlint/tests/contrib/rules/test_authors_commit.py +++ b/gitlint-core/gitlint/tests/contrib/rules/test_authors_commit.py @@ -1,10 +1,10 @@ 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.config import LintConfig from gitlint.contrib.rules.authors_commit import AllowedAuthors +from gitlint.rules import RuleViolation +from gitlint.tests.base import BaseTestCase class ContribAuthorsCommitTests(BaseTestCase): @@ -101,6 +101,5 @@ class ContribAuthorsCommitTests(BaseTestCase): return_value=False, ) def test_read_authors_file_missing_file(self, _mock_iterdir): - with self.assertRaises(FileNotFoundError) as err: + with self.assertRaisesMessage(FileNotFoundError, "No AUTHORS file found!"): AllowedAuthors._read_authors_from_file(self.gitcontext) - self.assertEqual(err.exception.args[0], "AUTHORS file not found") diff --git a/gitlint-core/gitlint/tests/contrib/rules/test_conventional_commit.py b/gitlint-core/gitlint/tests/contrib/rules/test_conventional_commit.py index 7ce9c89..cbab684 100644 --- a/gitlint-core/gitlint/tests/contrib/rules/test_conventional_commit.py +++ b/gitlint-core/gitlint/tests/contrib/rules/test_conventional_commit.py @@ -1,7 +1,7 @@ -from gitlint.tests.base import BaseTestCase -from gitlint.rules import RuleViolation -from gitlint.contrib.rules.conventional_commit import ConventionalCommit from gitlint.config import LintConfig +from gitlint.contrib.rules.conventional_commit import ConventionalCommit +from gitlint.rules import RuleViolation +from gitlint.tests.base import BaseTestCase class ContribConventionalCommitTests(BaseTestCase): diff --git a/gitlint-core/gitlint/tests/contrib/rules/test_disallow_cleanup_commits.py b/gitlint-core/gitlint/tests/contrib/rules/test_disallow_cleanup_commits.py index 841640a..1983367 100644 --- a/gitlint-core/gitlint/tests/contrib/rules/test_disallow_cleanup_commits.py +++ b/gitlint-core/gitlint/tests/contrib/rules/test_disallow_cleanup_commits.py @@ -1,8 +1,7 @@ -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 +from gitlint.contrib.rules.disallow_cleanup_commits import DisallowCleanupCommits +from gitlint.rules import RuleViolation +from gitlint.tests.base import BaseTestCase class ContribDisallowCleanupCommitsTest(BaseTestCase): diff --git a/gitlint-core/gitlint/tests/contrib/rules/test_signedoff_by.py b/gitlint-core/gitlint/tests/contrib/rules/test_signedoff_by.py index 88ff1db..bf526a0 100644 --- a/gitlint-core/gitlint/tests/contrib/rules/test_signedoff_by.py +++ b/gitlint-core/gitlint/tests/contrib/rules/test_signedoff_by.py @@ -1,8 +1,7 @@ -from gitlint.tests.base import BaseTestCase -from gitlint.rules import RuleViolation -from gitlint.contrib.rules.signedoff_by import SignedOffBy - from gitlint.config import LintConfig +from gitlint.contrib.rules.signedoff_by import SignedOffBy +from gitlint.rules import RuleViolation +from gitlint.tests.base import BaseTestCase class ContribSignedOffByTests(BaseTestCase): diff --git a/gitlint-core/gitlint/tests/contrib/test_contrib_rules.py b/gitlint-core/gitlint/tests/contrib/test_contrib_rules.py index bd098c6..b0372d8 100644 --- a/gitlint-core/gitlint/tests/contrib/test_contrib_rules.py +++ b/gitlint-core/gitlint/tests/contrib/test_contrib_rules.py @@ -1,9 +1,9 @@ import os -from gitlint.tests.base import BaseTestCase +from gitlint import rule_finder, rules from gitlint.contrib import rules as contrib_rules +from gitlint.tests.base import BaseTestCase from gitlint.tests.contrib import rules as contrib_tests -from gitlint import rule_finder, rules class ContribRuleTests(BaseTestCase): diff --git a/gitlint-core/gitlint/tests/expected/cli/test_cli/test_debug_1 b/gitlint-core/gitlint/tests/expected/cli/test_cli/test_debug_1 index 4bd3b7d..046294c 100644 --- a/gitlint-core/gitlint/tests/expected/cli/test_cli/test_debug_1 +++ b/gitlint-core/gitlint/tests/expected/cli/test_cli/test_debug_1 @@ -4,7 +4,8 @@ DEBUG: gitlint.cli Python version: {python_version} DEBUG: gitlint.cli Git version: git version 1.2.3 DEBUG: gitlint.cli Gitlint version: {gitlint_version} DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} -DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING} +DEBUG: gitlint.cli TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} DEBUG: gitlint.cli Configuration config-path: {config_path} [GENERAL] @@ -88,8 +89,7 @@ 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 +{changed_files_stats1} ----------------------- DEBUG: gitlint.git ('log', '25053ccec5e28e1bb8f7551fdbb5ab213ada2401', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B') DEBUG: gitlint.lint Linting commit 25053ccec5e28e1bb8f7551fdbb5ab213ada2401 @@ -112,8 +112,7 @@ 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 +{changed_files_stats2} ----------------------- DEBUG: gitlint.git ('log', '4da2656b0dadc76c7ee3fd0243a96cb64007f125', '-1', '--pretty=%aN%x00%aE%x00%ai%x00%P%n%B') DEBUG: gitlint.lint Linting commit 4da2656b0dadc76c7ee3fd0243a96cb64007f125 @@ -135,7 +134,6 @@ 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 +{changed_files_stats3} ----------------------- DEBUG: gitlint.cli Exit Code = 6
\ No newline at end of file diff --git a/gitlint-core/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_2 b/gitlint-core/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_2 index 6d6da43..46a8adf 100644 --- a/gitlint-core/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_2 +++ b/gitlint-core/gitlint/tests/expected/cli/test_cli/test_input_stream_debug_2 @@ -4,7 +4,8 @@ DEBUG: gitlint.cli Python version: {python_version} DEBUG: gitlint.cli Git version: git version 1.2.3 DEBUG: gitlint.cli Gitlint version: {gitlint_version} DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} -DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING} +DEBUG: gitlint.cli TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} DEBUG: gitlint.cli Configuration config-path: None [GENERAL] diff --git a/gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_multiple_commits_csv_1 b/gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_multiple_commits_csv_1 new file mode 100644 index 0000000..be3288b --- /dev/null +++ b/gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_multiple_commits_csv_1 @@ -0,0 +1,8 @@ +Commit 6f29bf81a8: +3: B5 Body message is too short (12<20): "commït-body1" + +Commit 25053ccec5: +3: B5 Body message is too short (12<20): "commït-body2" + +Commit 4da2656b0d: +3: B5 Body message is too short (12<20): "commït-body3" diff --git a/gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_2 b/gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_2 index 59b2414..6b96a45 100644 --- a/gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_2 +++ b/gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_staged_msg_filename_2 @@ -4,7 +4,8 @@ DEBUG: gitlint.cli Python version: {python_version} DEBUG: gitlint.cli Git version: git version 1.2.3 DEBUG: gitlint.cli Gitlint version: {gitlint_version} DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} -DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING} +DEBUG: gitlint.cli TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} DEBUG: gitlint.cli Configuration config-path: None [GENERAL] @@ -87,7 +88,6 @@ 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 +{changed_files_stats} ----------------------- DEBUG: gitlint.cli Exit Code = 2
\ No newline at end of file diff --git a/gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_2 b/gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_2 index 23df7b2..45d94e2 100644 --- a/gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_2 +++ b/gitlint-core/gitlint/tests/expected/cli/test_cli/test_lint_staged_stdin_2 @@ -4,7 +4,8 @@ DEBUG: gitlint.cli Python version: {python_version} DEBUG: gitlint.cli Git version: git version 1.2.3 DEBUG: gitlint.cli Gitlint version: {gitlint_version} DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} -DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING} +DEBUG: gitlint.cli TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} DEBUG: gitlint.cli Configuration config-path: None [GENERAL] @@ -89,7 +90,6 @@ 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 +{changed_files_stats} ----------------------- DEBUG: gitlint.cli Exit Code = 3
\ No newline at end of file diff --git a/gitlint-core/gitlint/tests/expected/cli/test_cli/test_named_rules_2 b/gitlint-core/gitlint/tests/expected/cli/test_cli/test_named_rules_2 index c4491f1..f4df46e 100644 --- a/gitlint-core/gitlint/tests/expected/cli/test_cli/test_named_rules_2 +++ b/gitlint-core/gitlint/tests/expected/cli/test_cli/test_named_rules_2 @@ -4,7 +4,8 @@ DEBUG: gitlint.cli Python version: {python_version} DEBUG: gitlint.cli Git version: git version 1.2.3 DEBUG: gitlint.cli Gitlint version: {gitlint_version} DEBUG: gitlint.cli GITLINT_USE_SH_LIB: {GITLINT_USE_SH_LIB} -DEBUG: gitlint.cli DEFAULT_ENCODING: {DEFAULT_ENCODING} +DEBUG: gitlint.cli TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} DEBUG: gitlint.cli Configuration config-path: {config_path} [GENERAL] diff --git a/gitlint-core/gitlint/tests/git/test_git.py b/gitlint-core/gitlint/tests/git/test_git.py index 9c73bd9..b6a146a 100644 --- a/gitlint-core/gitlint/tests/git/test_git.py +++ b/gitlint-core/gitlint/tests/git/test_git.py @@ -1,11 +1,15 @@ import os - -from unittest.mock import patch, call - -from gitlint.shell import ErrorReturnCode, CommandNotFound - +from unittest.mock import call, patch + +from gitlint.git import ( + GitContext, + GitContextError, + GitNotInstalledError, + git_commentchar, + git_hooks_dir, +) +from gitlint.shell import CommandNotFound, ErrorReturnCode from gitlint.tests.base import BaseTestCase -from gitlint.git import GitContext, GitContextError, GitNotInstalledError, git_commentchar, git_hooks_dir class GitTests(BaseTestCase): diff --git a/gitlint-core/gitlint/tests/git/test_git_commit.py b/gitlint-core/gitlint/tests/git/test_git_commit.py index b27deaf..e6b0b2c 100644 --- a/gitlint-core/gitlint/tests/git/test_git_commit.py +++ b/gitlint-core/gitlint/tests/git/test_git_commit.py @@ -1,25 +1,21 @@ import copy import datetime from pathlib import Path - -import dateutil +from unittest.mock import call, patch import arrow - -from unittest.mock import patch, call - -from gitlint.tests.base import BaseTestCase +import dateutil from gitlint.git import ( GitChangedFileStats, - GitContext, GitCommit, + GitCommitMessage, + GitContext, GitContextError, LocalGitCommit, StagedLocalGitCommit, - GitCommitMessage, - GitChangedFileStats, ) from gitlint.shell import ErrorReturnCode +from gitlint.tests.base import BaseTestCase class GitCommitTests(BaseTestCase): @@ -383,7 +379,7 @@ class GitCommitTests(BaseTestCase): @patch("gitlint.git.sh") def test_get_latest_commit_fixup_squash_commit(self, sh): commit_prefixes = {"fixup": "is_fixup_commit", "squash": "is_squash_commit", "amend": "is_fixup_amend_commit"} - for commit_type in commit_prefixes.keys(): + for commit_type in commit_prefixes: sample_sha = "d8ac47e9f2923c7f22d8668e3a1ed04eb4cdbca9" sh.git.side_effect = [ @@ -616,7 +612,7 @@ class GitCommitTests(BaseTestCase): # 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(): + for commit_type in commit_prefixes: commit_msg = f"{commit_type}! Test message" gitcontext = GitContext.from_commit_msg(commit_msg) commit = gitcontext.commits[-1] @@ -642,7 +638,7 @@ class GitCommitTests(BaseTestCase): @patch("gitlint.git.sh") @patch("arrow.now") def test_staged_commit(self, now, sh): - # StagedLocalGitCommit() + """Test for StagedLocalGitCommit()""" sh.git.side_effect = [ "#", # git config --get core.commentchar @@ -744,7 +740,7 @@ class GitCommitTests(BaseTestCase): git.return_value = "foöbar" # Test simple equality case - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) context1 = GitContext() commit_message1 = GitCommitMessage(context1, "tëst\n\nfoo", "tëst\n\nfoo", "tēst", ["", "föo"]) commit1 = GitCommit( diff --git a/gitlint-core/gitlint/tests/git/test_git_context.py b/gitlint-core/gitlint/tests/git/test_git_context.py index 3dcbe4a..751136c 100644 --- a/gitlint-core/gitlint/tests/git/test_git_context.py +++ b/gitlint-core/gitlint/tests/git/test_git_context.py @@ -1,7 +1,7 @@ -from unittest.mock import patch, call +from unittest.mock import call, patch -from gitlint.tests.base import BaseTestCase from gitlint.git import GitContext +from gitlint.tests.base import BaseTestCase class GitContextTests(BaseTestCase): diff --git a/gitlint-core/gitlint/tests/rules/test_body_rules.py b/gitlint-core/gitlint/tests/rules/test_body_rules.py index 94b1edf..c142e6e 100644 --- a/gitlint-core/gitlint/tests/rules/test_body_rules.py +++ b/gitlint-core/gitlint/tests/rules/test_body_rules.py @@ -1,5 +1,5 @@ -from gitlint.tests.base import BaseTestCase from gitlint import rules +from gitlint.tests.base import BaseTestCase class BodyRuleTests(BaseTestCase): @@ -100,13 +100,13 @@ class BodyRuleTests(BaseTestCase): expected_violation = rules.RuleViolation("B5", "Body message is too short (21<120)", "å" * 21, 3) rule = rules.BodyMinLength({"min-length": 120}) - commit = self.gitcommit("Title\n\n{}\n".format("å" * 21)) # pylint: disable=consider-using-f-string + commit = self.gitcommit("Title\n\n{}\n".format("å" * 21)) violations = rule.validate(commit) self.assertListEqual(violations, [expected_violation]) # Make sure we don't get the error if the body-length is exactly the min-length rule = rules.BodyMinLength({"min-length": 8}) - commit = self.gitcommit("Tïtle\n\n{}\n".format("å" * 8)) # pylint: disable=consider-using-f-string + commit = self.gitcommit("Tïtle\n\n{}\n".format("å" * 8)) violations = rule.validate(commit) self.assertIsNone(violations) diff --git a/gitlint-core/gitlint/tests/rules/test_configuration_rules.py b/gitlint-core/gitlint/tests/rules/test_configuration_rules.py index 9e3b07c..5935a4a 100644 --- a/gitlint-core/gitlint/tests/rules/test_configuration_rules.py +++ b/gitlint-core/gitlint/tests/rules/test_configuration_rules.py @@ -1,6 +1,9 @@ -from gitlint.tests.base import BaseTestCase, EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING from gitlint import rules from gitlint.config import LintConfig +from gitlint.tests.base import ( + EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING, + BaseTestCase, +) class ConfigurationRuleTests(BaseTestCase): @@ -89,6 +92,25 @@ class ConfigurationRuleTests(BaseTestCase): self.assertEqual(config, LintConfig()) self.assert_logged([]) # nothing logged -> nothing ignored + # No author available -> rule is skipped and warning logged + staged_commit = self.gitcommit("Tïtle\n\nThis is\n a relëase body\n line") + rule = rules.IgnoreByAuthorName({"regex": "foo"}) + config = LintConfig() + rule.apply(config, staged_commit) + self.assertEqual(config, LintConfig()) + expected_log_messages = [ + "WARNING: gitlint.rules ignore-by-author-name - I4: skipping - commit.author_name unknown. " + "Suggested fix: Use the --staged flag (or set general.staged=True in .gitlint). " + "More details: https://jorisroovers.com/gitlint/configuration/#staged" + ] + self.assert_logged(expected_log_messages) + + # Non-Matching regex -> expect config to stay the same + rule = rules.IgnoreByAuthorName({"regex": "foo"}) + expected_config = LintConfig() + rule.apply(config, commit) + self.assertEqual(config, LintConfig()) + # Matching regex -> expect config to ignore all rules rule = rules.IgnoreByAuthorName({"regex": "(.*)ëst(.*)"}) expected_config = LintConfig() @@ -96,7 +118,7 @@ class ConfigurationRuleTests(BaseTestCase): rule.apply(config, commit) self.assertEqual(config, expected_config) - expected_log_messages = [ + 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(.*)'," diff --git a/gitlint-core/gitlint/tests/rules/test_meta_rules.py b/gitlint-core/gitlint/tests/rules/test_meta_rules.py index 0b8a10a..a574aa3 100644 --- a/gitlint-core/gitlint/tests/rules/test_meta_rules.py +++ b/gitlint-core/gitlint/tests/rules/test_meta_rules.py @@ -1,5 +1,8 @@ -from gitlint.tests.base import BaseTestCase, EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING from gitlint.rules import AuthorValidEmail, RuleViolation +from gitlint.tests.base import ( + EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING, + BaseTestCase, +) class MetaRuleTests(BaseTestCase): diff --git a/gitlint-core/gitlint/tests/rules/test_rules.py b/gitlint-core/gitlint/tests/rules/test_rules.py index 199cc7e..b401372 100644 --- a/gitlint-core/gitlint/tests/rules/test_rules.py +++ b/gitlint-core/gitlint/tests/rules/test_rules.py @@ -1,8 +1,12 @@ -from gitlint.tests.base import BaseTestCase from gitlint.rules import Rule, RuleViolation +from gitlint.tests.base import BaseTestCase class RuleTests(BaseTestCase): + def test_ruleviolation__str__(self): + expected = '57: rule-ïd Tēst message: "Tēst content"' + self.assertEqual(str(RuleViolation("rule-ïd", "Tēst message", "Tēst content", 57)), expected) + def test_rule_equality(self): self.assertEqual(Rule(), Rule()) # Ensure rules are not equal if they differ on their attributes @@ -13,9 +17,16 @@ class RuleTests(BaseTestCase): def test_rule_log(self): rule = Rule() + self.assertIsNone(rule._log) rule.log.debug("Tēst message") self.assert_log_contains("DEBUG: gitlint.rules Tēst message") + # Assert the same logger is reused when logging multiple messages + log = rule._log + rule.log.debug("Anöther message") + self.assertEqual(log, rule._log) + self.assert_log_contains("DEBUG: gitlint.rules Anöther message") + def test_rule_violation_equality(self): violation1 = RuleViolation("ïd1", "My messåge", "My cöntent", 1) self.object_equality_test(violation1, ["rule_id", "message", "content", "line_nr"]) diff --git a/gitlint-core/gitlint/tests/rules/test_title_rules.py b/gitlint-core/gitlint/tests/rules/test_title_rules.py index 4796e54..cba3851 100644 --- a/gitlint-core/gitlint/tests/rules/test_title_rules.py +++ b/gitlint-core/gitlint/tests/rules/test_title_rules.py @@ -1,15 +1,15 @@ -from gitlint.tests.base import BaseTestCase from gitlint.rules import ( - TitleMaxLength, - TitleTrailingWhitespace, + RuleViolation, TitleHardTab, - TitleMustNotContainWord, - TitleTrailingPunctuation, TitleLeadingWhitespace, - TitleRegexMatches, - RuleViolation, + TitleMaxLength, TitleMinLength, + TitleMustNotContainWord, + TitleRegexMatches, + TitleTrailingPunctuation, + TitleTrailingWhitespace, ) +from gitlint.tests.base import BaseTestCase class TitleRuleTests(BaseTestCase): diff --git a/gitlint-core/gitlint/tests/rules/test_user_rules.py b/gitlint-core/gitlint/tests/rules/test_user_rules.py index fc8d423..8086bea 100644 --- a/gitlint-core/gitlint/tests/rules/test_user_rules.py +++ b/gitlint-core/gitlint/tests/rules/test_user_rules.py @@ -1,11 +1,10 @@ import os import sys -from gitlint.tests.base import BaseTestCase -from gitlint.rule_finder import find_rule_classes, assert_valid_rule_class -from gitlint.rules import UserRuleError - from gitlint import options, rules +from gitlint.rule_finder import assert_valid_rule_class, find_rule_classes +from gitlint.rules import UserRuleError +from gitlint.tests.base import BaseTestCase class UserRuleTests(BaseTestCase): @@ -104,21 +103,21 @@ class UserRuleTests(BaseTestCase): target = rules.CommitMessageTitle def validate(self): - pass + pass # pragma: nocover class MyCommitRuleClass(rules.CommitRule): id = "UC2" name = "my-cömmit-rule" def validate(self): - pass + pass # pragma: nocover class MyConfigurationRuleClass(rules.ConfigurationRule): id = "UC3" name = "my-cönfiguration-rule" def apply(self): - pass + pass # pragma: nocover # Just assert that no error is raised self.assertIsNone(assert_valid_rule_class(MyLineRuleClass)) @@ -203,7 +202,7 @@ class UserRuleTests(BaseTestCase): assert_valid_rule_class(MyRuleClass) # option_spec is a list, but not of gitlint options - MyRuleClass.options_spec = ["föo", 123] # pylint: disable=bad-option-value,redefined-variable-type + MyRuleClass.options_spec = ["föo", 123] with self.assertRaisesMessage(UserRuleError, expected_msg): assert_valid_rule_class(MyRuleClass) @@ -236,8 +235,8 @@ class UserRuleTests(BaseTestCase): with self.assertRaisesMessage(UserRuleError, expected_msg): assert_valid_rule_class(MyRuleClass) - # validate attribute - not a method - MyRuleClass.validate = "föo" + # apply attribute - not a method + MyRuleClass.apply = "föo" with self.assertRaisesMessage(UserRuleError, expected_msg): assert_valid_rule_class(MyRuleClass) @@ -247,7 +246,7 @@ class UserRuleTests(BaseTestCase): name = "my-rüle-class" def validate(self): - pass + pass # pragma: nocover # no target expected_msg = ( @@ -263,5 +262,5 @@ class UserRuleTests(BaseTestCase): assert_valid_rule_class(MyRuleClass) # valid target, no exception should be raised - MyRuleClass.target = rules.CommitMessageTitle # pylint: disable=bad-option-value,redefined-variable-type + MyRuleClass.target = rules.CommitMessageTitle self.assertIsNone(assert_valid_rule_class(MyRuleClass)) diff --git a/gitlint-core/gitlint/tests/samples/user_rules/my_commit_rules.py b/gitlint-core/gitlint/tests/samples/user_rules/my_commit_rules.py index 02c922d..c947250 100644 --- a/gitlint-core/gitlint/tests/samples/user_rules/my_commit_rules.py +++ b/gitlint-core/gitlint/tests/samples/user_rules/my_commit_rules.py @@ -1,5 +1,5 @@ -from gitlint.rules import CommitRule, RuleViolation from gitlint.options import IntOption +from gitlint.rules import CommitRule, RuleViolation class MyUserCommitRule(CommitRule): @@ -19,7 +19,7 @@ class MyUserCommitRule(CommitRule): def func_should_be_ignored(): - pass + pass # pragma: nocover global_variable_should_be_ignored = True diff --git a/gitlint-core/gitlint/tests/samples/user_rules/parent_package/__init__.py b/gitlint-core/gitlint/tests/samples/user_rules/parent_package/__init__.py index 22c3f65..c2863fe 100644 --- a/gitlint-core/gitlint/tests/samples/user_rules/parent_package/__init__.py +++ b/gitlint-core/gitlint/tests/samples/user_rules/parent_package/__init__.py @@ -9,4 +9,4 @@ class InitFileRule(CommitRule): options_spec = [] def validate(self, _commit): - return [] + return [] # pragma: nocover diff --git a/gitlint-core/gitlint/tests/test_cache.py b/gitlint-core/gitlint/tests/test_cache.py index 9c327dc..08b821e 100644 --- a/gitlint-core/gitlint/tests/test_cache.py +++ b/gitlint-core/gitlint/tests/test_cache.py @@ -1,5 +1,5 @@ -from gitlint.tests.base import BaseTestCase from gitlint.cache import PropertyCache, cache +from gitlint.tests.base import BaseTestCase class CacheTests(BaseTestCase): diff --git a/gitlint-core/gitlint/tests/test_deprecation.py b/gitlint-core/gitlint/tests/test_deprecation.py index d85593a..bfe5934 100644 --- a/gitlint-core/gitlint/tests/test_deprecation.py +++ b/gitlint-core/gitlint/tests/test_deprecation.py @@ -1,7 +1,10 @@ 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 +from gitlint.tests.base import ( + EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING, + BaseTestCase, +) class DeprecationTests(BaseTestCase): diff --git a/gitlint-core/gitlint/tests/test_display.py b/gitlint-core/gitlint/tests/test_display.py index 1f759d2..e669cdb 100644 --- a/gitlint-core/gitlint/tests/test_display.py +++ b/gitlint-core/gitlint/tests/test_display.py @@ -1,9 +1,8 @@ from io import StringIO +from unittest.mock import patch -from unittest.mock import patch # pylint: disable=no-name-in-module, import-error - -from gitlint.display import Display from gitlint.config import LintConfig +from gitlint.display import Display from gitlint.tests.base import BaseTestCase diff --git a/gitlint-core/gitlint/tests/test_hooks.py b/gitlint-core/gitlint/tests/test_hooks.py index f92b148..7390f14 100644 --- a/gitlint-core/gitlint/tests/test_hooks.py +++ b/gitlint-core/gitlint/tests/test_hooks.py @@ -1,16 +1,15 @@ import os +from unittest.mock import ANY, mock_open, patch -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, + COMMIT_MSG_HOOK_SRC_PATH, GITLINT_HOOK_IDENTIFIER, + GitHookInstaller, + GitHookInstallerError, ) +from gitlint.tests.base import BaseTestCase class HookTests(BaseTestCase): @@ -58,9 +57,10 @@ class HookTests(BaseTestCase): expected_msg = f"{lint_config.target} is not a git repository." with self.assertRaisesMessage(GitHookInstallerError, expected_msg): GitHookInstaller.install_commit_msg_hook(lint_config) - isdir.assert_called_with(git_hooks_dir.return_value) - path_exists.assert_not_called() - copy.assert_not_called() + + isdir.assert_called_with(git_hooks_dir.return_value) + path_exists.assert_not_called() + copy.assert_not_called() # mock that there is already a commit hook present isdir.return_value = True @@ -106,9 +106,10 @@ class HookTests(BaseTestCase): expected_msg = f"{lint_config.target} is not a git repository." with self.assertRaisesMessage(GitHookInstallerError, expected_msg): GitHookInstaller.uninstall_commit_msg_hook(lint_config) - isdir.assert_called_with(git_hooks_dir.return_value) - path_exists.assert_not_called() - remove.assert_not_called() + + isdir.assert_called_with(git_hooks_dir.return_value) + path_exists.assert_not_called() + remove.assert_not_called() # mock that there is no commit hook present isdir.return_value = True @@ -117,9 +118,10 @@ class HookTests(BaseTestCase): expected_msg = f"There is no commit-msg hook present in {expected_dst}." with self.assertRaisesMessage(GitHookInstallerError, expected_msg): GitHookInstaller.uninstall_commit_msg_hook(lint_config) - isdir.assert_called_with(git_hooks_dir.return_value) - path_exists.assert_called_once_with(expected_dst) - remove.assert_not_called() + + isdir.assert_called_with(git_hooks_dir.return_value) + path_exists.assert_called_once_with(expected_dst) + remove.assert_not_called() # mock that there is a different (=not gitlint) commit hook isdir.return_value = True diff --git a/gitlint-core/gitlint/tests/test_lint.py b/gitlint-core/gitlint/tests/test_lint.py index 2af4615..1cf3772 100644 --- a/gitlint-core/gitlint/tests/test_lint.py +++ b/gitlint-core/gitlint/tests/test_lint.py @@ -1,11 +1,10 @@ from io import StringIO +from unittest.mock import patch -from unittest.mock import patch # pylint: disable=no-name-in-module, import-error - -from gitlint.tests.base import BaseTestCase +from gitlint.config import LintConfig, LintConfigBuilder from gitlint.lint import GitLinter from gitlint.rules import RuleViolation, TitleMustNotContainWord -from gitlint.config import LintConfig, LintConfigBuilder +from gitlint.tests.base import BaseTestCase class LintTests(BaseTestCase): diff --git a/gitlint-core/gitlint/tests/test_options.py b/gitlint-core/gitlint/tests/test_options.py index 7b146e7..deff723 100644 --- a/gitlint-core/gitlint/tests/test_options.py +++ b/gitlint-core/gitlint/tests/test_options.py @@ -1,12 +1,23 @@ import os import re +from gitlint.options import ( + BoolOption, + IntOption, + ListOption, + PathOption, + RegexOption, + RuleOptionError, + StrOption, +) from gitlint.tests.base import BaseTestCase -from gitlint.options import IntOption, BoolOption, StrOption, ListOption, PathOption, RegexOption, RuleOptionError - class RuleOptionTests(BaseTestCase): + def test_option__str__(self): + option = StrOption("tëst-option", "åbc", "Test Dëscription") + self.assertEqual(str(option), "(tëst-option: åbc (Test Dëscription))") + def test_option_equality(self): options = { IntOption: 123, @@ -158,7 +169,7 @@ class RuleOptionTests(BaseTestCase): option = PathOption("tëst-directory", ".", "Tëst Description", type="dir") self.assertEqual(option.name, "tëst-directory") self.assertEqual(option.description, "Tëst Description") - self.assertEqual(option.value, os.getcwd()) + self.assertEqual(option.value, os.path.realpath(".")) self.assertEqual(option.type, "dir") # re-set value diff --git a/gitlint-core/gitlint/tests/test_utils.py b/gitlint-core/gitlint/tests/test_utils.py index 27036d3..d21ec3f 100644 --- a/gitlint-core/gitlint/tests/test_utils.py +++ b/gitlint-core/gitlint/tests/test_utils.py @@ -27,7 +27,7 @@ class UtilsTests(BaseTestCase): self.assertEqual(utils.use_sh_library(), False) @patch("gitlint.utils.locale") - def test_default_encoding_non_windows(self, mocked_locale): + def test_terminal_encoding_non_windows(self, mocked_locale): utils.PLATFORM_IS_WINDOWS = False mocked_locale.getpreferredencoding.return_value = "foöbar" self.assertEqual(utils.getpreferredencoding(), "foöbar") @@ -37,7 +37,7 @@ class UtilsTests(BaseTestCase): self.assertEqual(utils.getpreferredencoding(), "UTF-8") @patch("os.environ") - def test_default_encoding_windows(self, patched_env): + def test_terminal_encoding_windows(self, patched_env): utils.PLATFORM_IS_WINDOWS = True # Mock out os.environ mock_env = {} diff --git a/gitlint-core/gitlint/utils.py b/gitlint-core/gitlint/utils.py index 697b472..3ccb78b 100644 --- a/gitlint-core/gitlint/utils.py +++ b/gitlint-core/gitlint/utils.py @@ -1,9 +1,7 @@ -# pylint: disable=bad-option-value,unidiomatic-typecheck,undefined-variable,no-else-return import codecs -import platform -import os - import locale +import os +import platform # Note: While we can easily inline the logic related to the constants set in this module, we deliberately create # small functions that encapsulate that logic as this enables easy unit testing. In particular, by creating functions @@ -40,30 +38,28 @@ def use_sh_library(): USE_SH_LIB = use_sh_library() ######################################################################################################################## -# DEFAULT_ENCODING +# TERMINAL_ENCODING +# Encoding used for terminal encoding/decoding. 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.""" fallback_encoding = "UTF-8" - default_encoding = locale.getpreferredencoding() or fallback_encoding + preferred_encoding = locale.getpreferredencoding() or fallback_encoding # On Windows, we mimic git/linux by trying to read the LC_ALL, LC_CTYPE, LANG env vars manually # (on Linux/MacOS the `getpreferredencoding()` call will take care of this). # We fallback to UTF-8 if PLATFORM_IS_WINDOWS: - default_encoding = fallback_encoding + preferred_encoding = fallback_encoding for env_var in ["LC_ALL", "LC_CTYPE", "LANG"]: encoding = os.environ.get(env_var, False) if encoding: # Support dotted (C.UTF-8) and non-dotted (C or UTF-8) charsets: # 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 :] - else: - default_encoding = encoding + preferred_encoding = encoding[dot_index + 1 :] if dot_index != -1 else encoding break # We've determined what encoding the user *wants*, let's now check if it's actually a valid encoding on the @@ -71,11 +67,21 @@ def getpreferredencoding(): # This scenario is fairly common on Windows where git sets LC_CTYPE=C when invoking the commit-msg hook, which # is not a valid encoding in Python on Windows. try: - codecs.lookup(default_encoding) # pylint: disable=no-member + codecs.lookup(preferred_encoding) except LookupError: - default_encoding = fallback_encoding + preferred_encoding = fallback_encoding + + return preferred_encoding - return default_encoding +TERMINAL_ENCODING = getpreferredencoding() -DEFAULT_ENCODING = getpreferredencoding() +######################################################################################################################## +# FILE_ENCODING +# Gitlint assumes UTF-8 encoding for all file operations: +# - reading/writing its own hook and config files +# - reading/writing git commit messages +# Git does have i18n.commitEncoding and i18n.logOutputEncoding options which we might want to take into account, +# but that's not supported today. + +FILE_ENCODING = "UTF-8" diff --git a/gitlint-core/pyproject.toml b/gitlint-core/pyproject.toml new file mode 100644 index 0000000..e65b7b0 --- /dev/null +++ b/gitlint-core/pyproject.toml @@ -0,0 +1,71 @@ +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +[project] +name = "gitlint-core" +dynamic = ["version", "urls"] +description = "Git commit message linter written in python, checks your commit messages for style." +readme = "README.md" +license = "MIT" +requires-python = ">=3.7" +authors = [{ name = "Joris Roovers" }] +keywords = [ + "git", + "gitlint", + "lint", # +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.7", + "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", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Software Development :: Testing", +] +dependencies = [ + "arrow>=1", + "Click>=8", + "importlib-metadata >= 1.0 ; python_version < \"3.8\"", + "sh>=1.13.0 ; sys_platform != \"win32\"", +] + +[project.optional-dependencies] +trusted-deps = [ + "arrow==1.2.3", + "Click==8.1.3", + "sh==1.14.3 ; sys_platform != \"win32\"", +] + +[project.scripts] +gitlint = "gitlint.cli:cli" + +[tool.hatch.version] +source = "vcs" +raw-options = { root = ".." } + +[tool.hatch.build] +include = [ + "/gitlint", # +] + +exclude = [ + "/gitlint/tests", # +] + +[tool.hatch.metadata.hooks.vcs.urls] +Homepage = "https://jorisroovers.github.io/gitlint" +Documentation = "https://jorisroovers.github.io/gitlint" +Source = "https://github.com/jorisroovers/gitlint/tree/main/gitlint-core" +Changelog = "https://github.com/jorisroovers/gitlint/blob/main/CHANGELOG.md" +# TODO(jorisroovers): Temporary disable until fixed in hatch-vcs (see #460) +# 'Source Commit' = "https://github.com/jorisroovers/gitlint/tree/{commit_hash}/gitlint-core"
\ No newline at end of file diff --git a/gitlint-core/setup.cfg b/gitlint-core/setup.cfg deleted file mode 100644 index 2a9acf1..0000000 --- a/gitlint-core/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal = 1 diff --git a/gitlint-core/setup.py b/gitlint-core/setup.py deleted file mode 100644 index 8917e27..0000000 --- a/gitlint-core/setup.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python -from setuptools import setup, find_packages -import io -import re -import os -import platform -import sys - - -description = "Git commit message linter written in python, checks your commit messages for style." -long_description = """ -Great for use as a commit-msg git hook or as part of your gating script in a CI pipeline (e.g. jenkins, github actions). -Many of the gitlint validations are based on `well-known`_ community_ `standards`_, others are based on checks that -we've found useful throughout the years. Gitlint has sane defaults, but you can also easily customize it to your -own liking. - -Demo and full documentation on `jorisroovers.github.io/gitlint`_. -To see what's new in the latest release, visit the CHANGELOG_. - -Source code on `github.com/jorisroovers/gitlint`_. - -.. _well-known: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html -.. _community: http://addamhardy.com/blog/2013/06/05/good-commit-messages-and-enforcing-them-with-git-hooks/ -.. _standards: http://chris.beams.io/posts/git-commit/ -.. _jorisroovers.github.io/gitlint: https://jorisroovers.github.io/gitlint -.. _CHANGELOG: https://github.com/jorisroovers/gitlint/blob/main/CHANGELOG.md -.. _github.com/jorisroovers/gitlint: https://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 = open(os.path.join(package, "__init__.py"), encoding="UTF-8").read() - return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) - - -setup( - name="gitlint-core", - version=get_version("gitlint"), - description=description, - long_description=long_description, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "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", - ], - python_requires=">=3.6", - install_requires=[ - "Click>=8", - "arrow>=1", - 'sh>=1.13.0 ; sys_platform != "win32"', - ], - extras_require={ - "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", - project_urls={ - "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": [ - "gitlint = gitlint.cli:cli", - ], - }, -) - -# 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" - ) - 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*******************" - ) - print(msg) diff --git a/hatch_build.py b/hatch_build.py new file mode 100644 index 0000000..4be7a45 --- /dev/null +++ b/hatch_build.py @@ -0,0 +1,13 @@ +# hatch_build.py is executed by hatch at build-time and can contain custom build logic hooks +import os +from hatchling.metadata.plugin.interface import MetadataHookInterface + + +class CustomMetadataHook(MetadataHookInterface): + """Custom metadata hook for hatch that ensures that gitlint and gitlint-core[trusted-deps] versions always match""" + + def update(self, metadata: dict) -> None: + # Only enforce versioning matching outside of the 'dev' environment, this allows for re-use of the 'dev' + # environment between different git branches. + if os.environ.get("HATCH_ENV_ACTIVE", "not-dev") != "dev": + metadata["dependencies"] = [f"gitlint-core[trusted-deps]=={metadata['version']}"] diff --git a/pyproject.toml b/pyproject.toml index 495acd0..7435b66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,203 @@ +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +[project] +name = "gitlint" +dynamic = ["version", "dependencies", "urls"] +description = "Git commit message linter written in python, checks your commit messages for style." +readme = "README.md" + +license = "MIT" +requires-python = ">=3.7" +authors = [{ name = "Joris Roovers" }] +keywords = [ + "git", + "gitlint", + "lint", # +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.7", + "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", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Software Development :: Testing", +] + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build] +exclude = ["*"] + +[tool.hatch.metadata.hooks.vcs.urls] +Homepage = "https://jorisroovers.github.io/gitlint" +Documentation = "https://jorisroovers.github.io/gitlint" +Source = "https://github.com/jorisroovers/gitlint" +Changelog = "https://github.com/jorisroovers/gitlint/blob/main/CHANGELOG.md" +# TODO(jorisroovers): Temporary disable until fixed in hatch-vcs (see #460) +# 'Source Archive' = "https://github.com/jorisroovers/gitlint/archive/{commit_hash}.zip" +# 'Source Commit' = "https://github.com/jorisroovers/gitlint/tree/{commit_hash}" + +# Use metadata hooks specified in 'hatch_build.py' +# (this line is critical when building wheels, when building sdist it seems optional) +[tool.hatch.metadata.hooks.custom] + +# Environments ######################################################################################################### +# NOTE: By default all environments inherit from the 'default' environment + +# DEV +# Workaround for editable install: +# https://github.com/pypa/hatch/issues/588 +[tool.hatch.envs.dev] +description = """ +Dev environment (running gitlint itself from source) +""" +pre-install-commands = [ + "pip install -e ./gitlint-core", # +] + +[tool.hatch.envs.dev.scripts] +fullclean = [ + "rm .coverage .coverage.lcov", + "rm -rf site dist .pytest_cache", + "rm -rf gitlint-core/dist gitlint-core/build gitlint-core/.pytest_cache", + "rm -rf qa/__pycache__ qa/.pytest_cache", +] + +# TEST +[tool.hatch.envs.test] +description = """ +Test environment (unit tests, formatting, lint) +""" +skip-install = true +dependencies = [ + "gitlint-core[trusted-deps] @ {root:uri}/gitlint-core", + "black==23.1.0", + "pytest==7.2.1", + "pytest-cov==4.0.0", + "python-coveralls==2.9.3", + "ruff==0.0.252", + "radon==5.1.0", + "pdbr==0.8.2; sys_platform != \"win32\"", +] + +[tool.hatch.envs.test.scripts] +unit-tests = [ + "pytest --cov=gitlint-core --cov-report=term --cov-report=lcov:.coverage.lcov -rw -s {args:gitlint-core}", +] +u = "unit-tests" +unit-tests-no-cov = "pytest -rw -s {args:gitlint-core}" +format = "black --check --diff {args:.}" +lint = "ruff {args:gitlint-core/gitlint qa}" +autoformat = "black {args:.}" +autofix = [ + "ruff --fix {args:gitlint-core/gitlint qa}", + "autoformat", # +] + +all = [ + "unit-tests", + "format", + "lint", # +] +stats = ["./tools/stats.sh"] + +# QA +[tool.hatch.envs.qa] +description = """ +Integration test environment. +Run a set of integration tests against any gitlint binary (not just the one from local source). +""" +detached = true +dependencies = [ + "pytest==7.2.1", + "arrow==1.2.3", + "sh==1.14.3; sys_platform != \"win32\"", + "pdbr==0.8.2; sys_platform != \"win32\"", +] + +[tool.hatch.envs.qa.scripts] +# The integration tests can be ran against any gitlint binary, e.g. one installed from pypi (for post-release testing) +# This is why by default we don't install the local dev version of gitlint in the qa environment +# To run integration tests against the dev version of gitlint, use install-local first +install-local = "pip install -e ./gitlint-core[trusted-deps]" +integration-tests = "pytest -rw -s {args:qa}" +i = "integration-tests" + + +# DOCS +[tool.hatch.envs.docs] +description = """ +Documentation environment. Run docs build and serve commands. +""" +detached = true +dependencies = [ + "mkdocs==1.4.2", # +] + +[tool.hatch.envs.docs.scripts] +build = "mkdocs build --clean --strict" +serve = "mkdocs serve" + +# Tool config ########################################################################################################## + [tool.black] -target_version = ['py36', 'py37', 'py38','py39','py310'] +target_version = ['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"
\ No newline at end of file +extend-exclude = "gitlint-core/gitlint/tests/samples/user_rules/import_exception/invalid_python.py" + +[tool.ruff] +target-version = "py37" +extend-exclude = [ + "gitlint-core/gitlint/tests/samples/user_rules/import_exception/invalid_python.py", +] + +ignore = [ + "E501", # Never enforce `E501` (line length violations) - taken care of by black + "SIM108", # Use ternary operator instead of if-else-block + "PLR0913", # Too many arguments to function call +] + +select = [ + "F", # PyFlakes + "E", # Pycodestyle + "W", # Pycodestyle + "I", # isort (import order) + "YTT", # flake8-2020 (misuse of sys.version) + "S", # flake8-bandit (security) + "B", # flake8-bugbear + "C4", # flake8-comprehensions (correct use of comprehensions) + "T10", # flake8-debugger (no debug statements) + "T20", # flake8-print (no print statements) + "SIM", # flake8-simplify (use simple code) + "TID", # flake8-tidy-imports (correct import syntax) + "ARG", # flake8-unused-arguments (no unused function arguments) + "DTZ", # flake8-datetimez (correct datetime usage) + "ERA", # eradicate (no commented out code) + "UP", # pyupgrade (modern python syntax) + "PLC", # pylint + "PLE", # pylint + "PLR", # pylint + "PLW", # pylint + "PIE", # flake8-pie + "RUF", # ruff specific +] + +[tool.coverage.run] +branch = true # measure branch coverage in addition to statement coverage + +[tool.coverage.report] +fail_under = 97 +show_missing = true @@ -1,20 +1,16 @@ -# pylint: disable=bad-option-value,unidiomatic-typecheck,undefined-variable,no-else-return, -# pylint: disable=too-many-function-args,unexpected-keyword-arg - import os import platform import shutil import sys import tempfile -from datetime import datetime -from uuid import uuid4 +from datetime import datetime, timezone from unittest import TestCase +from uuid import uuid4 import arrow - -from qa.shell import git, gitlint, RunningCommand -from qa.utils import DEFAULT_ENCODING +from qa.shell import RunningCommand, git, gitlint +from qa.utils import FILE_ENCODING, PLATFORM_IS_WINDOWS, TERMINAL_ENCODING class BaseTestCase(TestCase): @@ -40,18 +36,19 @@ class BaseTestCase(TestCase): for tmpfile in self.tmpfiles: os.remove(tmpfile) for repo in self.tmp_git_repos: - shutil.rmtree(repo) + # On windows we need to ignore errors because git might still be holding on to some files + shutil.rmtree(repo, ignore_errors=PLATFORM_IS_WINDOWS) - def assertEqualStdout(self, output, expected): # pylint: disable=invalid-name + def assertEqualStdout(self, output, expected): self.assertIsInstance(output, RunningCommand) - output = output.stdout.decode(DEFAULT_ENCODING) + output = output.stdout.decode(TERMINAL_ENCODING) output = output.replace("\r", "") self.assertMultiLineEqual(output, expected) @staticmethod def generate_temp_path(): - timestamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f") - return os.path.realpath(f"/tmp/gitlint-test-{timestamp}") + timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f") + return os.path.realpath(f"/tmp/gitlint-test-{timestamp}") # noqa def create_tmp_git_repo(self): """Creates a temporary git repository and returns its directory path""" @@ -72,6 +69,9 @@ class BaseTestCase(TestCase): # http://stackoverflow.com/questions/5581857/git-and-the-umlaut-problem-on-mac-os-x git("config", "core.precomposeunicode", "true", _cwd=tmp_git_repo) + # Git now does commit message cleanup by default (e.g. removing trailing whitespace), disable that for testing + git("config", "commit.cleanup", "verbatim", _cwd=tmp_git_repo) + return tmp_git_repo @staticmethod @@ -84,13 +84,12 @@ class BaseTestCase(TestCase): if isinstance(content, bytes): open_kwargs = {"mode": "wb"} else: - open_kwargs = {"mode": "w", "encoding": DEFAULT_ENCODING} + open_kwargs = {"mode": "w", "encoding": FILE_ENCODING} - with open(full_path, **open_kwargs) as f: # pylint: disable=unspecified-encoding + with open(full_path, **open_kwargs) as f: f.write(content) else: - # pylint: disable=consider-using-with - open(full_path, "a", encoding=DEFAULT_ENCODING).close() + open(full_path, "a", encoding=FILE_ENCODING).close() # noqa: SIM115 (Use context handler for opening files) return test_filename @@ -150,9 +149,9 @@ class BaseTestCase(TestCase): if isinstance(content, bytes): open_kwargs = {"mode": "wb"} else: - open_kwargs = {"mode": "w", "encoding": DEFAULT_ENCODING} + open_kwargs = {"mode": "w", "encoding": FILE_ENCODING} - with open(tmpfile, **open_kwargs) as f: # pylint: disable=unspecified-encoding + with open(tmpfile, **open_kwargs) as f: f.write(content) return tmpfilepath @@ -181,7 +180,8 @@ class BaseTestCase(TestCase): specified by variable_dict.""" expected_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "expected") expected_path = os.path.join(expected_dir, filename) - with open(expected_path, encoding=DEFAULT_ENCODING) as file: + # Expected files are UTF-8 encoded (not dependent on the system's default encoding) + with open(expected_path, encoding=FILE_ENCODING) as file: expected = file.read() if variable_dict: @@ -199,7 +199,8 @@ class BaseTestCase(TestCase): "git_version": expected_git_version, "gitlint_version": expected_gitlint_version, "GITLINT_USE_SH_LIB": BaseTestCase.GITLINT_USE_SH_LIB, - "DEFAULT_ENCODING": DEFAULT_ENCODING, + "TERMINAL_ENCODING": TERMINAL_ENCODING, + "FILE_ENCODING": FILE_ENCODING, } def get_debug_vars_last_commit(self, git_repo=None): diff --git a/qa/expected/test_commits/test_lint_staged_msg_filename_1 b/qa/expected/test_commits/test_lint_staged_msg_filename_1 index f2ab49e..03a558c 100644 --- a/qa/expected/test_commits/test_lint_staged_msg_filename_1 +++ b/qa/expected/test_commits/test_lint_staged_msg_filename_1 @@ -5,7 +5,8 @@ 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 TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} DEBUG: gitlint.cli Configuration config-path: None [GENERAL] diff --git a/qa/expected/test_commits/test_lint_staged_stdin_1 b/qa/expected/test_commits/test_lint_staged_stdin_1 index cf34b8b..7892865 100644 --- a/qa/expected/test_commits/test_lint_staged_stdin_1 +++ b/qa/expected/test_commits/test_lint_staged_stdin_1 @@ -5,7 +5,8 @@ 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 TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} DEBUG: gitlint.cli Configuration config-path: None [GENERAL] diff --git a/qa/expected/test_config/test_config_from_env_1 b/qa/expected/test_config/test_config_from_env_1 index 38fba21..91eee40 100644 --- a/qa/expected/test_config/test_config_from_env_1 +++ b/qa/expected/test_config/test_config_from_env_1 @@ -5,7 +5,8 @@ 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 TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} DEBUG: gitlint.cli Configuration config-path: None [GENERAL] diff --git a/qa/expected/test_config/test_config_from_env_2 b/qa/expected/test_config/test_config_from_env_2 index 50d1e3f..06b0c1b 100644 --- a/qa/expected/test_config/test_config_from_env_2 +++ b/qa/expected/test_config/test_config_from_env_2 @@ -5,7 +5,8 @@ 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 TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} DEBUG: gitlint.cli Configuration config-path: None [GENERAL] diff --git a/qa/expected/test_config/test_config_from_file_debug_1 b/qa/expected/test_config/test_config_from_file_debug_1 index 39bdf52..279fb32 100644 --- a/qa/expected/test_config/test_config_from_file_debug_1 +++ b/qa/expected/test_config/test_config_from_file_debug_1 @@ -5,7 +5,8 @@ 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 TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} DEBUG: gitlint.cli Configuration config-path: {config_path} [GENERAL] diff --git a/qa/expected/test_gitlint/test_commit_binary_file_1 b/qa/expected/test_gitlint/test_commit_binary_file_1 index 6bc119b..83faf1b 100644 --- a/qa/expected/test_gitlint/test_commit_binary_file_1 +++ b/qa/expected/test_gitlint/test_commit_binary_file_1 @@ -5,7 +5,8 @@ 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 TERMINAL_ENCODING: {TERMINAL_ENCODING} +DEBUG: gitlint.cli FILE_ENCODING: {FILE_ENCODING} DEBUG: gitlint.cli Configuration config-path: None [GENERAL] diff --git a/qa/expected/test_rules/test_ignore_rules_1 b/qa/expected/test_rules/test_ignore_rules_1 new file mode 100644 index 0000000..f87f303 --- /dev/null +++ b/qa/expected/test_rules/test_ignore_rules_1 @@ -0,0 +1,3 @@ +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Commït Tïtle" +3: B3 Line contains hard tab characters (\t): "Sïmple commit body" +4: B2 Line has trailing whitespace: "Anōther Line " diff --git a/qa/expected/test_rules/test_ignore_rules_2 b/qa/expected/test_rules/test_ignore_rules_2 new file mode 100644 index 0000000..dc6428c --- /dev/null +++ b/qa/expected/test_rules/test_ignore_rules_2 @@ -0,0 +1,2 @@ +1: T5 Title contains the word 'WIP' (case-insensitive): "WIP: Commït Tïtle" +3: B3 Line contains hard tab characters (\t): "Sïmple commit body" diff --git a/qa/expected/test_rules/test_match_regex_rules_1 b/qa/expected/test_rules/test_match_regex_rules_1 new file mode 100644 index 0000000..3bfaa58 --- /dev/null +++ b/qa/expected/test_rules/test_match_regex_rules_1 @@ -0,0 +1,2 @@ +1: T7 Title does not match regex (foo): "Thåt dûr bår" +4: B8 Body does not match regex (bar) diff --git a/qa/expected/test_user_defined/test_user_defined_rules_examples_2 b/qa/expected/test_user_defined/test_user_defined_rules_examples_2 index 9b96423..d706b12 100644 --- a/qa/expected/test_user_defined/test_user_defined_rules_examples_2 +++ b/qa/expected/test_user_defined/test_user_defined_rules_examples_2 @@ -2,3 +2,4 @@ 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 +3: B3 Line contains hard tab characters (\t) diff --git a/qa/requirements.txt b/qa/requirements.txt deleted file mode 100644 index cf6baa5..0000000 --- a/qa/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -sh==1.14.3 -pytest==7.0.1; -arrow==1.2.3; -gitlint # no version as you want to test the currently installed version diff --git a/qa/samples/user_rules/extra/extra_rules.py b/qa/samples/user_rules/extra/extra_rules.py index cad531b..7996590 100644 --- a/qa/samples/user_rules/extra/extra_rules.py +++ b/qa/samples/user_rules/extra/extra_rules.py @@ -1,5 +1,5 @@ -from gitlint.rules import CommitRule, RuleViolation, ConfigurationRule -from gitlint.options import IntOption, StrOption, ListOption +from gitlint.options import IntOption, ListOption, StrOption +from gitlint.rules import CommitRule, ConfigurationRule, RuleViolation class GitContextRule(CommitRule): @@ -64,9 +64,9 @@ class ConfigurableCommitRule(CommitRule): def validate(self, _): violations = [ - RuleViolation(self.id, f"int-öption: {self.options[u'int-öption'].value}", line_nr=1), - RuleViolation(self.id, f"str-öption: {self.options[u'str-öption'].value}", line_nr=1), - RuleViolation(self.id, f"list-öption: {self.options[u'list-öption'].value}", line_nr=1), + RuleViolation(self.id, f"int-öption: {self.options['int-öption'].value}", line_nr=1), + RuleViolation(self.id, f"str-öption: {self.options['str-öption'].value}", line_nr=1), + RuleViolation(self.id, f"list-öption: {self.options['list-öption'].value}", line_nr=1), ] return violations diff --git a/qa/shell.py b/qa/shell.py index 44716c0..3ef874d 100644 --- a/qa/shell.py +++ b/qa/shell.py @@ -2,24 +2,31 @@ # on gitlint internals for our integration testing framework. import subprocess -from qa.utils import USE_SH_LIB, DEFAULT_ENCODING + +from qa.utils import TERMINAL_ENCODING, USE_SH_LIB if USE_SH_LIB: - from sh import git, echo, gitlint # pylint: disable=unused-import,no-name-in-module,import-error + from sh import ( + echo, + git, + gitlint, + ) - gitlint = gitlint.bake(_unify_ttys=True, _tty_in=True) # pylint: disable=invalid-name + gitlint = gitlint.bake(_unify_ttys=True, _tty_in=True) # import exceptions separately, this makes it a little easier to mock them out in the unit tests - from sh import CommandNotFound, ErrorReturnCode, RunningCommand # pylint: disable=import-error + from sh import ( + CommandNotFound, + ErrorReturnCode, + RunningCommand, + ) else: class CommandNotFound(Exception): """Exception indicating a command was not found during execution""" - pass - class RunningCommand: - pass + ... class ShResult(RunningCommand): """Result wrapper class. We use this to more easily migrate from using https://amoffat.github.io/sh/ to using @@ -29,18 +36,35 @@ else: self.full_cmd = full_cmd # TODO(jorisroovers): The 'sh' library by default will merge stdout and stderr. We mimic this behavior # for now until we fully remove the 'sh' library. - self.stdout = stdout + stderr.decode(DEFAULT_ENCODING) - self.stderr = stderr + self._stdout = stdout + stderr + self._stderr = stderr self.exit_code = exitcode def __str__(self): + return self.stdout.decode(TERMINAL_ENCODING) + + def __unicode__(self): return self.stdout + @property + def stdout(self): + return self._stdout + + @property + def stderr(self): + return self._stderr + + def __getattr__(self, p): + # https://github.com/amoffat/sh/blob/e0ed8e244e9d973ef4e0749b2b3c2695e7b5255b/sh.py#L952= + _unicode_methods = set(dir(str())) # noqa + if p in _unicode_methods: + return getattr(str(self), p) + + raise AttributeError + class ErrorReturnCode(ShResult, Exception): """ShResult subclass for unexpected results (acts as an exception).""" - pass - def git(*command_parts, **kwargs): return run_command("git", *command_parts, **kwargs) @@ -51,31 +75,36 @@ else: return run_command("gitlint", *command_parts, **kwargs) def run_command(command, *args, **kwargs): - args = [command] + list(args) - result = _exec(*args, **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: - return result - return str(result) + args = [command, *list(args)] + 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"] - if "_env" in kwargs: - popen_kwargs["env"] = kwargs["_env"] + popen_kwargs = { + "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, + "stdin": subprocess.PIPE, + "shell": kwargs.get("_tty_out", False), + "cwd": kwargs.get("_cwd", None), + "env": kwargs.get("_env", None), + } + + stdin_input = None + if len(args) > 1 and isinstance(args[1], ShResult): + stdin_input = args[1].stdout + # pop args[1] from the array and use it as stdin + args = list(args) + args.pop(1) + popen_kwargs["stdin"] = subprocess.PIPE try: with subprocess.Popen(args, **popen_kwargs) as p: - result = p.communicate() + result = p.communicate(stdin_input) + except FileNotFoundError as exc: raise CommandNotFound from exc exit_code = p.returncode - stdout = result[0].decode(DEFAULT_ENCODING) + stdout = result[0] stderr = result[1] # 'sh' does not decode the stderr bytes to unicode full_cmd = "" if args is None else " ".join(args) diff --git a/qa/test_commits.py b/qa/test_commits.py index d40c211..11d1851 100644 --- a/qa/test_commits.py +++ b/qa/test_commits.py @@ -1,10 +1,9 @@ -# pylint: disable=too-many-function-args,unexpected-keyword-arg import re import arrow -from qa.shell import echo, git, gitlint from qa.base import BaseTestCase +from qa.shell import echo, git, gitlint class CommitsTests(BaseTestCase): @@ -111,6 +110,11 @@ class CommitsTests(BaseTestCase): self.assertEqual(output.exit_code, 2) self.assertEqualStdout(output, expected) + # Lint using --commits <commit sha>, + output = gitlint("--commits", f"{commit_sha},", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2]) + self.assertEqual(output.exit_code, 2) + self.assertEqualStdout(output, expected) + # Lint a single commit using --commits <refspec> pointing to the single commit output = gitlint("--commits", refspec, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2]) self.assertEqual(output.exit_code, 2) @@ -129,7 +133,7 @@ class CommitsTests(BaseTestCase): self.assertEqual(output.exit_code, 254) def test_lint_staged_stdin(self): - """Tests linting a staged commit. Gitint should lint the passed commit message andfetch additional meta-data + """Tests linting a staged commit. Gitint should lint the passed commit message and fetch additional meta-data from the underlying repository. The easiest way to test this is by inspecting `--debug` output. This is the equivalent of doing: echo "WIP: Pïpe test." | gitlint --staged --debug diff --git a/qa/test_config.py b/qa/test_config.py index 1225f6a..d051686 100644 --- a/qa/test_config.py +++ b/qa/test_config.py @@ -1,10 +1,8 @@ -# pylint: disable=too-many-function-args,unexpected-keyword-arg - +import os import re -from qa.shell import gitlint from qa.base import BaseTestCase -from qa.utils import DEFAULT_ENCODING +from qa.shell import gitlint class ConfigTests(BaseTestCase): @@ -69,7 +67,7 @@ class ConfigTests(BaseTestCase): "This line of the body is here because we need it" ) filename = self.create_simple_commit(commit_msg, git_repo=target_repo) - config_path = self.get_sample_path("config/gitlintconfig") + config_path = self.get_sample_path(os.path.join("config", "gitlintconfig")) output = gitlint("--config", config_path, "--debug", _cwd=target_repo, _tty_in=True, _ok_code=[5]) expected_kwargs = self.get_debug_vars_last_commit(git_repo=target_repo) @@ -128,7 +126,7 @@ class ConfigTests(BaseTestCase): # Extract date from actual output to insert it into the expected output # We have to do this since there's no way for us to deterministically know that date otherwise p = re.compile("Date: (.*)\n", re.UNICODE | re.MULTILINE) - result = p.search(output.stdout.decode(DEFAULT_ENCODING)) + result = p.search(str(output)) date = result.group(1).strip() expected_kwargs.update({"date": date}) diff --git a/qa/test_contrib.py b/qa/test_contrib.py index 129e576..d3a45ba 100644 --- a/qa/test_contrib.py +++ b/qa/test_contrib.py @@ -1,6 +1,5 @@ -# pylint: disable= -from qa.shell import gitlint from qa.base import BaseTestCase +from qa.shell import gitlint class ContribRuleTests(BaseTestCase): diff --git a/qa/test_gitlint.py b/qa/test_gitlint.py index 6c45196..7a04a39 100644 --- a/qa/test_gitlint.py +++ b/qa/test_gitlint.py @@ -1,8 +1,8 @@ -# pylint: disable=too-many-function-args,unexpected-keyword-arg import os -from qa.shell import echo, git, gitlint + from qa.base import BaseTestCase -from qa.utils import DEFAULT_ENCODING +from qa.shell import echo, git, gitlint +from qa.utils import FILE_ENCODING class IntegrationTests(BaseTestCase): @@ -58,7 +58,7 @@ class IntegrationTests(BaseTestCase): self.assertEqualStdout(output, expected) # Make a small modification to the commit and commit it using fixup commit - with open(os.path.join(self.tmp_git_repo, test_filename), "a", encoding=DEFAULT_ENCODING) as fh: + with open(os.path.join(self.tmp_git_repo, test_filename), "a", encoding=FILE_ENCODING) as fh: fh.write("Appending söme stuff\n") git("add", test_filename, _cwd=self.tmp_git_repo) @@ -87,7 +87,7 @@ class IntegrationTests(BaseTestCase): self.assertEqualStdout(output, expected) # Make a small modification to the commit and commit it using fixup=amend commit - with open(os.path.join(self.tmp_git_repo, test_filename), "a", encoding=DEFAULT_ENCODING) as fh: + with open(os.path.join(self.tmp_git_repo, test_filename), "a", encoding=FILE_ENCODING) as fh: fh.write("Appending söme stuff\n") git("add", test_filename, _cwd=self.tmp_git_repo) @@ -133,7 +133,7 @@ class IntegrationTests(BaseTestCase): self.assertEqualStdout(output, expected) # Make a small modification to the commit and commit it using squash commit - with open(os.path.join(self.tmp_git_repo, test_filename), "a", encoding=DEFAULT_ENCODING) as fh: + with open(os.path.join(self.tmp_git_repo, test_filename), "a", encoding=FILE_ENCODING) as fh: # Wanted to write a unicode string, but that's obnoxious if you want to do it across Python 2 and 3. # https://stackoverflow.com/questions/22392377/ # error-writing-a-file-with-file-write-in-python-unicodeencodeerror @@ -252,7 +252,7 @@ class IntegrationTests(BaseTestCase): binary_filename = self.create_simple_commit("Sïmple commit", file_contents=bytes([0x48, 0x00, 0x49, 0x00])) output = gitlint( "--debug", - _ok_code=1, + _ok_code=[1], _cwd=self.tmp_git_repo, ) diff --git a/qa/test_hooks.py b/qa/test_hooks.py index 19edeb2..99e76dd 100644 --- a/qa/test_hooks.py +++ b/qa/test_hooks.py @@ -1,7 +1,7 @@ -# pylint: disable=too-many-function-args,unexpected-keyword-arg import os -from qa.shell import git, gitlint + from qa.base import BaseTestCase +from qa.shell import git, gitlint class HookTests(BaseTestCase): @@ -30,18 +30,16 @@ class HookTests(BaseTestCase): # install git commit-msg hook and assert output output_installed = gitlint("install-hook", _cwd=self.tmp_git_repo) - expected_installed = ( - f"Successfully installed gitlint commit-msg hook in {self.tmp_git_repo}/.git/hooks/commit-msg\n" - ) + commit_msg_hook_path = os.path.join(self.tmp_git_repo, ".git", "hooks", "commit-msg") + expected_installed = f"Successfully installed gitlint commit-msg hook in {commit_msg_hook_path}\n" self.assertEqualStdout(output_installed, expected_installed) def tearDown(self): # uninstall git commit-msg hook and assert output output_uninstalled = gitlint("uninstall-hook", _cwd=self.tmp_git_repo) - expected_uninstalled = ( - f"Successfully uninstalled gitlint commit-msg hook from {self.tmp_git_repo}/.git/hooks/commit-msg\n" - ) + commit_msg_hook_path = os.path.join(self.tmp_git_repo, ".git", "hooks", "commit-msg") + expected_uninstalled = f"Successfully uninstalled gitlint commit-msg hook from {commit_msg_hook_path}\n" self.assertEqualStdout(output_uninstalled, expected_uninstalled) super().tearDown() @@ -171,10 +169,10 @@ class HookTests(BaseTestCase): output_installed = gitlint("install-hook", _cwd=worktree_dir) expected_hook_path = os.path.join(tmp_git_repo, ".git", "hooks", "commit-msg") - expected_msg = f"Successfully installed gitlint commit-msg hook in {expected_hook_path}\r\n" - self.assertEqual(output_installed, expected_msg) + expected_msg = f"Successfully installed gitlint commit-msg hook in {expected_hook_path}\n" + self.assertEqualStdout(output_installed, expected_msg) output_uninstalled = gitlint("uninstall-hook", _cwd=worktree_dir) expected_hook_path = os.path.join(tmp_git_repo, ".git", "hooks", "commit-msg") - expected_msg = f"Successfully uninstalled gitlint commit-msg hook from {expected_hook_path}\r\n" - self.assertEqual(output_uninstalled, expected_msg) + expected_msg = f"Successfully uninstalled gitlint commit-msg hook from {expected_hook_path}\n" + self.assertEqualStdout(output_uninstalled, expected_msg) diff --git a/qa/test_named_rules.py b/qa/test_named_rules.py index 75cd9a1..e3c6908 100644 --- a/qa/test_named_rules.py +++ b/qa/test_named_rules.py @@ -1,5 +1,5 @@ -from qa.shell import gitlint from qa.base import BaseTestCase +from qa.shell import gitlint class NamedRuleTests(BaseTestCase): diff --git a/qa/test_rules.py b/qa/test_rules.py new file mode 100644 index 0000000..218a13a --- /dev/null +++ b/qa/test_rules.py @@ -0,0 +1,61 @@ +from qa.base import BaseTestCase +from qa.shell import gitlint + + +class RuleTests(BaseTestCase): + """ + Tests for specific rules that are worth testing as integration tests. + It's not a goal to test every edge case of each rule, that's what the unit tests do. + """ + + def test_match_regex_rules(self): + """ + Test that T7 (title-match-regex) and B8 (body-match-regex) work as expected. + By default, these rules don't do anything, only when setting a custom regex will they run. + """ + + commit_msg = "Thåt dûr bår\n\nSïmple commit message body" + self.create_simple_commit(commit_msg) + + # Assert violations when T7 and B8 regexes don't match + output = gitlint("-c", "T7.regex=foo", "-c", "B8.regex=bar", _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[2]) + self.assertEqualStdout(output, self.get_expected("test_rules/test_match_regex_rules_1")) + + # Assert no violations when T7 and B8 regexes do match + output = gitlint("-c", "T7.regex=^Thåt", "-c", "B8.regex=commit message", _cwd=self.tmp_git_repo, _tty_in=True) + self.assertEqualStdout(output, "") + + def test_ignore_rules(self): + """ + Test that ignore rules work as expected: + ignore-by-title, ignore-by-body, ignore-by-author-name, ignore-body-lines + By default, these rules don't do anything, only when setting a custom regex will they run. + """ + commit_msg = "WIP: Commït Tïtle\n\nSïmple commit\tbody\nAnōther Line \nLåst Line" + self.create_simple_commit(commit_msg) + + # Assert violations when not ignoring anything + output = gitlint(_cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[3]) + self.assertEqualStdout(output, self.get_expected("test_rules/test_ignore_rules_1")) + + # Simple convenience function that passes in common arguments for this test + def invoke_gitlint(*args, **kwargs): + return gitlint( + *args, "-c", "general.regex-style-search=True", **kwargs, _cwd=self.tmp_git_repo, _tty_in=True + ) + + # ignore-by-title + output = invoke_gitlint("-c", "ignore-by-title.regex=Commït") + self.assertEqualStdout(output, "") + + # ignore-by-body + output = invoke_gitlint("-c", "ignore-by-body.regex=Anōther Line") + self.assertEqualStdout(output, "") + + # ignore-by-author-name + output = invoke_gitlint("-c", "ignore-by-author-name.regex=gitlint-test-user") + self.assertEqualStdout(output, "") + + # ignore-body-lines + output = invoke_gitlint("-c", "ignore-body-lines.regex=^Anōther", _ok_code=[2]) + self.assertEqualStdout(output, self.get_expected("test_rules/test_ignore_rules_2")) diff --git a/qa/test_stdin.py b/qa/test_stdin.py index 8ed4cb1..04a3de9 100644 --- a/qa/test_stdin.py +++ b/qa/test_stdin.py @@ -1,8 +1,8 @@ -# pylint: disable=too-many-function-args,unexpected-keyword-arg import subprocess -from qa.shell import echo, gitlint + from qa.base import BaseTestCase -from qa.utils import DEFAULT_ENCODING +from qa.shell import echo, gitlint +from qa.utils import FILE_ENCODING, TERMINAL_ENCODING class StdInTests(BaseTestCase): @@ -33,7 +33,7 @@ class StdInTests(BaseTestCase): # http://amoffat.github.io/sh/sections/special_arguments.html?highlight=_tty_in#err-to-out output = gitlint(echo("-n", ""), _cwd=self.tmp_git_repo, _tty_in=False, _err_to_out=True, _ok_code=[3]) - self.assertEqual(output, self.get_expected("test_stdin/test_stdin_pipe_empty_1")) + self.assertEqualStdout(output, self.get_expected("test_stdin/test_stdin_pipe_empty_1")) def test_stdin_file(self): """Test the scenario where STDIN is a regular file (stat.S_ISREG = True) @@ -42,7 +42,7 @@ class StdInTests(BaseTestCase): """ tmp_commit_msg_file = self.create_tmpfile("WIP: STDIN ïs a file test.") - with open(tmp_commit_msg_file, encoding=DEFAULT_ENCODING) as file_handle: + with open(tmp_commit_msg_file, encoding=FILE_ENCODING) as file_handle: # noqa: SIM117 # We need to use subprocess.Popen() here instead of sh because when passing a file_handle to sh, it will # deal with reading the file itself instead of passing it on to gitlint as a STDIN. Since we're trying to # test for the condition where stat.S_ISREG == True that won't work for us here. @@ -50,4 +50,4 @@ class StdInTests(BaseTestCase): "gitlint", stdin=file_handle, cwd=self.tmp_git_repo, stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) as p: output, _ = p.communicate() - self.assertEqual(output.decode(DEFAULT_ENCODING), self.get_expected("test_stdin/test_stdin_file_1")) + self.assertEqual(output.decode(TERMINAL_ENCODING), self.get_expected("test_stdin/test_stdin_file_1")) diff --git a/qa/test_user_defined.py b/qa/test_user_defined.py index a003f3e..718766c 100644 --- a/qa/test_user_defined.py +++ b/qa/test_user_defined.py @@ -1,6 +1,5 @@ -# pylint: disable=too-many-function-args,unexpected-keyword-arg -from qa.shell import gitlint from qa.base import BaseTestCase +from qa.shell import gitlint class UserDefinedRuleTests(BaseTestCase): @@ -19,7 +18,7 @@ class UserDefinedRuleTests(BaseTestCase): extra_path = self.get_example_path() commit_msg = "Release: Thi$ is å title\nContent on the second line\n$This line is ignored \nThis isn't\t\n" self.create_simple_commit(commit_msg) - output = gitlint("--extra-path", extra_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[4]) + output = gitlint("--extra-path", extra_path, _cwd=self.tmp_git_repo, _tty_in=True, _ok_code=[5]) self.assertEqualStdout(output, self.get_expected("test_user_defined/test_user_defined_rules_examples_2")) def test_user_defined_rules_examples_with_config(self): diff --git a/qa/utils.py b/qa/utils.py index 89292cd..d560d86 100644 --- a/qa/utils.py +++ b/qa/utils.py @@ -1,8 +1,6 @@ -# pylint: disable=bad-option-value,unidiomatic-typecheck,undefined-variable,no-else-return -import platform -import os - import locale +import os +import platform ######################################################################################################################## # PLATFORM_IS_WINDOWS @@ -31,32 +29,20 @@ def use_sh_library(): USE_SH_LIB = use_sh_library() ######################################################################################################################## -# DEFAULT_ENCODING +# TERMINAL_ENCODING +# Encoding for reading gitlint command output def getpreferredencoding(): - """Modified version of local.getpreferredencoding() that takes into account LC_ALL, LC_CTYPE, LANG env vars - on windows and falls back to UTF-8.""" - default_encoding = locale.getpreferredencoding() or "UTF-8" - - # On Windows, we mimic git/linux by trying to read the LC_ALL, LC_CTYPE, LANG env vars manually - # (on Linux/MacOS the `getpreferredencoding()` call will take care of this). - # We fallback to UTF-8 - if PLATFORM_IS_WINDOWS: - default_encoding = "UTF-8" - for env_var in ["LC_ALL", "LC_CTYPE", "LANG"]: - encoding = os.environ.get(env_var, False) - if encoding: - # Support dotted (C.UTF-8) and non-dotted (C or UTF-8) charsets: - # 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 :] - else: - default_encoding = encoding - break - - return default_encoding - - -DEFAULT_ENCODING = getpreferredencoding() + """Use local.getpreferredencoding() or fallback to UTF-8.""" + return locale.getpreferredencoding() or "UTF-8" + + +TERMINAL_ENCODING = getpreferredencoding() + + +######################################################################################################################## +# FILE_ENCODING + +# Encoding for reading/writing files within the tests, this is always UTF-8 +FILE_ENCODING = "UTF-8" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 99b12de..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -setuptools -wheel==0.37.1 --e . --e ./gitlint-core[trusted-deps] diff --git a/run_tests.sh b/run_tests.sh deleted file mode 100755 index 2a95a92..0000000 --- a/run_tests.sh +++ /dev/null @@ -1,532 +0,0 @@ -#!/bin/bash - - -help(){ - echo "Usage: $0 [OPTION]..." - echo "Run gitlint's test suite(s) or some convience commands" - echo " -h, --help Show this help output" - echo " -c, --clean Clean the project of temporary files" - echo " -f, --format Run format checks" - echo " -l, --lint Run pylint checks" - echo " -g, --git Run gitlint checks" - echo " -i, --integration Run integration tests" - echo " -b, --build Run build tests" - echo " -a, --all Run all tests and checks (unit, integration, formatting, git)" - echo " -e, --envs [ENV1],[ENV2] Run tests against specified python environments" - echo " (envs: 36,37,38,39,pypy37)." - echo " Also works for integration, formatting and lint tests." - echo " -C, --container Run the specified command in the container for the --envs specified" - echo " --all-env Run all tests against all python environments" - echo " --install Install virtualenvs for the --envs specified" - echo " --uninstall Remove virtualenvs for the --envs specified" - echo " --install-container Build and run Docker container for the --envs specified" - echo " --uninstall-container Kill Docker container for the --envs specified" - echo " --exec [CMD] Execute [CMD] in the --envs specified" - echo " -s, --stats Show some project stats" - echo " --no-coverage Don't make a unit test coverage report" - echo "" - exit 0 -} - -RED="\033[31m" -YELLOW="\033[33m" -BLUE="\033[94m" -GREEN="\033[32m" -NO_COLOR="\033[0m" - -title(){ - MSG="$BLUE$1$NO_COLOR" - echo -e $MSG -} - -subtitle(){ - MSG="$YELLOW$1$NO_COLOR" - echo -e $MSG -} - -fatal(){ - MSG="$RED$1$NO_COLOR" - echo -e $MSG - exit 1 -} - -assert_root(){ - if [ "$(id -u)" != "0" ]; then - fatal "$1" - fi -} - -# Utility method that prints SUCCESS if a test was successful, or FAIL together with the test output -handle_test_result(){ - EXIT_CODE=$1 - RESULT="$2" - # Change color to red or green depending on SUCCESS - if [ $EXIT_CODE -eq 0 ]; then - echo -e "${GREEN}SUCCESS" - else - echo -e "${RED}FAIL" - fi - # Print RESULT if not empty - if [ -n "$RESULT" ] ; then - echo -e "\n$RESULT" - fi - # Reset color - echo -e "${NO_COLOR}" -} - -run_formatting_check(){ - # BLACK - target=${testargs:-"."} - echo -ne "Running black --check..." - RESULT=$(black --check --diff $target) - local exit_code=$? - handle_test_result $exit_code "$RESULT" - return $exit_code -} - -run_unit_tests(){ - clean - # py.test -s => print standard output (i.e. show print statement output) - # -rw => print warnings - target=${testargs:-"gitlint-core"} - if [ $include_coverage -eq 1 ]; then - coverage run -m pytest -rw -s $target - TEST_RESULT=$? - COVERAGE_REPORT=$(coverage report -m) - echo "$COVERAGE_REPORT" - else - pytest -rw -s $target - TEST_RESULT=$? - fi - - return $TEST_RESULT; -} - -run_integration_tests(){ - clean - # Make sure the version of python used by the git hooks in our integration tests - # is the same one as the one that is currently active. In order to achieve this, we need to set - # GIT_EXEC_PATH (https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables) to the current PATH, otherwise - # the git hooks will use the default PATH variable as defined by .bashrc which doesn't contain the current - # virtualenv's python binary path. - export GIT_EXEC_PATH="$PATH" - - echo "" - gitlint --version - echo -e "Using $(which gitlint)\n" - - # py.test -s => print standard output (i.e. show print statement output) - # -rw => print warnings - target=${testargs:-"qa/"} - py.test -s $target -} - -run_git_check(){ - echo -ne "Running gitlint...${RED}" - RESULT=$(gitlint $testargs 2>&1) - local exit_code=$? - handle_test_result $exit_code "$RESULT" - # FUTURE: check if we use str() function: egrep -nriI "( |\(|\[)+str\(" gitlint | egrep -v "\w*#(.*)" - return $exit_code -} - -run_lint_check(){ - echo -ne "Running pylint...${RED}" - target=${testargs:-"gitlint-core/gitlint qa"} - RESULT=$(pylint $target --rcfile=".pylintrc" -r n) - local exit_code=$? - handle_test_result $exit_code "$RESULT" - return $exit_code -} - -run_build_test(){ - clean - datestr=$(date +"%Y-%m-%d-%H-%M-%S") - temp_dir="/tmp/gitlint-build-test-$datestr" - - # Copy gitlint to a new temp dir - echo -n "Copying gitlint to $temp_dir..." - mkdir "$temp_dir" - rsync -az --exclude ".git" --exclude ".venv*" . "$temp_dir" - echo -e "${GREEN}DONE${NO_COLOR}" - - # Update the version to include a timestamp - echo -n "Writing new version to file..." - version_file="$temp_dir/gitlint-core/gitlint/__init__.py" - version_str="$(cat $version_file)" - version_str="${version_str:0:${#version_str}-1}-$datestr\"" - echo "$version_str" > $version_file - echo -e "${GREEN}DONE${NO_COLOR}" - # Attempt to build the package - echo "Building package ..." - pushd "$temp_dir/gitlint-core" - # Copy stdout file descriptor so we can both print output to stdout as well as capture it in a variable - # https://stackoverflow.com/questions/12451278/bash-capture-stdout-to-a-variable-but-still-display-it-in-the-console - exec 5>&1 - output=$(python setup.py sdist bdist_wheel | tee /dev/fd/5) - local exit_code=$? - popd - # Cleanup :-) - rm -rf "$temp_dir" - - # Print success/no success - if [ $exit_code -gt 0 ]; then - echo -e "Building package...${RED}FAIL${NO_COLOR}" - else - echo -e "Building package...${GREEN}SUCCESS${NO_COLOR}" - fi - - return $exit_code -} - -run_stats(){ - clean # required for py.test to count properly - echo "*** Code ***" - radon raw -s gitlint | tail -n 11 - echo "*** Docs ***" - echo " Markdown: $(cat docs/*.md | wc -l | tr -d " ") lines" - echo "*** Tests ***" - nr_unit_tests=$(py.test gitlint-core/ --collect-only | grep TestCaseFunction | wc -l) - nr_integration_tests=$(py.test qa/ --collect-only | grep TestCaseFunction | wc -l) - echo " Unit Tests: ${nr_unit_tests//[[:space:]]/}" - echo " Integration Tests: ${nr_integration_tests//[[:space:]]/}" - echo "*** Git ***" - echo " Commits: $(git rev-list --all --count)" - echo " Commits (main): $(git rev-list main --count)" - echo " First commit: $(git log --pretty="%aD" $(git rev-list --max-parents=0 HEAD))" - echo " Contributors: $(git log --format='%aN' | sort -u | wc -l | tr -d ' ')" - echo " Releases (tags): $(git tag --list | wc -l | tr -d ' ')" - latest_tag=$(git tag --sort=creatordate | tail -n 1) - echo " Latest Release (tag): $latest_tag" - echo " Commits since $latest_tag: $(git log --format=oneline HEAD...$latest_tag | wc -l | tr -d ' ')" - echo " Line changes since $latest_tag: $(git diff --shortstat $latest_tag)" - # PyPi API: https://pypistats.org/api/ - echo "*** PyPi ***" - info=$(curl -Ls https://pypi.python.org/pypi/gitlint/json) - echo " Current version: $(echo $info | jq -r .info.version)" - echo "*** PyPi (Downloads) ***" - overall_stats=$(curl -s https://pypistats.org/api/packages/gitlint/overall) - recent_stats=$(curl -s https://pypistats.org/api/packages/gitlint/recent) - echo " Last 6 Months: $(echo $overall_stats | jq -r '.data[].downloads' | awk '{sum+=$1} END {print sum}')" - echo " Last Month: $(echo $recent_stats | jq .data.last_month)" - echo " Last Week: $(echo $recent_stats | jq .data.last_week)" - echo " Last Day: $(echo $recent_stats | jq .data.last_day)" -} - -clean(){ - echo -n "Cleaning the *.pyc, site/, build/, dist/ and all __pycache__ directories..." - find gitlint-core qa -type d -name "__pycache__" -exec rm -rf {} \; 2> /dev/null - find gitlint-core qa -iname "*.pyc" -exec rm -rf {} \; 2> /dev/null - rm -rf "site" "dist" "build" "gitlint-core/dist" "gitlint-core/build" - echo -e "${GREEN}DONE${NO_COLOR}" -} - -run_all(){ - local exit_code=0 - subtitle "# UNIT TESTS ($(python --version 2>&1), $(which python)) #" - run_unit_tests - exit_code=$((exit_code + $?)) - subtitle "# INTEGRATION TESTS ($(python --version 2>&1), $(which python)) #" - run_integration_tests - exit_code=$((exit_code + $?)) - subtitle "# BUILD TEST ($(python --version 2>&1), $(which python)) #" - run_build_test - exit_code=$((exit_code + $?)) - subtitle "# STYLE CHECKS ($(python --version 2>&1), $(which python)) #" - run_formatting_check - exit_code=$((exit_code + $?)) - run_lint_check - exit_code=$((exit_code + $?)) - run_git_check - exit_code=$((exit_code + $?)) - return $exit_code -} - -uninstall_virtualenv(){ - version="$1" - venv_name=".venv$version" - echo -n "Uninstalling $venv_name..." - deactivate 2> /dev/null # deactivate any active environment - rm -rf "$venv_name" - echo -e "${GREEN}DONE${NO_COLOR}" -} - -install_virtualenv(){ - version="$1" - venv_name=".venv$version" - - # For regular python: the binary has a dot between the first and second char of the version string - python_binary="/usr/bin/python${version:0:1}.${version:1:1}" - - # For pypy: custom path + fetch from the web if not installed (=distro agnostic) - if [[ $version == *"pypy"* ]]; then - pypy_download_mirror="https://downloads.python.org/pypy" - if [[ $version == *"pypy36"* ]]; then - pypy_full_version="pypy3.6-v7.3.2-linux64" - elif [[ $version == *"pypy37"* ]]; then - pypy_full_version="pypy3.7-v7.3.2-linux64" - fi - - python_binary="/opt/$pypy_full_version/bin/pypy" - pypy_archive="$pypy_full_version.tar.bz2" - if [ ! -f $python_binary ]; then - assert_root "Must be root to install $version, use sudo" - title "### DOWNLOADING $version ($pypy_archive) ###" - pushd "/opt" - wget "$pypy_download_mirror/$pypy_archive" - title "### EXTRACTING PYPY TARBALL ($pypy_archive) ###" - tar xvf $pypy_archive - popd - fi - fi - - title "### INSTALLING $venv_name ($python_binary) ###" - deactivate 2> /dev/null # deactivate any active environment - virtualenv -p "$python_binary" "$venv_name" - source "${venv_name}/bin/activate" - pip install --ignore-requires-python -r requirements.txt - pip install --ignore-requires-python -r test-requirements.txt - deactivate 2> /dev/null -} - -container_name(){ - echo "jorisroovers/gitlint:dev-python-$1" -} - -start_container(){ - container_name="$1" - echo -n "Starting container $1..." - container_details=$(docker container inspect $container_name 2>&1 > /dev/null) - local exit_code=$? - if [ $exit_code -gt 0 ]; then - docker run -t -d -v $(pwd):/gitlint --name $container_name $container_name - exit_code=$? - echo -e "${GREEN}DONE${NO_COLOR}" - else - echo -e "${YELLOW}SKIP (ALREADY RUNNING)${NO_COLOR}" - exit_code=0 - fi - return $exit_code -} - -stop_container(){ - container_name="$1" - echo -n "Stopping container $container_name..." - result=$(docker kill $container_name 2> /dev/null) - local exit_code=$? - if [ $exit_code -gt 0 ]; then - echo -e "${YELLOW}SKIP (DOES NOT EXIST)${NO_COLOR}" - exit_code=0 - else - echo -e "${GREEN}DONE${NO_COLOR}" - fi - return $exit_code -} - -install_container(){ - local exit_code=0 - python_version="$1" - python_version_dotted="${python_version:0:1}.${python_version:1:1}" - container_name="$(container_name $python_version)" - - title "Installing container $container_name" - image_details=$(docker image inspect $container_name 2> /dev/null) - tmp_exit_code=$? - if [ $tmp_exit_code -gt 0 ]; then - subtitle "Building container image from python:${python_version_dotted}-stretch..." - docker build -f Dockerfile.dev --build-arg python_version_dotted="$python_version_dotted" -t $container_name . - exit_code=$? - else - subtitle "Building container image from python:${python_version_dotted}-stretch...SKIP (ALREADY-EXISTS)" - echo " Use '$0 --uninstall-container; $0 --install-container' to rebuild" - exit_code=0 - fi - return $exit_code -} - -uninstall_container(){ - python_version="$1" - container_name="$(container_name $python_version)" - - echo -n "Removing container image $container_name..." - image_details=$(docker image inspect $container_name 2> /dev/null) - tmp_exit_code=$? - if [ $tmp_exit_code -gt 0 ]; then - echo -e "${YELLOW}SKIP (DOES NOT EXIST)${NO_COLOR}" - exit_code=0 - else - result=$(docker image rm -f $container_name 2> /dev/null) - exit_code=$? - fi - return $exit_code -} - -assert_specific_env(){ - if [ -z "$1" ] || [ "$1" == "default" ]; then - fatal "ERROR: Please specify one or more valid python environments using --envs: 36,37,38,39,pypy37" - exit 1 - fi -} - -switch_env(){ - if [ "$1" != "default" ]; then - # If we activated a virtualenv within this script, deactivate it - deactivate 2> /dev/null # deactivate any active environment - - # If this script was run from within an existing virtualenv, manually remove the current VIRTUAL_ENV from the - # current path. This ensures that our PATH is clean of that virtualenv. - # Note that the 'deactivate' function from the virtualenv is not available here unless the script was invoked - # as 'source ./run_tests.sh'). - # Thanks internet stranger! https://unix.stackexchange.com/a/496050/38465 - if [ ! -z "$VIRTUAL_ENV" ]; then - export PATH=$(echo $PATH | tr ":" "\n" | grep -v "$VIRTUAL_ENV" | tr "\n" ":"); - fi - set -e # Let's error out if you try executing against a non-existing env - source ".venv${1}/bin/activate" - set +e - fi - title "### PYTHON ($(python --version 2>&1), $(which python)) ###" -} - -run_in_container(){ - python_version="$1" - envs="$2" - args="$3" - container_name="$(container_name $python_version)" - container_command=$(echo "$0 $args" | sed -E "s/( -e | --envs )$envs//" | sed -E "s/( --container| -C)//") - - title "### CONTAINER $container_name" - start_container "$container_name" - docker exec "$container_name" $container_command -} -############################################################################## -# The magic starts here: argument parsing and determining what to do - - -# default behavior -just_formatting=0 -just_lint=0 -just_git=0 -just_integration_tests=0 -just_build_tests=0 -just_stats=0 -just_all=0 -just_clean=0 -just_install=0 -just_uninstall=0 -just_install_container=0 -just_uninstall_container=0 -just_exec=0 -container_enabled=0 -include_coverage=1 -envs="default" -cmd="" -testargs="" -original_args="$@" -while [ "$#" -gt 0 ]; do - case "$1" in - -h|--help) shift; help;; - -c|--clean) shift; just_clean=1;; - -f|--format) shift; just_formatting=1;; - -l|--lint) shift; just_lint=1;; - -g|--git) shift; just_git=1;; - -b|--build) shift; just_build_tests=1;; - -s|--stats) shift; just_stats=1;; - -i|--integration) shift; just_integration_tests=1;; - -a|--all) shift; just_all=1;; - -e|--envs) shift; envs="$1"; shift;; - --exec) shift; just_exec=1; cmd="$1"; shift;; - --install) shift; just_install=1;; - --uninstall) shift; just_uninstall=1;; - --install-container) shift; just_install_container=1;; - --uninstall-container) shift; just_uninstall_container=1;; - --all-env) shift; envs="all";; - -C|--container) shift; container_enabled=1;; - --no-coverage)shift; include_coverage=0;; - *) testargs="$1"; shift; - esac -done - -old_virtualenv="$VIRTUAL_ENV" # Store the current virtualenv so we can restore it at the end - -trap exit_script INT # Exit on interrupt (i.e. ^C) -exit_script(){ - echo -e -n $NO_COLOR # make sure we don't have color left on the terminal - exit -} - -exit_code=0 - -# If the users specified 'all', then just replace $envs with the list of all envs -if [ "$envs" == "all" ]; then - envs="36,37,38,39,pypy37" -fi -original_envs="$envs" -envs=$(echo "$envs" | tr ',' '\n') # Split the env list on comma so we can loop through it - -for environment in $envs; do - - if [ $container_enabled -eq 1 ]; then - run_in_container "$environment" "$original_envs" "$original_args" - elif [ $just_formatting -eq 1 ]; then - switch_env "$environment" - run_formatting_check - elif [ $just_stats -eq 1 ]; then - switch_env "$environment" - run_stats - elif [ $just_integration_tests -eq 1 ]; then - switch_env "$environment" - run_integration_tests - elif [ $just_build_tests -eq 1 ]; then - switch_env "$environment" - run_build_test - elif [ $just_git -eq 1 ]; then - switch_env "$environment" - run_git_check - elif [ $just_lint -eq 1 ]; then - switch_env "$environment" - run_lint_check - elif [ $just_all -eq 1 ]; then - switch_env "$environment" - run_all - elif [ $just_clean -eq 1 ]; then - switch_env "$environment" - clean - elif [ $just_exec -eq 1 ]; then - switch_env "$environment" - eval "$cmd" - elif [ $just_uninstall -eq 1 ]; then - assert_specific_env "$environment" - uninstall_virtualenv "$environment" - elif [ $just_install -eq 1 ]; then - assert_specific_env "$environment" - install_virtualenv "$environment" - elif [ $just_install_container -eq 1 ]; then - assert_specific_env "$environment" - install_container "$environment" - elif [ $just_uninstall_container -eq 1 ]; then - assert_specific_env "$environment" - uninstall_container "$environment" - else - switch_env "$environment" - run_unit_tests - fi - # We add up all the exit codes and use that as our final exit code - # While we lose the meaning of the exit code per individual environment by doing this, we do ensure that the end - # exit code reflects success (=0) or failure (>0). - exit_code=$((exit_code + $?)) -done - -# reactivate the virtualenv if we had one before -if [ ! -z "$old_virtualenv" ]; then - source "$old_virtualenv/bin/activate" -fi - -# Report some overall status -if [ $exit_code -eq 0 ]; then - echo -e "\n${GREEN}### OVERALL STATUS: SUCCESS ###${NO_COLOR}" -else - echo -e "\n${RED}### OVERALL STATUS: FAILURE ###${NO_COLOR}" -fi - -exit $exit_code diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 7c2b287..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal = 1
\ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index b94cd50..0000000 --- a/setup.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python -from __future__ import print_function -from setuptools import setup - -description = "Git commit message linter written in python, checks your commit messages for style." -long_description = """ -Great for use as a commit-msg git hook or as part of your gating script in a CI pipeline (e.g. jenkins, github actions). -Many of the gitlint validations are based on `well-known`_ community_ `standards`_, others are based on checks that -we've found useful throughout the years. Gitlint has sane defaults, but you can also easily customize it to your -own liking. - -Demo and full documentation on `jorisroovers.github.io/gitlint`_. -To see what's new in the latest release, visit the CHANGELOG_. - -Source code on `github.com/jorisroovers/gitlint`_. - -.. _well-known: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html -.. _community: http://addamhardy.com/blog/2013/06/05/good-commit-messages-and-enforcing-them-with-git-hooks/ -.. _standards: http://chris.beams.io/posts/git-commit/ -.. _jorisroovers.github.io/gitlint: https://jorisroovers.github.io/gitlint -.. _CHANGELOG: https://github.com/jorisroovers/gitlint/blob/main/CHANGELOG.md -.. _github.com/jorisroovers/gitlint: https://github.com/jorisroovers/gitlint -""" - - -version = "0.19.0dev" - -setup( - name="gitlint", - version=version, - description=description, - long_description=long_description, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "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", - ], - python_requires=">=3.6", - install_requires=[ - "gitlint-core[trusted-deps]==" + version, - ], - 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", - "Changelog": "https://github.com/jorisroovers/gitlint/blob/main/CHANGELOG.md", - }, - license="MIT", -) diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 149c36a..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -black==22.8.0 -coverage==6.2; python_version == '3.6' -coverage==6.4.4; python_version != '3.6' -python-coveralls==2.9.3 -radon==5.1.0 -pytest==7.0.1 -pylint==2.13.7; python_version == '3.6' -pylint==2.15.3; python_version != '3.6' -pdbr==0.6.6; sys_platform != "win32" --r requirements.txt diff --git a/tools/changelog.py b/tools/changelog.py new file mode 100755 index 0000000..24b74a8 --- /dev/null +++ b/tools/changelog.py @@ -0,0 +1,51 @@ +# ruff: noqa: T201 (Allow print statements) +# Simple script to generate a rough changelog from git log. +# This changelog is manually edited before it goes into CHANGELOG.md + +import re +import subprocess +import sys +from collections import defaultdict + +if len(sys.argv) != 2: + print("Usage: python changelog.py <tag>") + sys.exit(1) + +tag = sys.argv[1] +# Get all commits since the last release +cmd = ["git", "log", "--pretty=%s|%aN", f"{tag}..HEAD"] +log_lines = subprocess.Popen(cmd, stdout=subprocess.PIPE).stdout.read().decode("UTF-8") +log_lines = log_lines.split("\n")[:-1] + +# Group commits by type +commit_groups = defaultdict(list) +for log_line in log_lines: + message, author = log_line.split("|") + # skip dependabot commits + if author == "dependabot[bot]": + group = "dependabot" + else: + type_parts = message.split(":") + if len(type_parts) == 1: + group = "other" + else: + group = type_parts[0] + + commit_groups[group].append((message, author)) + +# Print the changelog +for group, commits in commit_groups.items(): + print(group) + for message, author in commits: + # Thank authors other than maintainer + author_thanks = "" + if author != "Joris Roovers": + author_thanks = f" - Thanks {author}" + + # Find the issue number in message using regex, format: (#1234) + issue_number = re.search(r"\(#(\d+)\)", message) + if issue_number: + issue_url = f"https://github.com/jorisroovers/gitlint/issues/{issue_number.group(1)}" + message = message.replace(issue_number.group(0), f"([#{issue_number.group(1)}]({issue_url}))") + + print(f" - {message}{author_thanks}") diff --git a/tools/stats.sh b/tools/stats.sh new file mode 100755 index 0000000..ada2658 --- /dev/null +++ b/tools/stats.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Script that displays some interesting stats about the gitlint project (LOC, # commits, downloads, etc) + +BLUE="\033[94m" +NO_COLOR="\033[0m" + +title(){ + echo -e "$BLUE=== $1 ===$NO_COLOR" +} + +title Code +radon raw -s gitlint-core | tail -n 11 | sed 's/^ //' + +title Docs +echo "Markdown: $(cat docs/*.md | wc -l | tr -d " ") lines" + +title Tests +nr_unit_tests=$(py.test gitlint-core/ --collect-only | grep TestCaseFunction | wc -l) +nr_integration_tests=$(py.test qa/ --collect-only | grep TestCaseFunction | wc -l) +echo "Unit Tests: ${nr_unit_tests//[[:space:]]/}" +echo "Integration Tests: ${nr_integration_tests//[[:space:]]/}" + +title Git +echo "Commits: $(git rev-list --all --count)" +echo "Commits (main): $(git rev-list main --count)" +echo "First commit: $(git log --pretty="%aD" $(git rev-list --max-parents=0 HEAD))" +echo "Contributors: $(git log --format='%aN' | sort -u | wc -l | tr -d ' ')" +echo "Releases (tags): $(git tag --list | wc -l | tr -d ' ')" +latest_tag=$(git tag --sort=creatordate | tail -n 1) +echo "Latest Release (tag): $latest_tag" +echo "Commits since $latest_tag: $(git log --format=oneline HEAD...$latest_tag | wc -l | tr -d ' ')" +echo "Line changes since $latest_tag: $(git diff --shortstat $latest_tag)" + +# PyPi API: https://pypistats.org/api/ +title PyPi +info=$(curl -Ls https://pypi.python.org/pypi/gitlint/json) +echo "Current version: $(echo $info | jq -r .info.version)" + +title "PyPI (Downloads)" +overall_stats=$(curl -s https://pypistats.org/api/packages/gitlint/overall) +recent_stats=$(curl -s https://pypistats.org/api/packages/gitlint/recent) +echo "Last 6 Months: $(echo $overall_stats | jq -r '.data[].downloads' | awk '{sum+=$1} END {print sum}')" +echo "Last Month: $(echo $recent_stats | jq .data.last_month)" +echo "Last Week: $(echo $recent_stats | jq .data.last_week)" +echo "Last Day: $(echo $recent_stats | jq .data.last_day)"
\ No newline at end of file |