diff --git a/.codacy.yaml b/.codacy.yaml new file mode 100644 index 0000000..cab9740 --- /dev/null +++ b/.codacy.yaml @@ -0,0 +1,3 @@ +exclude_paths: + - 'tests/**' + - 'docs/**' diff --git a/.github/.codecov.yml b/.github/.codecov.yml new file mode 100644 index 0000000..85b43f3 --- /dev/null +++ b/.github/.codecov.yml @@ -0,0 +1,7 @@ +coverage: + status: + project: + default: + # minimum of 97% (real 96%) + target: 97% + threshold: 1% diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..5d29c85 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @woile @Lee-W @noirbizarre diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..68203f2 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +open_collective: commitizen-tools +github: commitizen-tools diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..10d782d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,61 @@ +name: ๐Ÿ›  Bug report +description: Create a report to help us improve +title: Good bug title tells us about precise symptom, not about the root cause. +labels: [bug] +body: + - type: textarea + id: description + attributes: + label: Description + description: | + A clear and concise description of what the bug is + validations: + required: true + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to reproduce + description: Steps to reproduce the behavior + placeholder: | + 1. Run ... + 2. ... + 3. ... + validations: + required: true + - type: textarea + id: current-behavior + attributes: + label: Current behavior + description: What happens actually so you think this is a bug. + validations: + required: true + - type: textarea + id: desired-behavior + attributes: + label: Desired behavior + description: | + A clear and concise description of what you expected to happen. + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: | + If applicable, add screenshots to help explain your problem. + - type: textarea + id: environment + attributes: + label: Environment + description: | + For older commitizen versions, please include the output of the following commands manually + placeholder: | + - commitizen version: `cz version` + - python version: `python --version` + - operating system: `python3 -c "import platform; print(platform.system())"` + + ```bash + cz version --report + ``` + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..884fe16 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +# Configuration: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository + +blank_issues_enabled: false +contact_links: + - name: Security Contact + about: Please report security vulnerabilities to santiwilly@gmail.com diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 0000000..51d378b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,29 @@ +name: ๐Ÿ“– Documentation +description: Suggest an improvement for the documentation of this project +title: Content to be added or fixed +labels: [documentation] +body: + - type: checkboxes + id: type + attributes: + label: Type + options: + - label: Content inaccurate + - label: Content missing + - label: Typo + - type: input + id: url + attributes: + label: URL + placeholder: | + URL to the code we did not clearly describe or the document page where the content is inaccurate + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + description: | + A clear and concise description of what content should be added or fixed + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..7d67eb1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,31 @@ +name: ๐Ÿš€ Feature request +description: Suggest an idea for this project +title: "" +labels: [feature] +body: + - type: textarea + id: description + attributes: + label: Description + description: | + A clear and concise description for us to know your idea. + validations: + required: true + - type: textarea + id: possible-solution + attributes: + label: Possible Solution + description: | + A clear and concise description of what you want to happen. + - type: textarea + id: additional-context + attributes: + label: Additional context + description: | + Add any other context or screenshots about the feature request here. + - type: textarea + id: related-issue + attributes: + label: Additional context + description: | + If applicable, add link to existing issue also help us know better. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..82fb674 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +version: 2 +updates: + - + # Maintain dependencies for GitHub Actions + package-ecosystem: github-actions + directory: / + schedule: + interval: daily + labels: + - dependencies + commit-message: + prefix: "ci" + include: "scope" + - + # Maintain python dependencies + package-ecosystem: pip + directory: / + schedule: + interval: daily + labels: + - dependencies + commit-message: + prefix: "build" + include: "scope" diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..5f3f5b8 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,3 @@ +'pr-status: wait-for-review': +- changed-files: + - any-glob-to-any-file: '**' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..0064604 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,29 @@ + + +## Description + + + +## Checklist + +- [ ] Add test cases to all the changes you introduce +- [ ] Run `poetry all` locally to ensure this change passes linter check and test +- [ ] Test the changes on the local machine manually +- [ ] Update the documentation for the changes + +## Expected behavior + + + +## Steps to Test This Pull Request + + + +## Additional context + diff --git a/.github/workflows/bumpversion.yml b/.github/workflows/bumpversion.yml new file mode 100644 index 0000000..ed0c8cf --- /dev/null +++ b/.github/workflows/bumpversion.yml @@ -0,0 +1,29 @@ +name: Bump version + +on: + push: + branches: + - master + +jobs: + bump-version: + if: "!startsWith(github.event.head_commit.message, 'bump:')" + runs-on: ubuntu-latest + name: "Bump version and create changelog with commitizen" + steps: + - name: Check out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" + - name: Create bump and changelog + uses: commitizen-tools/commitizen-action@master + with: + github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + changelog_increment_filename: body.md + - name: Release + uses: ncipollo/release-action@v1 + with: + tag: v${{ env.REVISION }} + bodyFile: "body.md" + skipIfReleaseExists: true diff --git a/.github/workflows/docspublish.yml b/.github/workflows/docspublish.yml new file mode 100644 index 0000000..a871d3c --- /dev/null +++ b/.github/workflows/docspublish.yml @@ -0,0 +1,78 @@ +name: Publish documentation + +on: + push: + branches: + - master + +jobs: + update-cli-screenshots: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install -U pip poetry poethepoet + poetry --version + poetry install --only main,script + - name: Update CLI screenshots + run: | + poetry doc:screenshots + - name: Commit and push updated CLI screenshots + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add docs/images/cli_help + + if [[ -n "$(git status --porcelain)" ]]; then + git commit -m "docs(cli/screenshots): update CLI screenshots" -m "[skip ci]" + git push + else + echo "No changes to commit. Skipping." + fi + + publish-documentation: + runs-on: ubuntu-latest + needs: update-cli-screenshots + steps: + - uses: actions/checkout@v4 + with: + token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" + fetch-depth: 0 + - name: Pull latest changes + run: | + git pull origin master + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install -U pip poetry poethepoet + poetry --version + poetry install --no-root --only documentation + - name: Build docs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + poetry doc:build + - name: Generate Sponsors ๐Ÿ’– + uses: JamesIves/github-sponsors-readme-action@v1 + with: + token: ${{ secrets.PERSONAL_ACCESS_TOKEN_FOR_ORG }} + file: "docs/README.md" + - name: Push doc to Github Page + uses: peaceiris/actions-gh-pages@v4 + with: + personal_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + publish_branch: gh-pages + publish_dir: ./site + user_name: "github-actions[bot]" + user_email: "github-actions[bot]@users.noreply.github.com" diff --git a/.github/workflows/homebrewpublish.yml b/.github/workflows/homebrewpublish.yml new file mode 100644 index 0000000..443a13b --- /dev/null +++ b/.github/workflows/homebrewpublish.yml @@ -0,0 +1,32 @@ +name: Publish to Homebrew + +on: + workflow_run: + workflows: ["Upload Python Package"] + types: + - completed + +jobs: + deploy: + runs-on: macos-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install -U commitizen + - name: Set Project version env variable + run: | + echo "project_version=$(cz version --project)" >> $GITHUB_ENV + - name: Update Homebrew formula + uses: dawidd6/action-homebrew-bump-formula@v4 + with: + token: ${{secrets.PERSONAL_ACCESS_TOKEN}} + formula: commitizen + tag: v${{ env.project_version }} + force: true diff --git a/.github/workflows/label_issues.yml b/.github/workflows/label_issues.yml new file mode 100644 index 0000000..45ca450 --- /dev/null +++ b/.github/workflows/label_issues.yml @@ -0,0 +1,23 @@ +name: Label issues + +on: + issues: + types: + - opened + - reopened + +jobs: + label-issue: + permissions: + issues: write + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + script: | + github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['issue-status: needs-triage'] + }) diff --git a/.github/workflows/label_pr.yml b/.github/workflows/label_pr.yml new file mode 100644 index 0000000..b409c8b --- /dev/null +++ b/.github/workflows/label_pr.yml @@ -0,0 +1,19 @@ +name: "Label Pull Request" +on: +- pull_request_target + +jobs: + label-pr: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: | + .github/labeler.yml + sparse-checkout-cone-mode: false + - uses: actions/labeler@v5 + with: + configuration-path: .github/labeler.yml diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml new file mode 100644 index 0000000..f236374 --- /dev/null +++ b/.github/workflows/pythonpackage.yml @@ -0,0 +1,38 @@ +name: Python package + +on: [workflow_dispatch, pull_request] + +jobs: + python-check: + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + platform: [ubuntu-20.04, macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install -U pip poetry poethepoet + poetry --version + poetry install --only main,linters,test + - name: Run tests and linters + run: | + git config --global user.email "action@github.com" + git config --global user.name "GitHub Action" + poetry ci + shell: bash + - name: Upload coverage to Codecov + if: runner.os == 'Linux' + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml + flags: unittests + name: codecov-umbrella diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml new file mode 100644 index 0000000..dc522bc --- /dev/null +++ b/.github/workflows/pythonpublish.yml @@ -0,0 +1,28 @@ +name: Upload Python Package + +on: + push: + tags: + - "v*" + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install -U pip poetry + poetry --version + - name: Publish + env: + POETRY_HTTP_BASIC_PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }} + POETRY_HTTP_BASIC_PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: poetry publish --build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be07cf2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,115 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +.static_storage/ +.media/ +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +.idea +.vscode/ +*.bak + +# macOSX +.DS_Store + +# ruff +.ruff_cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..1373277 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,72 @@ +default_install_hook_types: + - pre-commit + - commit-msg + - pre-push + +default_stages: + - pre-commit + +repos: + - repo: meta + hooks: + - id: check-hooks-apply + - id: check-useless-excludes + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-vcs-permalinks + - id: end-of-file-fixer + exclude: "tests/((commands|data|providers/test_uv_provider)/|test_).+" + - id: trailing-whitespace + args: [ --markdown-linebreak-ext=md ] + exclude: '\.svg$' + - id: debug-statements + - id: no-commit-to-branch + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + args: [ '--unsafe' ] # for mkdocs.yml + - id: detect-private-key + + - repo: https://github.com/asottile/blacken-docs + rev: 1.19.1 + hooks: + - id: blacken-docs + additional_dependencies: [ black~=23.11 ] + + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + name: Run codespell to check for common misspellings in files + # config section is within pyproject.toml + language: python + types: [ text ] + args: [ "--write-changes" ] + additional_dependencies: + - tomli + + - repo: https://github.com/commitizen-tools/commitizen + rev: v4.6.0 # automatically updated by Commitizen + hooks: + - id: commitizen + - id: commitizen-branch + stages: + - post-commit + + - repo: local + hooks: + - id: format + name: Format + language: system + pass_filenames: false + entry: poetry format + types: [ python ] + + - id: linter and test + name: Linters + language: system + pass_filenames: false + entry: poetry lint + types: [ python ] diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..2a3a088 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,27 @@ +- id: commitizen + name: commitizen check + description: > + Check whether the current commit message follows committing rules. Allow + empty commit messages by default, because they typically indicate to Git + that the commit should be aborted. + entry: cz check + args: [--allow-abort, --commit-msg-file] + stages: [commit-msg] + language: python + language_version: python3 + minimum_pre_commit_version: "1.4.3" + +- id: commitizen-branch + name: commitizen check branch + description: > + Check all commit messages that are already on the current branch but not the + default branch on the origin repository. Useful for checking messages after + the fact (e.g., pre-push or in CI) without an expensive check of the entire + repository history. + entry: cz check + args: [--rev-range, origin/HEAD..HEAD] + always_run: true + pass_filenames: false + language: python + language_version: python3 + minimum_pre_commit_version: "1.4.3" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1d2ab1e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1986 @@ +## v4.6.0 (2025-04-13) + +### Feat + +- **changelog**: expose commit parents' digests when processing commits +- **git**: add parents' digests in commit information + +## v4.5.1 (2025-04-09) + +### Fix + +- print which tag is invalid + +## v4.5.0 (2025-04-04) + +### Feat + +- **init**: set uv to default value if both pyproject.toml and uv.lock present + +### Fix + +- **commands/init**: add missing uv provider to "cz init" + +## v4.4.1 (2025-03-02) + +### Fix + +- **tags**: fixes ImportError on Python >=3.11 (#1363) (#1364) + +## v4.4.0 (2025-03-02) + +### Feat + +- **tags**: adds `legacy_tag_formats` and `ignored_tag_formats` settings + +### Refactor + +- **get_tag_regexes**: dedup tag regexes definition + +## v4.3.0 (2025-02-28) + +### Feat + +- **providers**: add uv_provider + +## v4.2.2 (2025-02-18) + +### Fix + +- **bump**: manual version bump if prerelease offset is configured + +## v4.2.1 (2025-02-08) + +### Fix + +- **bump**: add debugging to bump + +## v4.2.0 (2025-02-07) + +### Feat + +- draft of the --empty parameter + +### Refactor + +- **bump**: rename --empty as --allow-no-commit + +## v4.1.1 (2025-01-26) + +### Fix + +- **get-next-bump**: add a test case +- **get-next-bump**: fix to permit usage of --get-next options even when update_changelog_on_bump is set to true + +## v4.1.0 (2024-12-06) + +### Feat + +- **commit**: allow '-- --allow-empty' to create empty commits + +## v4.0.0 (2024-11-26) + +## v3.31.0 (2024-11-16) + +### Feat + +- **commitizen**: document '--' double dash in '--help' + +### Fix + +- **commit**: avoid warnings with 'always_signoff' configuration +- **commit**: resolve 'always_signoff' configuration and '-s' CLI issues + +## v3.30.1 (2024-11-10) + +### Refactor + +- **cli**: replace magic number 0 with ExitCode.EXPECTED_EXIT +- **defaults**: disallow style as None +- **cz_customize**: return empty string for info, example, schema and schema_pattern if not provided + +## v3.30.0 (2024-10-23) + +### Feat + +- **commands/commit**: add force-edit functionality after answering questions + +### Refactor + +- remove redundant return None + +## v3.29.1 (2024-09-26) + +### Fix + +- **changelog**: Factorized TAG_FORMAT_REGEXES +- **changelog**: Handle tag format without version pattern +- **changelog**: handle custom tag_format in changelog generation + +### Refactor + +- Use format strings + +## v3.29.0 (2024-08-11) + +### Feat + +- **bump**: add functionality to write the next version to stdout + +## v3.28.0 (2024-07-17) + +### Feat + +- add argument to limit length of commit message in checks + +## v3.27.0 (2024-05-22) + +### Feat + +- **config_files**: add support for "cz.toml" config file + +## v3.26.2 (2024-05-22) + +### Fix + +- **base.py**: add encoding when open changlelog_file + +## v3.26.1 (2024-05-22) + +### Fix + +- **cli/commands**: add description for subcommands + +### Refactor + +- **KNOWN_SCHEMES**: replace set comprehension for list comprehension +- **tests/commands**: move "other" tests for the correspondent file + +## v3.26.0 (2024-05-18) + +### Feat + +- **ci/cd**: automates the generation of CLI screenshots + +## v3.25.1 (2024-05-15) + +### Refactor + +- strip possessive from note about ci option + +## v3.25.0 (2024-04-30) + +### Feat + +- add an argument to limit the length of commit message + +### Fix + +- strip the commit message for calculating length +- resolve test error by removing defaults + +### Refactor + +- **commands/commit**: replace comparison with chained comparison +- check the length in Commit instead of Commitizen + +## v3.24.0 (2024-04-18) + +### Feat + +- **schemes**: adds support for SemVer 2.0 (dot in pre-releases) (fix #1025) (#1072) + +## v3.23.0 (2024-04-18) + +### Feat + +- **bump**: `version_files` now support glob patterns (fix #1067) (#1070) + +## v3.22.0 (2024-04-11) + +### Feat + +- **cli**: add config option to specify config file path + +## v3.21.3 (2024-03-30) + +### Refactor + +- **defaults**: move cz_conventional_commit defaults out of defaults.py + +## v3.21.2 (2024-03-30) + +### Fix + +- **commitizen/git.py,-tests/test_git.py**: Resolve tempfile path spaces issue in git commit function + +## v3.21.1 (2024-03-30) + +### Fix + +- **command-init**: "cz init" should list existing tag in reverse order + +## v3.21.0 (2024-03-30) + +### Feat + +- **commit**: add retry_after_failure config option and --no-retry flag + +### Refactor + +- **utils**: convert git project root to posix path for backup file name +- **commit**: use Optional[str] instead of str | None +- **commit**: remove unused tempfile import +- **git-hooks**: make git hooks use get_backup_file_path +- **utils**: move backup path creation to utils + +## v3.20.0 (2024-03-19) + +### Feat + +- **changelog**: expose commits `sha1`, `author` and `author_email` in changelog tree (fix #987) (#1013) + +## v3.19.0 (2024-03-19) + +### Feat + +- **changelog**: adds a `changelog_release_hook` called for each release in the changelog (#1018) + +## v3.18.4 (2024-03-14) + +### Fix + +- **changelog**: include latest change when dry run and incremental + +## v3.18.3 (2024-03-11) + +### Fix + +- **warnings**: all warnings should go to `stdout` + +## v3.18.2 (2024-03-11) + +### Fix + +- **git**: force the default git locale on methods relying on parsing the output (#1012) + +## v3.18.1 (2024-03-11) + +### Fix + +- **changelog**: changelog hook was not called on dry run + +## v3.18.0 (2024-03-07) + +### Feat + +- **changelog**: `changelog_message_build_hook` can now generate multiple changelog entries from a single commit (#1003) + +## v3.17.2 (2024-03-07) + +### Fix + +- **changelog**: ensure `changelog_message_builder_hook` can access and modify `change_type` (#1002) + +## v3.17.1 (2024-03-07) + +### Fix + +- **bump**: pre and post bump hooks were failing when an increment was provided (fix #1004) + +## v3.17.0 (2024-03-06) + +### Feat + +- **changelog**: `changelog_message_build_hook` can remove message by returning a falsy value + +## v3.16.0 (2024-02-26) + +### Feat + +- **commands**: add bump --exact + +### Fix + +- **bump**: change --exact-increment to --increment-mode +- **bump**: only get and validate commits if increment is not provided +- Improve type annotations + +## v3.15.0 (2024-02-17) + +### Feat + +- **bump**: functionality to add build-metadata to version string + +## v3.14.1 (2024-02-04) + +### Fix + +- **bump**: remove unused method +- **scm**: only search tags that are reachable by the current commit + +## v3.14.0 (2024-02-01) + +### Feat + +- properly bump versions between prereleases (#799) + +## v3.13.0 (2023-12-03) + +### Feat + +- **commands-bump**: automatically create annotated tag if message is given +- add tag message argument to cli +- **git**: add get tag message function +- add custom message to annotated git tag + +### Fix + +- **test-bump-command**: typo in --annotated-tag option inside test +- **commitizen-git**: add quotes for tag message + +### Refactor + +- **commands-bump**: make changelog variable in 1 line +- **commands-bump**: cast str to bool + +## v3.12.0 (2023-10-18) + +### Feat + +- **formats**: expose some new customizable changelog formats on the `commitizen.changelog_format` endpoint (Textile, AsciiDoc and RestructuredText) +- **template**: add `changelog --export-template` command +- **template**: allow to override the template from cli, configuration and plugins +- **cli.py**: Added support for extra git CLI args after -- separator for `cz commit` command + +### Fix + +- **filename**: ensure `file_name` can be passed to `changelog` from `bump` command + +### Refactor + +- **git.py**: Removed 'extra_args' from git.commit +- **extra_args**: Fixed broken code due to rebase and finalized tests +- Code Review - round 1 changes +- **Commit**: Added deprecation on git signoff mechanic + +## v3.10.1 (2023-10-14) + +### Fix + +- **bump**: add bump support with custom type + scope + exclamation mark +- **bump**: version bumping + +## v3.10.0 (2023-09-25) + +### Feat + +- Drop support for Python 3.7 (#858) + +## v3.9.1 (2023-09-22) + +### Fix + +- **conf**: handle parse error when init (#856) + +## v3.9.0 (2023-09-15) + +### Feat + +- **commands**: add arg of cz commit to execute git add + +### Fix + +- **tests**: modify the arg of commit from add to all +- **commitizen**: Modify the function of the arg a of commit from git add all to git add update + +### Refactor + +- **commitizen**: add return type hint of git add function + +## v3.8.2 (2023-09-09) + +### Refactor + +- **provider**: split provider code and related tests into individual files for maintainability (#830) + +## v3.8.1 (2023-09-08) + +### Fix + +- add sponsors to README + +## v3.8.0 (2023-09-05) + +### Feat + +- **defaults.py**: add always_signoff config option for commits + +## v3.7.1 (2023-09-04) + +### Fix + +- empty error on bump failure + +## v3.7.0 (2023-08-26) + +### Feat + +- **provider**: add npm2 provider to update package.json, package-lock.json, and npm-shrinkwrap.json + +### Fix + +- **provider**: fix npm version provider to update package-lock.json and npm-shrinkwrap.json if they exist +- **provider**: fix npm provider to update package-lock.json and npm-shrinkwrap.json if they exist +- **test**: pass correct type to get_package_version tests +- **tests**: completed test coverage for npm2 + +## v3.6.0 (2023-08-01) + +### Feat + +- **changelog.py**: add encoding to get_metadata +- **unicode**: add unicode support + +### Fix + +- add missing `encoding` parameter +- **out.py**: `TextIOWrapper.reconfigure` typing +- correct type hinting +- use base config for encoding + +### Refactor + +- **defaults.py**: use variables in `DEFAULT_SETTINGS` + +## v3.5.4 (2023-07-29) + +### Refactor + +- replace SemVer type literals by respective constants + +## v3.5.3 (2023-07-15) + +### Fix + +- Treat $version the same as unset tag_format in ScmProvider + +### Refactor + +- Make tag_format properly default to $version + +## v3.5.2 (2023-06-25) + +### Fix + +- **typing**: no_raise is declared as optional + +## v3.5.1 (2023-06-24) + +### Fix + +- only use version tags when generating a changelog + +## v3.5.0 (2023-06-23) + +### Feat + +- Add option in bump command to redirect git output to stderr + +## v3.4.1 (2023-06-23) + +### Fix + +- **veresion_schemes**: import missing Self for python 3.11 + +## v3.4.0 (2023-06-20) + +### Feat + +- **version-schemes**: expose `version_schemes` as a `commitizen.scheme` endpoint. + +## v3.3.0 (2023-06-13) + +### Feat + +- add support for cargo workspaces + +## v3.2.2 (2023-05-11) + +### Fix + +- **init**: fix is_pre_commit_installed method + +## v3.2.1 (2023-05-03) + +### Fix + +- add support for importlib_metadata 6 + +## v3.2.0 (2023-05-01) + +### Feat + +- **hooks**: add prepare-commit-msg and post-commit hooks +- **commit**: add --write-message-to-file option + +### Fix + +- **bump**: better match for change_type when finding increment +- **changelog**: breaking change on additional types for conventional commits +- **bump**: breaking changes on additional types for conventional commits +- improve errors message when empty .cz.json found +- **init**: poetry detection +- bump decli which is type hinted + +### Refactor + +- **commit**: change type of write_message_to_file to path + +## v3.1.1 (2023-04-28) + +### Fix + +- bump changelog for prerelease without commits + +## v3.1.0 (2023-04-25) + +### Feat + +- make `major_version_zero` customizable by third parties + +## v3.0.1 (2023-04-23) + +### Fix + +- typo in hook + +### Refactor + +- set default_install_hook_types + +## v3.0.0 (2023-04-23) + +### BREAKING CHANGE + +- Plugins are now exposed as `commitizen.plugin` entrypoints +- Python 3.6 is not officially supported anymore. Please migrate from 3.6 to 3.7 or greater. + +### Feat + +- **init**: add new settings +- add semver support through version provider new api (#686) +- **changelog**: add merge_prereleases flag +- **providers**: add a `scm` version provider +- **providers**: add support for some JSON-based version providers (NPM, Composer) +- **providers**: add support for some TOML-based versions (PEP621, Poetry, Cargo) +- **providers**: add a `commitizen.provider` endpoint for alternative versions providers +- **plugins**: Switch to an importlib.metadata.EntryPoint-based plugin loading + +### Fix + +- **init**: welcome message +- small corrections and clean up +- major version zero message +- update dependencies +- **commands/changelog**: use topological order for commit ordering +- **excepthook**: ensure traceback can only be a `TracebackType` or `None` + +## v2.42.1 (2023-02-25) + +### Fix + +- **bump**: fixed environment variables in bump hooks + +## v2.42.0 (2023-02-11) + +### Feat + +- **bump**: support prereleases with start offset + +## v2.41.0 (2023-02-08) + +### Feat + +- **bump**: added support for running arbitrary hooks during bump + +## v2.40.0 (2023-01-23) + +### Feat + +- **yaml_config**: add explicit_start for yaml output + +## v2.39.1 (2022-12-31) + +### Fix + +- filter git diff from commit message + +## v2.39.0 (2022-12-31) + +### Feat + +- **init**: allow user to select which type of pre commit hooks to install + +### Fix + +- **init**: space between `--hook-type` options +- **init**: report error when hook installation failed + +### Refactor + +- **init**: `_install_pre_commit_hook` raise error when failed + +## v2.38.0 (2022-12-12) + +### Feat + +- **poetry**: relax packaging version + +## v2.37.1 (2022-11-30) + +### Fix + +- **changelog**: allow rev range lookups without a tag format + +## v2.37.0 (2022-10-28) + +### Feat + +- add major-version-zero option to support initial package development + +## v2.36.0 (2022-10-28) + +### Feat + +- **scripts**: remove `venv/bin/` +- **scripts**: add error message to `test` + +### Fix + +- **scripts/test**: MinGW64 workaround +- **scripts/test**: use double-quotes +- **scripts**: pydocstyle and cz +- **bump.py**: use `sys.stdin.isatty()` +- **scripts**: use cross-platform POSIX +- **scripts**: use portable shebang +- **pythonpackage.yml**: undo indent reformatting +- **pythonpackage.yml**: use `bash` + +## v2.35.0 (2022-09-23) + +### Feat + +- allow fixup! and squash! in commit messages + +## v2.34.0 (2022-09-19) + +### Feat + +- **bump**: support optional manual version argument + +### Fix + +- **bump**: fix type hint +- **bump**: fix typos + +## v2.33.1 (2022-09-16) + +### Fix + +- **bump.py**: `CHANGELOG.md` gets git added and committed correctly + +## v2.33.0 (2022-09-15) + +### Feat + +- add functionality for dev-releases + +## v2.32.7 (2022-09-14) + +### Fix + +- **README.md**: fix pre-commit install command + +## v2.32.6 (2022-09-14) + +### Fix + +- **bump**: log git commit stderr and stdout during bump + +## v2.32.5 (2022-09-10) + +### Fix + +- **command_changelog**: Fixed issue #561 cz bump could not find the latest version tag with custom tag_format + +## v2.32.4 (2022-09-08) + +### Refactor + +- **bump**: Remove a redundant join call + +## v2.32.3 (2022-09-07) + +### Fix + +- **bump**: Search for version number line by line + +## v2.32.2 (2022-08-22) + +### Fix + +- **bump**: Support regexes containing colons + +## v2.32.1 (2022-08-21) + +### Fix + +- **git**: Improves error checking in get_tags +- **git**: improves git error checking in get_commits + +### Refactor + +- **git**: test the git log parser behaves properly when the repository has no commits +- **changelog**: fixes logic issue made evident by latest fix(git) commit + +## v2.32.0 (2022-08-21) + +### Feat + +- **pre-commit**: Add commitizen-branch hook + +## v2.31.0 (2022-08-14) + +### Feat + +- new file + +### Fix + +- **pyproject.toml**: remove test added configurations +- **changelog**: use defaults.change_type_order in conventional commit +- capitalize types in default change_type_order + +## v2.30.0 (2022-08-14) + +### Feat + +- Determine newline to write with Git + +## v2.29.6 (2022-08-13) + +### Fix + +- **cmd**: improve character encoding detection for sub-commands + +## v2.29.5 (2022-08-07) + +### Fix + +- **git**: use "git tag -v" return_code to check whether a tag is signed + +## v2.29.4 (2022-08-05) + +### Refactor + +- **tool**: use charset_normalizer instead of chardet + +## v2.29.3 (2022-08-02) + +### Refactor + +- **changelog**: removes unused code. duplicates are found in changelog_parser + +## v2.29.2 (2022-07-27) + +### Fix + +- **bump**: send changelog to stdout when `dry-run` is paired with `changelog-to-stdout` + +## v2.29.1 (2022-07-26) + +### Fix + +- **Check**: process empty commit message +- **ConventionalCommitsCz**: cz's schema validates the whole commit message now + +### Refactor + +- **Check**: remove the extra preprocessing of commit message file + +## v2.29.0 (2022-07-22) + +### Feat + +- use chardet to get correct encoding +- **bump**: add signed tag support for bump command + +### Fix + +- avoid that pytest overrides existing gpg config +- **test**: set git to work with gpg + +## v2.28.1 (2022-07-22) + +### Fix + +- **changelog**: skip non-compliant commit subjects when building changelog + +## v2.28.0 (2022-07-03) + +### Feat + +- **bump**: make increment option case insensitive + +## v2.27.1 (2022-05-22) + +### Fix + +- **pre-commit**: Use new --allow-abort option +- **pre-commit**: Confine hook to commit-msg stage +- **pre-commit**: Set min pre-commit to v1.4.3 +- **pre-commit**: Don't require serial execution + +## v2.27.0 (2022-05-16) + +### Feat + +- **bump**: let it respect pre-commit reformats when bumping + +## v2.26.0 (2022-05-14) + +### Feat + +- **check**: Add --allow-abort option + +## v2.25.0 (2022-05-10) + +### Feat + +- **changelog**: Improve whitespace in changelog + +### Refactor + +- **changelog**: Simplify incremental_build + +## v2.24.0 (2022-04-15) + +### Feat + +- add --no-raise to avoid raising error codes + +### Fix + +- change error code for NoneIncrementExit + +## v2.23.0 (2022-03-29) + +### Feat + +- **customize.py**: adding support for commit_parser, changelog_pattern, change_type_map + +## v2.22.0 (2022-03-29) + +### Feat + +- **changelog**: add support for single version and version range + +### Refactor + +- speed up testing and wait for tags +- **git**: use date as a function in GitTag to easily patch + +## v2.21.2 (2022-02-22) + +### Fix + +- remove type ignore + +## v2.21.1 (2022-02-22) + +### Refactor + +- Switch to issue forms +- Switch to issue forms +- Switch to issue forms + +## v2.21.0 (2022-02-17) + +### Feat + +- skip merge messages that start with Pull request + +## v2.20.5 (2022-02-07) + +### Fix + +- Ignore packages that are not plugins + +### Refactor + +- iter_modules only accepts str + +## v2.20.4 (2022-01-17) + +### Fix + +- **bump**: raise non zero error code when there's no eligible commit to bump + +## v2.20.3 (2021-12-20) + +### Fix + +- **check**: filter out comment message when checking + +## v2.20.2 (2021-12-14) + +### Fix + +- **poetry**: add typing-exteions to dev + +## v2.20.1 (2021-12-14) + +### Fix + +- import TypedDict from type_extensions for backward compatibility + +### Refactor + +- **conventional_commits**: remove duplicate patterns and import from defaults +- **config**: add CzSettings and Questions TypedDict +- **defaults**: add Settings typeddict +- **defaults**: move bump_map, bump_pattern, commit_parser from defaults to ConventionalCommitsCz + +## v2.20.0 (2021-10-06) + +### Feat + +- **cli.py**: add shortcut for signoff command +- add signoff parameter to commit command + +## v2.19.0 (2021-09-27) + +### Feat + +- utility for showing system information + +## v2.18.2 (2021-09-27) + +### Fix + +- **cli**: handle argparse different behavior after python 3.9 + +## v2.18.1 (2021-09-12) + +### Fix + +- **commit**: correct the stage checker before committing + +## v2.18.0 (2021-08-13) + +### Feat + +- **prompt**: add keyboard shortcuts with config option + +### Refactor + +- **shortcuts**: move check for shortcut config setting to apply to any list select + +## v2.17.13 (2021-07-14) + +## v2.17.12 (2021-07-06) + +### Fix + +- **git.py**: ensure signed commits in changelog when git config log.showsignature=true + +## v2.17.11 (2021-06-24) + +### Fix + +- correct indentation for json config for better readability + +## v2.17.10 (2021-06-22) + +### Fix + +- add support for jinja2 v3 + +## v2.17.9 (2021-06-11) + +### Fix + +- **changelog**: generating changelog after a pre-release + +## v2.17.8 (2021-05-28) + +### Fix + +- **changelog**: annotated tags not generating proper changelog + +## v2.17.7 (2021-05-26) + +### Fix + +- **bump**: fix error due to bumping version file without eol through regex +- **bump**: fix offset error due to partially match + +## v2.17.6 (2021-05-06) + +### Fix + +- **cz/conventional_commits**: optionally expect '!' right before ':' in schema_pattern + +## v2.17.5 (2021-05-06) + +## v2.17.4 (2021-04-22) + +### Fix + +- version update in a docker-compose.yaml file + +## v2.17.3 (2021-04-19) + +### Fix + +- fix multiple versions bumps when version changes the string size + +## v2.17.2 (2021-04-10) + +### Fix + +- **bump**: replace all occurrences that match regex +- **wip**: add test for current breaking change + +## v2.17.1 (2021-04-08) + +### Fix + +- **commands/init**: fix toml config format error + +## v2.17.0 (2021-04-02) + +### Feat + +- Support versions on random positions + +## v2.16.0 (2021-03-08) + +### Feat + +- **bump**: send incremental changelog to stdout and bump output to stderr + +## v2.15.3 (2021-02-26) + +### Fix + +- add utf-8 encode when write toml file + +## v2.15.2 (2021-02-24) + +### Fix + +- **git**: fix get_commits deliminator + +## v2.15.1 (2021-02-21) + +### Fix + +- **config**: change read mode from `r` to `rb` + +## v2.15.0 (2021-02-21) + +### Feat + +- **changelog**: add support for multiline BREAKING paragraph + +## v2.14.2 (2021-02-06) + +### Fix + +- **git**: handle the empty commit and empty email cases + +## v2.14.1 (2021-02-02) + +### Fix + +- remove yaml warnings when using '.cz.yaml' + +## v2.14.0 (2021-01-20) + +### Feat + +- **#271**: enable creation of annotated tags when bumping + +## v2.13.0 (2021-01-01) + +### Feat + +- **#319**: add optional change_type_order + +### Refactor + +- raise an InvalidConfigurationError +- **#323**: address PR feedback +- move expected COMMITS_TREE to global + +## v2.12.1 (2020-12-30) + +### Fix + +- read commit_msg_file with utf-8 + +## v2.12.0 (2020-12-30) + +### Feat + +- **deps**: Update and relax tomlkit version requirement + +## v2.11.1 (2020-12-16) + +### Fix + +- **commit**: attach user info to backup for permission denied issue + +## v2.11.0 (2020-12-10) + +### Feat + +- add yaml as a config option +- **config**: add support for the new class YAMLConfig at the root of the confi internal package +- **init**: add support for yaml config file at init + +### Fix + +- **YAMLConfig**: add a TypeError exception to handle in _parse_settings method + +## v2.10.0 (2020-12-02) + +### Feat + +- **commitizen/cli**: add the integration with argcomplete + +## v2.9.0 (2020-12-02) + +### Feat + +- **Init**: add the json config support as an option at Init +- **commitizen/config/json_config**: add json support for configuration + +### Fix + +- **json_config**: fix the emtpy_config_content method + +## v2.8.2 (2020-11-21) + +### Fix + +- support `!` in cz check command + +## v2.8.1 (2020-11-21) + +### Fix + +- prevent prerelease from creating a bump when there are no commits + +## v2.8.0 (2020-11-15) + +### Feat + +- allow files-only to set config version and create changelog + +## v2.7.0 (2020-11-14) + +### Feat + +- **bump**: add flag `--local-version` that supports bumping only the local version instead of the public + +## v2.6.0 (2020-11-04) + +### Feat + +- **commands/bump**: add config option to create changelog on bump + +## v2.5.0 (2020-11-04) + +### Feat + +- **commands/changelog**: add config file options for start_rev and incremental + +## v2.4.2 (2020-10-26) + +### Fix + +- **init.py**: mypy error (types) +- **commands/bump**: Add NoneIncrementExit to fix git fatal error when creating existing tag + +### Refactor + +- **commands/bump**: Remove comment and changed ... for pass + +## v2.4.1 (2020-10-04) + +### Fix + +- **cz_customize**: make schema_pattern customiziable through config for cz_customize + +## v2.4.0 (2020-09-18) + +### Feat + +- **cz_check**: cz check can read commit message from pipe + +## v2.3.1 (2020-09-07) + +### Fix + +- conventional commit schema + +## v2.3.0 (2020-09-03) + +### Feat + +- **cli**: rewrite cli instructions to be more succinct about what they require + +### Fix + +- **cli**: add guideline for subject input +- **cli**: wrap the word enter with brackets + +## v2.2.0 (2020-08-31) + +### Feat + +- **cz_check**: cz check can read from a string input + +## v2.1.0 (2020-08-06) + +### Feat + +- **cz_check**: Add rev to all displayed ill-formatted commits +- **cz_check**: Update to show all ill-formatted commits + +### Refactor + +- **cz_check**: Refactor _get_commits to return GitCommit instead of dict + +## v2.0.2 (2020-08-03) + +### Fix + +- **git**: use double quotation mark in get_tags + +## v2.0.1 (2020-08-02) + +### Fix + +- **commands/changelog**: add exception message when failing to find an incremental revision +- **commands/bump**: display message variable properly + +## v2.0.0 (2020-07-26) + +### BREAKING CHANGE + +- setup.cfg, .cz and .cz.cfg are no longer supported +- Use "cz version" instead +- "cz --debug" will no longer work + #47 + +### Feat + +- **init**: enable setting up pre-commit hook through "cz init" + +### Fix + +- add missing `pyyaml` dependency +- **cli**: make command required for commitizen + +### Refactor + +- **config**: drop "files" configure support. Please use "version_files" instead +- **config**: remove ini configuration support +- **cli**: remove "--version" argument + +## v1.25.0 (2020-07-26) + +### Feat + +- **conventional_commits**: use and proper support for conventional commits v1.0.0 + +## v1.24.0 (2020-07-26) + +### Feat + +- add author and author_email to git commit + +## v1.23.4 (2020-07-26) + +### Refactor + +- **changelog**: remove pkg_resources dependency + +## v1.23.3 (2020-07-25) + +### Fix + +- **commands/bump**: use `return_code` in commands used by bump +- **commands/commit**: use return_code to raise commit error, not stderr + +### Refactor + +- **cmd**: add return code to Command + +## v1.23.2 (2020-07-25) + +### Fix + +- **bump**: add changelog file into stage when running `cz bump --changelog` + +## v1.23.1 (2020-07-14) + +### Fix + +- Raise NotAGitProjectError only in git related command + +## v1.23.0 (2020-06-14) + +### Feat + +- **cli**: enable displaying all traceback for CommitizenException when --debug flag is used + +### Refactor + +- **exception**: rename MissingConfigError as MissingCzCustomizeConfigError +- **exception**: Rename CommitFailedError and TagFailedError with Bump prefix +- **commands/init**: add test case and remove unaccessible code +- **exception**: move output message related to exception into exception +- **exception**: implement message handling mechanism for CommitizenException +- **cli**: do not show traceback if the raised exception is CommitizenException +- introduce DryRunExit, ExpectedExit, NoCommandFoundError, InvalidCommandArgumentError +- use custom exception for error handling +- **error_codes**: remove unused NO_COMMIT_MSG error code + +## v1.22.3 (2020-06-10) + +## v1.22.2 (2020-05-29) + +### Fix + +- **changelog**: empty lines at the beginning of the CHANGELOG + +## v1.22.1 (2020-05-23) + +### Fix + +- **templates**: remove trailing space in keep_a_changelog + +## v1.22.0 (2020-05-13) + +### Feat + +- **changelog**: add support for `changelog_hook` when changelog finishes the generation +- **changelog**: add support for `message_hook` method +- **changelog**: add support for modifying the change_type in the title of the changelog + +### Fix + +- **changelog**: rename `message_hook` -> `changelog_message_builder_hook` + +## v1.21.0 (2020-05-09) + +### Feat + +- **commands/bump**: add "--check-consistency" optional + +## v1.20.0 (2020-05-06) + +### Feat + +- **bump**: add optional --no-verify argument for bump command + +## v1.19.3 (2020-05-04) + +### Fix + +- **docs**: change old url woile.github.io to commitizen-tools.github.io +- **changelog**: generate today's date when using an unreleased_version + +## v1.19.2 (2020-05-03) + +### Fix + +- **changelog**: sort the commits properly to their version + +## v1.19.1 (2020-05-03) + +### Fix + +- **commands/check**: Show warning if no commit to check when running `cz check --rev-range` + +### Refactor + +- **cli**: add explicit category for deprecation warnings + +## v1.19.0 (2020-05-02) + +### Feat + +- **changelog**: add support for any commit rule system +- **changelog**: add incremental flag +- **commands/changelog**: make changelog_file an option in config +- **commands/changelog**: exit when there is no commit exists +- **commands/changelog**: add --start-rev argument to `cz changelog` +- **changelog**: generate changelog based on git log +- **commands/changelog**: generate changelog_tree from all past commits +- **cz/conventinal_commits**: add changelog_map, changelog_pattern and implement process_commit +- **cz/base**: add default process_commit for processing commit message +- **changelog**: changelog tree generation from markdown + +### Fix + +- **git**: missing dependency removed +- **changelog**: check get_metadata for existing changelog file +- **cz/conventional_commits**: fix schema_pattern break due to rebase +- **changelog_template**: fix list format +- **commitizen/cz**: set changelog_map, changelog_pattern to none as default +- **commands/changelog**: remove --skip-merge argument +- **cli**: add changelog arguments + +### Refactor + +- **changelog**: use functions from changelog.py +- **changelog**: rename category to change_type to fit 'keep a changelog' +- **templates**: rename as "keep_a_changelog_template.j2" +- **templates**: remove unneeded __init__ file +- **cli**: reorder commands +- **templates**: move changelog_template from cz to templates +- **tests/utils**: move create_file_and_commit to tests/utils +- **commands/changelog**: remove redundant if statement +- **commands/changelog**: use jinja2 template instead of string concatenation to build changelog + +## v1.18.3 (2020-04-22) + +### Refactor + +- **commands/init**: fix typo + +## v1.18.2 (2020-04-22) + +### Fix + +- **git**: fix returned value for GitCommit.message when body is empty + +### Refactor + +- **git**: replace GitCommit.message code with one-liner + +## v1.18.1 (2020-04-16) + +### Fix + +- **config**: display ini config deprecation warning only when commitizen config is inside + +## v1.18.0 (2020-04-13) + +### Feat + +- **bump**: support for ! as BREAKING change in commit message + +### Fix + +- **cz/customize**: add error handling when customize detail is not set + +### Refactor + +- **cz/customize**: remove unused mypy ignore +- **mypy**: fix mypy check by checking version.pre exists +- **cz**: add type annotation to registry +- **commands/check**: fix type annotation +- **config/base**: use Dict to replace dict in base_config +- **cz/base**: fix config type used in base cz +- **cz**: add type annotation for each function in cz +- **config**: fix mypy warning for _conf + +## v1.17.1 (2020-03-24) + +### Fix + +- **commands/check**: add help text for check command without argument + +### Refactor + +- **cli**: fix typo + +## v1.17.0 (2020-03-15) + +### Feat + +- **commands/check**: add --rev-range argument for checking commits within some range + +### Fix + +- **bump**: fix bump find_increment error + +### Refactor + +- **cz/connventional_commit**: use \S to check scope +- **git**: remove unnecessary dot between git range +- **tests/bump**: use parameterize to group similliar tests + +## v1.16.4 (2020-03-03) + +### Fix + +- **commands/init**: fix clean up file when initialize commitizen config + +### Refactor + +- **defaults**: split config files into long term support and deprecated ones + +## v1.16.3 (2020-02-20) + +### Fix + +- replace README.rst with docs/README.md in config files + +### Refactor + +- **docs**: remove README.rst and use docs/README.md + +## v1.16.2 (2020-02-01) + +### Fix + +- **commands/check**: add bump into valid commit message of convention commit pattern + +## v1.16.1 (2020-02-01) + +### Fix + +- **pre-commit**: set pre-commit check stage to commit-msg + +## v1.16.0 (2020-01-21) + +### Feat + +- **git**: get_commits default from first_commit + +### Refactor + +- **commands/bump**: rename parameter into bump_setting to distinguish bump_setting and argument +- **git**: rename get tag function to distinguish return str and GitTag +- **cmd**: reimplement how cmd is run +- **git**: Use GitCommit, GitTag object to store commit and git information +- **git**: make arguments other then start and end in get_commit keyword arguments +- **git**: Change get_commits into returning commits instead of lines of messages + +## v1.15.1 (2020-01-20) + +### Fix + +- **cli**: fix --version not functional + +### Refactor + +- **tests/commands/bump**: use tmp_dir to replace self implemented tmp dir behavior +- **test_bump_command**: rename camel case variables +- **tests/commands/check**: use pytest fixture tmpdir replace self implemented contextmanager +- **test/commands/other**: replace unit test style mock with mocker fixture +- **tests/commands**: separate command unit tests into modules +- **tests/commands**: make commands related tests a module + +## v1.15.0 (2020-01-20) + +### Feat + +- **config**: look up configuration in git project root +- **git**: add find_git_project_root + +### Fix + +- **git**: remove breakline in the return value of find_git_project_root + +### Refactor + +- **git**: make find_git_project_root return None if it's not a git project +- **config/base_config**: make set_key not implemented +- **error_codes**: move all the error_codes to a module +- **config**: replace string type path with pathlib.Path + +## v1.14.2 (2020-01-14) + +### Fix + +- **github_workflow/pythonpublish**: use peaceiris/actions-gh-pages@v2 to publish docs + +## v1.14.1 (2020-01-11) + +### Fix + +- **cli**: fix the way default handled for name argument +- **cli**: fix name cannot be overwritten through config in newly refactored config design + +## v1.14.0 (2020-01-06) + +### Feat + +- **pre-commit-hooks**: add pre-commit hook + +### Refactor + +- **pre-commit-hooks**: add metadata for the check hook + +## v1.13.1 (2019-12-31) + +### Fix + +- **github_workflow/pythonpackage**: set git config for unit testing +- **scripts/test**: ensure the script fails once the first failure happens + +## v1.13.0 (2019-12-30) + +### Feat + +- add project version to command init + +## v1.12.0 (2019-12-30) + +### Feat + +- new init command + +## v1.10.3 (2019-12-29) + +### Refactor + +- **commands/bump**: use "version_files" internally +- **config**: set "files" to alias of "version_files" + +## v1.10.2 (2019-12-27) + +### Fix + +- **config**: handle empty config file +- **config**: fix load global_conf even if it doesn't exist +- **config/ini_config**: replace outdated _parse_ini_settings with _parse_settings + +### Refactor + +- new config system where each config type has its own class +- **config**: add type annotation to config property +- **config**: fix wrongly type annotated functions +- **config/ini_config**: move deprecation warning into class initialization +- **config**: use add_path instead of directly assigning _path +- **all**: replace all the _settings invoke with settings.update +- **cz/customize**: remove unnecessary statement "raise NotImplementedError("Not Implemented yet")" +- **config**: move default settings back to defaults +- **config**: Make config a class and each type of config (e.g., toml, ini) a child class + +## v1.10.1 (2019-12-10) + +### Fix + +- **cli**: overwrite "name" only when it's not given +- **config**: fix typo + +## v1.10.0 (2019-11-28) + +### Feat + +- support for different commitizens in `cz check` +- **bump**: new argument --files-only + +## v1.9.2 (2019-11-23) + +### Fix + +- **commands/check.py**: --commit-msg-file is now a required argument + +## v1.9.1 (2019-11-23) + +### Fix + +- **cz/exceptions**: exception AnswerRequiredException not caught (#89) + +## v1.9.0 (2019-11-22) + +### Feat + +- **Commands/check**: enforce the project to always use conventional commits +- **config**: add deprecation warning for loading config from ini files +- **cz/customize**: add jinja support to enhance template flexibility +- **cz/filters**: add required_validator and multiple_line_breaker +- **Commands/commit**: add ยด--dry-runยด flag to the Commit command +- **cz/cz_customize**: implement info to support info and info_path +- **cz/cz_customize**: enable bump_pattern bump_map customization +- **cz/cz_customize**: implement customizable cz +- new 'git-cz' entrypoint + +### Fix + +- commit dry-run doesn't require staging to be clean +- **scripts**: add back the delete poetry prefix +- correct typo to spell "convention" +- removing folder in windows throwing a PermissionError +- **test_cli**: testing the version command + +### Refactor + +- **config**: remove has_pyproject which is no longer used +- **cz/customize**: make jinja2 a custom requirement. if not installed use string.Template instead +- **cz/utils**: rename filters as utils +- **cli**: add back --version and remove subcommand required constraint + +## v1.8.0 (2019-11-12) + +### Feat + +- **cz**: add a base exception for cz customization +- **commands/commit**: abort commit if there is nothing to commit +- **git**: add is_staging_clean to check if there is any file in git staging + +### Fix + +- **commands/commit**: catch exception raised by customization cz +- **cli**: handle the exception that command is not given +- **cli**: enforce subcommand as required + +### Refactor + +- **cz/conventional_commit**: make NoSubjectException inherit CzException and add error message +- **command/version**: use out.write instead of out.line +- **command**: make version a command instead of an argument + +## v1.7.0 (2019-11-08) + +### Feat + +- **config**: update style instead of overwrite +- **config**: parse style in config +- **commit**: make style configurable for commit command + +### Fix + +- **cz**: fix bug in BaseCommitizen.style +- **cz**: fix merge_style usage error +- **cz**: remove breakpoint + +### Refactor + +- **cz**: change the color of default style + +## v1.6.0 (2019-11-05) + +### Feat + +- **commit**: new retry argument to execute previous commit again + +## v1.5.1 (2019-06-04) + +### Fix + +- #28 allows poetry add on py36 envs + +## v1.5.0 (2019-05-11) + +### Feat + +- **bump**: it is now possible to specify a pattern in the files attr to replace the version + +## v1.4.0 (2019-04-26) + +### Feat + +- added argument yes to bump in order to accept questions + +### Fix + +- **bump**: handle commit and create tag failure + +## v1.3.0 (2019-04-24) + +### Feat + +- **bump**: new commit message template + +## v1.2.1 (2019-04-21) + +### Fix + +- **bump**: prefixes like docs, build, etc no longer generate a PATCH + +## v1.2.0 (2019-04-19) + +### Feat + +- custom cz plugins now support bumping version + +## v1.1.1 (2019-04-18) + +### Fix + +- **bump**: commit message now fits better with semver +- conventional commit 'breaking change' in body instead of title + +### Refactor + +- changed stdout statements +- **schema**: command logic removed from commitizen base +- **info**: command logic removed from commitizen base +- **example**: command logic removed from commitizen base +- **commit**: moved most of the commit logic to the commit command + +## v1.1.0 (2019-04-14) + +### Feat + +- new working bump command +- create version tag +- update given files with new version +- **config**: new set key, used to set version to cfg +- support for pyproject.toml +- first semantic version bump implementation + +### Fix + +- removed all from commit +- fix config file not working + +### Refactor + +- added commands folder, better integration with decli + +## v1.0.0 (2019-03-01) + +### Refactor + +- removed delegator, added decli and many tests + +## 1.0.0b2 (2019-01-18) + +## v1.0.0b1 (2019-01-17) + +### Feat + +- py3 only, tests and conventional commits 1.0 + +## v0.9.11 (2018-12-17) + +### Fix + +- **config**: load config reads in order without failing if there is no commitizen section + +## v0.9.10 (2018-09-22) + +### Fix + +- parse scope (this is my punishment for not having tests) + +## v0.9.9 (2018-09-22) + +### Fix + +- parse scope empty + +## v0.9.8 (2018-09-22) + +### Fix + +- **scope**: parse correctly again + +## v0.9.7 (2018-09-22) + +### Fix + +- **scope**: parse correctly + +## v0.9.6 (2018-09-19) + +### Fix + +- **manifest**: included missing files + +### Refactor + +- **conventionalCommit**: moved filters to questions instead of message + +## v0.9.5 (2018-08-24) + +### Fix + +- **config**: home path for python versions between 3.0 and 3.5 + +## v0.9.4 (2018-08-02) + +### Feat + +- **cli**: added version + +## v0.9.3 (2018-07-28) + +### Feat + +- **committer**: conventional commit is a bit more intelligent now + +## v0.9.2 (2017-11-11) + +## v0.9.1 (2017-11-11) + +### Fix + +- **setup.py**: future is now required for every python version + +## v0.9.0 (2017-11-08) + +### Refactor + +- python 2 support + +## v0.8.6 (2017-11-08) + +## v0.8.5 (2017-11-08) + +## v0.8.4 (2017-11-08) + +## v0.8.3 (2017-11-08) + +## v0.8.2 (2017-10-08) + +## v0.8.1 (2017-10-08) + +## v0.8.0 (2017-10-08) + +### Feat + +- **cz**: jira smart commits + +## v0.7.0 (2017-10-08) + +### Refactor + +- **cli**: renamed all to ls command +- **cz**: renamed angular cz to conventional changelog cz + +## v0.6.0 (2017-10-08) + +### Feat + +- info command for angular + +## v0.5.0 (2017-10-07) + +## v0.4.0 (2017-10-07) + +## v0.3.0 (2017-10-07) + +## v0.2.0 (2017-10-07) + +### Feat + +- **config**: new loads from ~/.cz and working project .cz .cz.cfg and setup.cfg +- package discovery + +### Refactor + +- **cz_angular**: improved messages diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..05cf267 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Santiago + +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/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..4c07f07 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include .bumpversion.cfg +include LICENSE +include commitizen/cz/*.txt +global-exclude *.py[cod] __pycache__ *.so *.dylib diff --git a/commitizen/__init__.py b/commitizen/__init__.py new file mode 100644 index 0000000..f16def4 --- /dev/null +++ b/commitizen/__init__.py @@ -0,0 +1,29 @@ +import logging +import logging.config + +from colorama import init # type: ignore + +from commitizen.cz.base import BaseCommitizen + +init() + + +LOGGING = { + "version": 1, + "disable_existing_loggers": True, + "formatters": {"standard": {"format": "%(message)s"}}, + "handlers": { + "default": { + "level": "DEBUG", + "formatter": "standard", + "class": "logging.StreamHandler", + } + }, + "loggers": { + "commitizen": {"handlers": ["default"], "level": "INFO", "propagate": True} + }, +} + +logging.config.dictConfig(LOGGING) + +__all__ = ["BaseCommitizen"] diff --git a/commitizen/__main__.py b/commitizen/__main__.py new file mode 100644 index 0000000..7e0a360 --- /dev/null +++ b/commitizen/__main__.py @@ -0,0 +1,4 @@ +from commitizen.cli import main + +if __name__ == "__main__": + main() diff --git a/commitizen/__version__.py b/commitizen/__version__.py new file mode 100644 index 0000000..db01fb2 --- /dev/null +++ b/commitizen/__version__.py @@ -0,0 +1 @@ +__version__ = "4.6.0" diff --git a/commitizen/bump.py b/commitizen/bump.py new file mode 100644 index 0000000..adfab64 --- /dev/null +++ b/commitizen/bump.py @@ -0,0 +1,153 @@ +from __future__ import annotations + +import os +import re +from collections import OrderedDict +from glob import iglob +from logging import getLogger +from string import Template +from typing import cast + +from commitizen.defaults import MAJOR, MINOR, PATCH, bump_message, encoding +from commitizen.exceptions import CurrentVersionNotFoundError +from commitizen.git import GitCommit, smart_open +from commitizen.version_schemes import Increment, Version + +VERSION_TYPES = [None, PATCH, MINOR, MAJOR] + +logger = getLogger("commitizen") + + +def find_increment( + commits: list[GitCommit], regex: str, increments_map: dict | OrderedDict +) -> Increment | None: + if isinstance(increments_map, dict): + increments_map = OrderedDict(increments_map) + + # Most important cases are major and minor. + # Everything else will be considered patch. + select_pattern = re.compile(regex) + increment: str | None = None + + for commit in commits: + for message in commit.message.split("\n"): + result = select_pattern.search(message) + + if result: + found_keyword = result.group(1) + new_increment = None + for match_pattern in increments_map.keys(): + if re.match(match_pattern, found_keyword): + new_increment = increments_map[match_pattern] + break + + if new_increment is None: + logger.debug( + f"no increment needed for '{found_keyword}' in '{message}'" + ) + + if VERSION_TYPES.index(increment) < VERSION_TYPES.index(new_increment): + logger.debug( + f"increment detected is '{new_increment}' due to '{found_keyword}' in '{message}'" + ) + increment = new_increment + + if increment == MAJOR: + break + + return cast(Increment, increment) + + +def update_version_in_files( + current_version: str, + new_version: str, + files: list[str], + *, + check_consistency: bool = False, + encoding: str = encoding, +) -> list[str]: + """Change old version to the new one in every file given. + + Note that this version is not the tag formatted one. + So for example, your tag could look like `v1.0.0` while your version in + the package like `1.0.0`. + + Returns the list of updated files. + """ + # TODO: separate check step and write step + updated = [] + for path, regex in files_and_regexs(files, current_version): + current_version_found, version_file = _bump_with_regex( + path, + current_version, + new_version, + regex, + encoding=encoding, + ) + + if check_consistency and not current_version_found: + raise CurrentVersionNotFoundError( + f"Current version {current_version} is not found in {path}.\n" + "The version defined in commitizen configuration and the ones in " + "version_files are possibly inconsistent." + ) + + # Write the file out again + with smart_open(path, "w", encoding=encoding) as file: + file.write(version_file) + updated.append(path) + return updated + + +def files_and_regexs(patterns: list[str], version: str) -> list[tuple[str, str]]: + """ + Resolve all distinct files with their regexp from a list of glob patterns with optional regexp + """ + out = [] + for pattern in patterns: + drive, tail = os.path.splitdrive(pattern) + path, _, regex = tail.partition(":") + filepath = drive + path + if not regex: + regex = _version_to_regex(version) + + for path in iglob(filepath): + out.append((path, regex)) + return sorted(list(set(out))) + + +def _bump_with_regex( + version_filepath: str, + current_version: str, + new_version: str, + regex: str, + encoding: str = encoding, +) -> tuple[bool, str]: + current_version_found = False + lines = [] + pattern = re.compile(regex) + with open(version_filepath, encoding=encoding) as f: + for line in f: + if pattern.search(line): + bumped_line = line.replace(current_version, new_version) + if bumped_line != line: + current_version_found = True + lines.append(bumped_line) + else: + lines.append(line) + return current_version_found, "".join(lines) + + +def _version_to_regex(version: str) -> str: + return version.replace(".", r"\.").replace("+", r"\+") + + +def create_commit_message( + current_version: Version | str, + new_version: Version | str, + message_template: str | None = None, +) -> str: + if message_template is None: + message_template = bump_message + t = Template(message_template) + return t.safe_substitute(current_version=current_version, new_version=new_version) diff --git a/commitizen/changelog.py b/commitizen/changelog.py new file mode 100644 index 0000000..704efe6 --- /dev/null +++ b/commitizen/changelog.py @@ -0,0 +1,353 @@ +"""Design + +## Metadata CHANGELOG.md + +1. Identify irrelevant information (possible: changelog title, first paragraph) +2. Identify Unreleased area +3. Identify latest version (to be able to write on top of it) + +## Parse git log + +1. get commits between versions +2. filter commits with the current cz rules +3. parse commit information +4. yield tree nodes +5. format tree nodes +6. produce full tree +7. generate changelog + +Extra: +- [x] Generate full or partial changelog +- [x] Include in tree from file all the extra comments added manually +- [x] Add unreleased value +- [x] hook after message is parsed (add extra information like hyperlinks) +- [x] hook after changelog is generated (api calls) +- [x] add support for change_type maps +""" + +from __future__ import annotations + +import re +from collections import OrderedDict, defaultdict +from collections.abc import Iterable +from dataclasses import dataclass +from datetime import date +from typing import TYPE_CHECKING + +from jinja2 import ( + BaseLoader, + ChoiceLoader, + Environment, + FileSystemLoader, + Template, +) + +from commitizen.cz.base import ChangelogReleaseHook +from commitizen.exceptions import InvalidConfigurationError, NoCommitsFoundError +from commitizen.git import GitCommit, GitTag +from commitizen.tags import TagRules + +if TYPE_CHECKING: + from commitizen.cz.base import MessageBuilderHook + + +@dataclass +class Metadata: + """ + Metadata extracted from the changelog produced by a plugin + """ + + unreleased_start: int | None = None + unreleased_end: int | None = None + latest_version: str | None = None + latest_version_position: int | None = None + latest_version_tag: str | None = None + + def __post_init__(self): + if self.latest_version and not self.latest_version_tag: + # Test syntactic sugar + # latest version tag is optional if same as latest version + self.latest_version_tag = self.latest_version + + +def get_commit_tag(commit: GitCommit, tags: list[GitTag]) -> GitTag | None: + return next((tag for tag in tags if tag.rev == commit.rev), None) + + +def generate_tree_from_commits( + commits: list[GitCommit], + tags: list[GitTag], + commit_parser: str, + changelog_pattern: str, + unreleased_version: str | None = None, + change_type_map: dict[str, str] | None = None, + changelog_message_builder_hook: MessageBuilderHook | None = None, + changelog_release_hook: ChangelogReleaseHook | None = None, + rules: TagRules | None = None, +) -> Iterable[dict]: + pat = re.compile(changelog_pattern) + map_pat = re.compile(commit_parser, re.MULTILINE) + body_map_pat = re.compile(commit_parser, re.MULTILINE | re.DOTALL) + current_tag: GitTag | None = None + rules = rules or TagRules() + + # Check if the latest commit is not tagged + if commits: + latest_commit = commits[0] + current_tag = get_commit_tag(latest_commit, tags) + + current_tag_name: str = unreleased_version or "Unreleased" + current_tag_date: str = "" + if unreleased_version is not None: + current_tag_date = date.today().isoformat() + if current_tag is not None and current_tag.name: + current_tag_name = current_tag.name + current_tag_date = current_tag.date + + changes: dict = defaultdict(list) + used_tags: list = [current_tag] + for commit in commits: + commit_tag = get_commit_tag(commit, tags) + + if ( + commit_tag + and commit_tag not in used_tags + and rules.include_in_changelog(commit_tag) + ): + used_tags.append(commit_tag) + release = { + "version": current_tag_name, + "date": current_tag_date, + "changes": changes, + } + if changelog_release_hook: + release = changelog_release_hook(release, commit_tag) + yield release + current_tag_name = commit_tag.name + current_tag_date = commit_tag.date + changes = defaultdict(list) + + matches = pat.match(commit.message) + if not matches: + continue + + # Process subject from commit message + if message := map_pat.match(commit.message): + process_commit_message( + changelog_message_builder_hook, + message, + commit, + changes, + change_type_map, + ) + + # Process body from commit message + body_parts = commit.body.split("\n\n") + for body_part in body_parts: + if message := body_map_pat.match(body_part): + process_commit_message( + changelog_message_builder_hook, + message, + commit, + changes, + change_type_map, + ) + + release = { + "version": current_tag_name, + "date": current_tag_date, + "changes": changes, + } + if changelog_release_hook: + release = changelog_release_hook(release, commit_tag) + yield release + + +def process_commit_message( + hook: MessageBuilderHook | None, + parsed: re.Match[str], + commit: GitCommit, + changes: dict[str | None, list], + change_type_map: dict[str, str] | None = None, +): + message: dict = { + "sha1": commit.rev, + "parents": commit.parents, + "author": commit.author, + "author_email": commit.author_email, + **parsed.groupdict(), + } + + if processed := hook(message, commit) if hook else message: + messages = [processed] if isinstance(processed, dict) else processed + for msg in messages: + change_type = msg.pop("change_type", None) + if change_type_map: + change_type = change_type_map.get(change_type, change_type) + changes[change_type].append(msg) + + +def order_changelog_tree(tree: Iterable, change_type_order: list[str]) -> Iterable: + if len(set(change_type_order)) != len(change_type_order): + raise InvalidConfigurationError( + f"Change types contain duplicates types ({change_type_order})" + ) + + sorted_tree = [] + for entry in tree: + ordered_change_types = change_type_order + sorted( + set(entry["changes"].keys()) - set(change_type_order) + ) + changes = [ + (ct, entry["changes"][ct]) + for ct in ordered_change_types + if ct in entry["changes"] + ] + sorted_tree.append({**entry, **{"changes": OrderedDict(changes)}}) + return sorted_tree + + +def get_changelog_template(loader: BaseLoader, template: str) -> Template: + loader = ChoiceLoader( + [ + FileSystemLoader("."), + loader, + ] + ) + env = Environment(loader=loader, trim_blocks=True) + return env.get_template(template) + + +def render_changelog( + tree: Iterable, + loader: BaseLoader, + template: str, + **kwargs, +) -> str: + jinja_template = get_changelog_template(loader, template) + changelog: str = jinja_template.render(tree=tree, **kwargs) + return changelog + + +def incremental_build( + new_content: str, lines: list[str], metadata: Metadata +) -> list[str]: + """Takes the original lines and updates with new_content. + + The metadata governs how to remove the old unreleased section and where to place the + new content. + + Args: + lines: The lines from the changelog + new_content: This should be placed somewhere in the lines + metadata: Information about the changelog + + Returns: + Updated lines + """ + unreleased_start = metadata.unreleased_start + unreleased_end = metadata.unreleased_end + latest_version_position = metadata.latest_version_position + skip = False + output_lines: list[str] = [] + for index, line in enumerate(lines): + if index == unreleased_start: + skip = True + elif index == unreleased_end: + skip = False + if ( + latest_version_position is None + or isinstance(latest_version_position, int) + and isinstance(unreleased_end, int) + and latest_version_position > unreleased_end + ): + continue + + if skip: + continue + + if index == latest_version_position: + output_lines.extend([new_content, "\n"]) + + output_lines.append(line) + if not isinstance(latest_version_position, int): + if output_lines and output_lines[-1].strip(): + # Ensure at least one blank line between existing and new content. + output_lines.append("\n") + output_lines.append(new_content) + return output_lines + + +def get_smart_tag_range( + tags: list[GitTag], newest: str, oldest: str | None = None +) -> list[GitTag]: + """Smart because it finds the N+1 tag. + + This is because we need to find until the next tag + """ + accumulator = [] + keep = False + if not oldest: + oldest = newest + for index, tag in enumerate(tags): + if tag.name == newest: + keep = True + if keep: + accumulator.append(tag) + if tag.name == oldest: + keep = False + try: + accumulator.append(tags[index + 1]) + except IndexError: + pass + break + return accumulator + + +def get_oldest_and_newest_rev( + tags: list[GitTag], + version: str, + rules: TagRules, +) -> tuple[str | None, str | None]: + """Find the tags for the given version. + + `version` may come in different formats: + - `0.1.0..0.4.0`: as a range + - `0.3.0`: as a single version + """ + oldest: str | None = None + newest: str | None = None + try: + oldest, newest = version.split("..") + except ValueError: + newest = version + if not (newest_tag := rules.find_tag_for(tags, newest)): + raise NoCommitsFoundError("Could not find a valid revision range.") + + oldest_tag = None + oldest_tag_name = None + if oldest: + if not (oldest_tag := rules.find_tag_for(tags, oldest)): + raise NoCommitsFoundError("Could not find a valid revision range.") + oldest_tag_name = oldest_tag.name + + tags_range = get_smart_tag_range( + tags, newest=newest_tag.name, oldest=oldest_tag_name + ) + if not tags_range: + raise NoCommitsFoundError("Could not find a valid revision range.") + + oldest_rev: str | None = tags_range[-1].name + newest_rev = newest_tag.name + + # check if it's the first tag created + # and it's also being requested as part of the range + if oldest_rev == tags[-1].name and oldest_rev == oldest_tag_name: + return None, newest_rev + + # when they are the same, and it's also the + # first tag created + if oldest_rev == newest_rev: + return None, newest_rev + + return oldest_rev, newest_rev diff --git a/commitizen/changelog_formats/__init__.py b/commitizen/changelog_formats/__init__.py new file mode 100644 index 0000000..782bfb2 --- /dev/null +++ b/commitizen/changelog_formats/__init__.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import sys +from typing import ClassVar, Protocol + +if sys.version_info >= (3, 10): + from importlib import metadata +else: + import importlib_metadata as metadata + +from commitizen.changelog import Metadata +from commitizen.config.base_config import BaseConfig +from commitizen.exceptions import ChangelogFormatUnknown + +CHANGELOG_FORMAT_ENTRYPOINT = "commitizen.changelog_format" +TEMPLATE_EXTENSION = "j2" + + +class ChangelogFormat(Protocol): + extension: ClassVar[str] + """Standard known extension associated with this format""" + + alternative_extensions: ClassVar[set[str]] + """Known alternatives extensions for this format""" + + config: BaseConfig + + def __init__(self, config: BaseConfig): + self.config = config + + @property + def ext(self) -> str: + """Dotted version of extensions, as in `pathlib` and `os` modules""" + return f".{self.extension}" + + @property + def template(self) -> str: + """Expected template name for this format""" + return f"CHANGELOG.{self.extension}.{TEMPLATE_EXTENSION}" + + @property + def default_changelog_file(self) -> str: + return f"CHANGELOG.{self.extension}" + + def get_metadata(self, filepath: str) -> Metadata: + """ + Extract the changelog metadata. + """ + raise NotImplementedError + + +KNOWN_CHANGELOG_FORMATS: dict[str, type[ChangelogFormat]] = { + ep.name: ep.load() + for ep in metadata.entry_points(group=CHANGELOG_FORMAT_ENTRYPOINT) +} + + +def get_changelog_format( + config: BaseConfig, filename: str | None = None +) -> ChangelogFormat: + """ + Get a format from its name + + :raises FormatUnknown: if a non-empty name is provided but cannot be found in the known formats + """ + name: str | None = config.settings.get("changelog_format") + format: type[ChangelogFormat] | None = guess_changelog_format(filename) + + if name and name in KNOWN_CHANGELOG_FORMATS: + format = KNOWN_CHANGELOG_FORMATS[name] + + if not format: + raise ChangelogFormatUnknown(f"Unknown changelog format '{name}'") + + return format(config) + + +def guess_changelog_format(filename: str | None) -> type[ChangelogFormat] | None: + """ + Try guessing the file format from the filename. + + Algorithm is basic, extension-based, and won't work + for extension-less file names like `CHANGELOG` or `NEWS`. + """ + if not filename or not isinstance(filename, str): + return None + for format in KNOWN_CHANGELOG_FORMATS.values(): + if filename.endswith(f".{format.extension}"): + return format + for alt_extension in format.alternative_extensions: + if filename.endswith(f".{alt_extension}"): + return format + return None diff --git a/commitizen/changelog_formats/asciidoc.py b/commitizen/changelog_formats/asciidoc.py new file mode 100644 index 0000000..ed3e860 --- /dev/null +++ b/commitizen/changelog_formats/asciidoc.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from .base import BaseFormat + +if TYPE_CHECKING: + from commitizen.tags import VersionTag + + +class AsciiDoc(BaseFormat): + extension = "adoc" + + RE_TITLE = re.compile(r"^(?P=+) (?P.*)$") + + def parse_version_from_title(self, line: str) -> VersionTag | None: + m = self.RE_TITLE.match(line) + if not m: + return None + # Capture last match as AsciiDoc use postfixed URL labels + return self.tag_rules.search_version(m.group("title"), last=True) + + def parse_title_level(self, line: str) -> int | None: + m = self.RE_TITLE.match(line) + if not m: + return None + return len(m.group("level")) diff --git a/commitizen/changelog_formats/base.py b/commitizen/changelog_formats/base.py new file mode 100644 index 0000000..f69cf8f --- /dev/null +++ b/commitizen/changelog_formats/base.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import os +from abc import ABCMeta +from typing import IO, Any, ClassVar + +from commitizen.changelog import Metadata +from commitizen.config.base_config import BaseConfig +from commitizen.tags import TagRules, VersionTag +from commitizen.version_schemes import get_version_scheme + +from . import ChangelogFormat + + +class BaseFormat(ChangelogFormat, metaclass=ABCMeta): + """ + Base class to extend to implement a changelog file format. + """ + + extension: ClassVar[str] = "" + alternative_extensions: ClassVar[set[str]] = set() + + def __init__(self, config: BaseConfig): + # Constructor needs to be redefined because `Protocol` prevent instantiation by default + # See: https://bugs.python.org/issue44807 + self.config = config + self.encoding = self.config.settings["encoding"] + self.tag_format = self.config.settings["tag_format"] + self.tag_rules = TagRules( + scheme=get_version_scheme(self.config.settings), + tag_format=self.tag_format, + legacy_tag_formats=self.config.settings["legacy_tag_formats"], + ignored_tag_formats=self.config.settings["ignored_tag_formats"], + ) + + def get_metadata(self, filepath: str) -> Metadata: + if not os.path.isfile(filepath): + return Metadata() + + with open(filepath, encoding=self.encoding) as changelog_file: + return self.get_metadata_from_file(changelog_file) + + def get_metadata_from_file(self, file: IO[Any]) -> Metadata: + meta = Metadata() + unreleased_level: int | None = None + for index, line in enumerate(file): + line = line.strip().lower() + + unreleased: int | None = None + if "unreleased" in line: + unreleased = self.parse_title_level(line) + # Try to find beginning and end lines of the unreleased block + if unreleased: + meta.unreleased_start = index + unreleased_level = unreleased + continue + elif unreleased_level and self.parse_title_level(line) == unreleased_level: + meta.unreleased_end = index + + # Try to find the latest release done + parsed = self.parse_version_from_title(line) + if parsed: + meta.latest_version = parsed.version + meta.latest_version_tag = parsed.tag + meta.latest_version_position = index + break # there's no need for more info + if meta.unreleased_start is not None and meta.unreleased_end is None: + meta.unreleased_end = index + + return meta + + def parse_version_from_title(self, line: str) -> VersionTag | None: + """ + Extract the version from a title line if any + """ + raise NotImplementedError( + "Default `get_metadata_from_file` requires `parse_version_from_changelog` to be implemented" + ) + + def parse_title_level(self, line: str) -> int | None: + """ + Get the title level/type of a line if any + """ + raise NotImplementedError( + "Default `get_metadata_from_file` requires `parse_title_type_of_line` to be implemented" + ) diff --git a/commitizen/changelog_formats/markdown.py b/commitizen/changelog_formats/markdown.py new file mode 100644 index 0000000..e3d30fe --- /dev/null +++ b/commitizen/changelog_formats/markdown.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from .base import BaseFormat + +if TYPE_CHECKING: + from commitizen.tags import VersionTag + + +class Markdown(BaseFormat): + extension = "md" + + alternative_extensions = {"markdown", "mkd"} + + RE_TITLE = re.compile(r"^(?P<level>#+) (?P<title>.*)$") + + def parse_version_from_title(self, line: str) -> VersionTag | None: + m = self.RE_TITLE.match(line) + if not m: + return None + return self.tag_rules.search_version(m.group("title")) + + def parse_title_level(self, line: str) -> int | None: + m = self.RE_TITLE.match(line) + if not m: + return None + return len(m.group("level")) diff --git a/commitizen/changelog_formats/restructuredtext.py b/commitizen/changelog_formats/restructuredtext.py new file mode 100644 index 0000000..b7e4e10 --- /dev/null +++ b/commitizen/changelog_formats/restructuredtext.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import sys +from itertools import zip_longest +from typing import IO, TYPE_CHECKING, Any, Union + +from commitizen.changelog import Metadata + +from .base import BaseFormat + +if TYPE_CHECKING: + # TypeAlias is Python 3.10+ but backported in typing-extensions + if sys.version_info >= (3, 10): + from typing import TypeAlias + else: + from typing_extensions import TypeAlias + + +# Can't use `|` operator and native type because of https://bugs.python.org/issue42233 only fixed in 3.10 +TitleKind: TypeAlias = Union[str, tuple[str, str]] + + +class RestructuredText(BaseFormat): + extension = "rst" + + def get_metadata_from_file(self, file: IO[Any]) -> Metadata: + """ + RestructuredText section titles are not one-line-based, + they spread on 2 or 3 lines and levels are not predefined + but determined byt their occurrence order. + + It requires its own algorithm. + + For a more generic approach, you need to rely on `docutils`. + """ + meta = Metadata() + unreleased_title_kind: TitleKind | None = None + in_overlined_title = False + lines = file.readlines() + for index, (first, second, third) in enumerate( + zip_longest(lines, lines[1:], lines[2:], fillvalue="") + ): + first = first.strip().lower() + second = second.strip().lower() + third = third.strip().lower() + title: str | None = None + kind: TitleKind | None = None + if self.is_overlined_title(first, second, third): + title = second + kind = (first[0], third[0]) + in_overlined_title = True + elif not in_overlined_title and self.is_underlined_title(first, second): + title = first + kind = second[0] + else: + in_overlined_title = False + + if title: + if "unreleased" in title: + unreleased_title_kind = kind + meta.unreleased_start = index + continue + elif unreleased_title_kind and unreleased_title_kind == kind: + meta.unreleased_end = index + # Try to find the latest release done + if version := self.tag_rules.search_version(title): + meta.latest_version = version[0] + meta.latest_version_tag = version[1] + meta.latest_version_position = index + break + if meta.unreleased_start is not None and meta.unreleased_end is None: + meta.unreleased_end = ( + meta.latest_version_position if meta.latest_version else index + 1 + ) + + return meta + + def is_overlined_title(self, first: str, second: str, third: str) -> bool: + return ( + len(first) >= len(second) + and len(first) == len(third) + and all(char == first[0] for char in first[1:]) + and first[0] == third[0] + and self.is_underlined_title(second, third) + ) + + def is_underlined_title(self, first: str, second: str) -> bool: + return ( + len(second) >= len(first) + and not second.isalnum() + and all(char == second[0] for char in second[1:]) + ) diff --git a/commitizen/changelog_formats/textile.py b/commitizen/changelog_formats/textile.py new file mode 100644 index 0000000..6693e5e --- /dev/null +++ b/commitizen/changelog_formats/textile.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from .base import BaseFormat + +if TYPE_CHECKING: + from commitizen.tags import VersionTag + + +class Textile(BaseFormat): + extension = "textile" + + RE_TITLE = re.compile(r"^h(?P<level>\d)\. (?P<title>.*)$") + + def parse_version_from_title(self, line: str) -> VersionTag | None: + if not self.RE_TITLE.match(line): + return None + return self.tag_rules.search_version(line) + + def parse_title_level(self, line: str) -> int | None: + m = self.RE_TITLE.match(line) + if not m: + return None + return int(m.group("level")) diff --git a/commitizen/cli.py b/commitizen/cli.py new file mode 100644 index 0000000..72d8243 --- /dev/null +++ b/commitizen/cli.py @@ -0,0 +1,660 @@ +from __future__ import annotations + +import argparse +import logging +import sys +from collections.abc import Sequence +from copy import deepcopy +from functools import partial +from pathlib import Path +from types import TracebackType +from typing import Any + +import argcomplete +from decli import cli + +from commitizen import commands, config, out, version_schemes +from commitizen.exceptions import ( + CommitizenException, + ExitCode, + ExpectedExit, + InvalidCommandArgumentError, + NoCommandFoundError, +) + +logger = logging.getLogger(__name__) + + +class ParseKwargs(argparse.Action): + """ + Parse arguments in the for `key=value`. + + Quoted strings are automatically unquoted. + Can be submitted multiple times: + + ex: + -k key=value -k double-quotes="value" -k single-quotes='value' + + will result in + + namespace["opt"] == { + "key": "value", + "double-quotes": "value", + "single-quotes": "value", + } + """ + + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + kwarg: str | Sequence[Any] | None, + option_string: str | None = None, + ): + if not isinstance(kwarg, str): + return + if "=" not in kwarg: + raise InvalidCommandArgumentError( + f"Option {option_string} expect a key=value format" + ) + kwargs = getattr(namespace, self.dest, None) or {} + key, value = kwarg.split("=", 1) + if not key: + raise InvalidCommandArgumentError( + f"Option {option_string} expect a key=value format" + ) + kwargs[key] = value.strip("'\"") + setattr(namespace, self.dest, kwargs) + + +tpl_arguments = ( + { + "name": ["--template", "-t"], + "help": ( + "changelog template file name (relative to the current working directory)" + ), + }, + { + "name": ["--extra", "-e"], + "action": ParseKwargs, + "dest": "extras", + "metavar": "EXTRA", + "help": "a changelog extra variable (in the form 'key=value')", + }, +) + +data = { + "prog": "cz", + "description": ( + "Commitizen is a cli tool to generate conventional commits.\n" + "For more information about the topic go to " + "https://conventionalcommits.org/" + ), + "formatter_class": argparse.RawDescriptionHelpFormatter, + "arguments": [ + { + "name": "--config", + "help": "the path of configuration file", + }, + {"name": "--debug", "action": "store_true", "help": "use debug mode"}, + { + "name": ["-n", "--name"], + "help": "use the given commitizen (default: cz_conventional_commits)", + }, + { + "name": ["-nr", "--no-raise"], + "type": str, + "required": False, + "help": "comma separated error codes that won't rise error, e.g: cz -nr 1,2,3 bump. See codes at https://commitizen-tools.github.io/commitizen/exit_codes/", + }, + ], + "subcommands": { + "title": "commands", + "required": True, + "commands": [ + { + "name": ["init"], + "description": "init commitizen configuration", + "help": "init commitizen configuration", + "func": commands.Init, + }, + { + "name": ["commit", "c"], + "description": "create new commit", + "help": "create new commit", + "func": commands.Commit, + "arguments": [ + { + "name": ["--retry"], + "action": "store_true", + "help": "retry last commit", + }, + { + "name": ["--no-retry"], + "action": "store_true", + "default": False, + "help": "skip retry if retry_after_failure is set to true", + }, + { + "name": "--dry-run", + "action": "store_true", + "help": "show output to stdout, no commit, no modified files", + }, + { + "name": "--write-message-to-file", + "type": Path, + "metavar": "FILE_PATH", + "help": "write message to file before committing (can be combined with --dry-run)", + }, + { + "name": ["-s", "--signoff"], + "action": "store_true", + "help": "sign off the commit", + }, + { + "name": ["-a", "--all"], + "action": "store_true", + "help": "Tell the command to automatically stage files that have been modified and deleted, but new files you have not told Git about are not affected.", + }, + { + "name": ["-e", "--edit"], + "action": "store_true", + "default": False, + "help": "edit the commit message before committing", + }, + { + "name": ["-l", "--message-length-limit"], + "type": int, + "default": 0, + "help": "length limit of the commit message; 0 for no limit", + }, + { + "name": ["--"], + "action": "store_true", + "dest": "double_dash", + "help": "Positional arguments separator (recommended)", + }, + ], + }, + { + "name": "ls", + "description": "show available commitizens", + "help": "show available commitizens", + "func": commands.ListCz, + }, + { + "name": "example", + "description": "show commit example", + "help": "show commit example", + "func": commands.Example, + }, + { + "name": "info", + "description": "show information about the cz", + "help": "show information about the cz", + "func": commands.Info, + }, + { + "name": "schema", + "description": "show commit schema", + "help": "show commit schema", + "func": commands.Schema, + }, + { + "name": "bump", + "description": "bump semantic version based on the git log", + "help": "bump semantic version based on the git log", + "func": commands.Bump, + "arguments": [ + { + "name": "--dry-run", + "action": "store_true", + "help": "show output to stdout, no commit, no modified files", + }, + { + "name": "--files-only", + "action": "store_true", + "help": "bump version in the files from the config", + }, + { + "name": "--local-version", + "action": "store_true", + "help": "bump only the local version portion", + }, + { + "name": ["--changelog", "-ch"], + "action": "store_true", + "default": False, + "help": "generate the changelog for the newest version", + }, + { + "name": ["--no-verify"], + "action": "store_true", + "default": False, + "help": "this option bypasses the pre-commit and commit-msg hooks", + }, + { + "name": "--yes", + "action": "store_true", + "help": "accept automatically questions done", + }, + { + "name": "--tag-format", + "help": ( + "the format used to tag the commit and read it, " + "use it in existing projects, " + "wrap around simple quotes" + ), + }, + { + "name": "--bump-message", + "help": ( + "template used to create the release commit, " + "useful when working with CI" + ), + }, + { + "name": ["--prerelease", "-pr"], + "help": "choose type of prerelease", + "choices": ["alpha", "beta", "rc"], + }, + { + "name": ["--devrelease", "-d"], + "help": "specify non-negative integer for dev. release", + "type": int, + }, + { + "name": ["--increment"], + "help": "manually specify the desired increment", + "choices": ["MAJOR", "MINOR", "PATCH"], + "type": str.upper, + }, + { + "name": ["--increment-mode"], + "choices": ["linear", "exact"], + "default": "linear", + "help": ( + "set the method by which the new version is chosen. " + "'linear' (default) guesses the next version based on typical linear version progression, " + "such that bumping of a pre-release with lower precedence than the current pre-release " + "phase maintains the current phase of higher precedence. " + "'exact' applies the changes that have been specified (or determined from the commit log) " + "without interpretation, such that the increment and pre-release are always honored" + ), + }, + { + "name": ["--check-consistency", "-cc"], + "help": ( + "check consistency among versions defined in " + "commitizen configuration and version_files" + ), + "action": "store_true", + }, + { + "name": ["--annotated-tag", "-at"], + "help": "create annotated tag instead of lightweight one", + "action": "store_true", + }, + { + "name": ["--annotated-tag-message", "-atm"], + "help": "create annotated tag message", + "type": str, + }, + { + "name": ["--gpg-sign", "-s"], + "help": "sign tag instead of lightweight one", + "default": False, + "action": "store_true", + }, + { + "name": ["--changelog-to-stdout"], + "action": "store_true", + "default": False, + "help": "Output changelog to the stdout", + }, + { + "name": ["--git-output-to-stderr"], + "action": "store_true", + "default": False, + "help": "Redirect git output to stderr", + }, + { + "name": ["--retry"], + "action": "store_true", + "default": False, + "help": "retry commit if it fails the 1st time", + }, + { + "name": ["--major-version-zero"], + "action": "store_true", + "default": None, + "help": "keep major version at zero, even for breaking changes", + }, + *deepcopy(tpl_arguments), + { + "name": "--file-name", + "help": "file name of changelog (default: 'CHANGELOG.md')", + }, + { + "name": ["--prerelease-offset"], + "type": int, + "default": None, + "help": "start pre-releases with this offset", + }, + { + "name": ["--version-scheme"], + "help": "choose version scheme", + "default": None, + "choices": version_schemes.KNOWN_SCHEMES, + }, + { + "name": ["--version-type"], + "help": "Deprecated, use --version-scheme", + "default": None, + "choices": version_schemes.KNOWN_SCHEMES, + }, + { + "name": "manual_version", + "type": str, + "nargs": "?", + "help": "bump to the given version (e.g: 1.5.3)", + "metavar": "MANUAL_VERSION", + }, + { + "name": ["--build-metadata"], + "help": "Add additional build-metadata to the version-number", + "default": None, + }, + { + "name": ["--get-next"], + "action": "store_true", + "help": "Determine the next version and write to stdout", + "default": False, + }, + { + "name": ["--allow-no-commit"], + "default": False, + "help": "bump version without eligible commits", + "action": "store_true", + }, + ], + }, + { + "name": ["changelog", "ch"], + "description": ( + "generate changelog (note that it will overwrite existing file)" + ), + "help": ( + "generate changelog (note that it will overwrite existing file)" + ), + "func": commands.Changelog, + "arguments": [ + { + "name": "--dry-run", + "action": "store_true", + "default": False, + "help": "show changelog to stdout", + }, + { + "name": "--file-name", + "help": "file name of changelog (default: 'CHANGELOG.md')", + }, + { + "name": "--unreleased-version", + "help": ( + "set the value for the new version (use the tag value), " + "instead of using unreleased" + ), + }, + { + "name": "--incremental", + "action": "store_true", + "default": False, + "help": ( + "generates changelog from last created version, " + "useful if the changelog has been manually modified" + ), + }, + { + "name": "rev_range", + "type": str, + "nargs": "?", + "help": "generates changelog for the given version (e.g: 1.5.3) or version range (e.g: 1.5.3..1.7.9)", + }, + { + "name": "--start-rev", + "default": None, + "help": ( + "start rev of the changelog. " + "If not set, it will generate changelog from the start" + ), + }, + { + "name": "--merge-prerelease", + "action": "store_true", + "default": False, + "help": ( + "collect all changes from prereleases into next non-prerelease. " + "If not set, it will include prereleases in the changelog" + ), + }, + { + "name": ["--version-scheme"], + "help": "choose version scheme", + "default": None, + "choices": version_schemes.KNOWN_SCHEMES, + }, + { + "name": "--export-template", + "default": None, + "help": "Export the changelog template into this file instead of rendering it", + }, + *deepcopy(tpl_arguments), + ], + }, + { + "name": ["check"], + "description": "validates that a commit message matches the commitizen schema", + "help": "validates that a commit message matches the commitizen schema", + "func": commands.Check, + "arguments": [ + { + "name": "--commit-msg-file", + "help": ( + "ask for the name of the temporal file that contains " + "the commit message. " + "Using it in a git hook script: MSG_FILE=$1" + ), + "exclusive_group": "group1", + }, + { + "name": "--rev-range", + "help": "a range of git rev to check. e.g, master..HEAD", + "exclusive_group": "group1", + }, + { + "name": ["-m", "--message"], + "help": "commit message that needs to be checked", + "exclusive_group": "group1", + }, + { + "name": ["--allow-abort"], + "action": "store_true", + "default": False, + "help": "allow empty commit messages, which typically abort a commit", + }, + { + "name": ["--allowed-prefixes"], + "nargs": "*", + "help": "allowed commit message prefixes. " + "If the message starts by one of these prefixes, " + "the message won't be checked against the regex", + }, + { + "name": ["-l", "--message-length-limit"], + "type": int, + "default": 0, + "help": "length limit of the commit message; 0 for no limit", + }, + ], + }, + { + "name": ["version"], + "description": ( + "get the version of the installed commitizen or the current project" + " (default: installed commitizen)" + ), + "help": ( + "get the version of the installed commitizen or the current project" + " (default: installed commitizen)" + ), + "func": commands.Version, + "arguments": [ + { + "name": ["-r", "--report"], + "help": "get system information for reporting bugs", + "action": "store_true", + "exclusive_group": "group1", + }, + { + "name": ["-p", "--project"], + "help": "get the version of the current project", + "action": "store_true", + "exclusive_group": "group1", + }, + { + "name": ["-c", "--commitizen"], + "help": "get the version of the installed commitizen", + "action": "store_true", + "exclusive_group": "group1", + }, + { + "name": ["-v", "--verbose"], + "help": ( + "get the version of both the installed commitizen " + "and the current project" + ), + "action": "store_true", + "exclusive_group": "group1", + }, + ], + }, + ], + }, +} + +original_excepthook = sys.excepthook + + +def commitizen_excepthook( + type, value, traceback, debug=False, no_raise: list[int] | None = None +): + traceback = traceback if isinstance(traceback, TracebackType) else None + if not no_raise: + no_raise = [] + if isinstance(value, CommitizenException): + if value.message: + value.output_method(value.message) + if debug: + original_excepthook(type, value, traceback) + exit_code = value.exit_code + if exit_code in no_raise: + exit_code = ExitCode.EXPECTED_EXIT + sys.exit(exit_code) + else: + original_excepthook(type, value, traceback) + + +commitizen_debug_excepthook = partial(commitizen_excepthook, debug=True) + +sys.excepthook = commitizen_excepthook + + +def parse_no_raise(comma_separated_no_raise: str) -> list[int]: + """Convert the given string to exit codes. + + Receives digits and strings and outputs the parsed integer which + represents the exit code found in exceptions. + """ + no_raise_items: list[str] = comma_separated_no_raise.split(",") + no_raise_codes = [] + for item in no_raise_items: + if item.isdecimal(): + no_raise_codes.append(int(item)) + continue + try: + exit_code = ExitCode[item.strip()] + except KeyError: + out.warn(f"WARN: no_raise key `{item}` does not exist. Skipping.") + continue + else: + no_raise_codes.append(exit_code.value) + return no_raise_codes + + +def main(): + parser = cli(data) + argcomplete.autocomplete(parser) + # Show help if no arg provided + if len(sys.argv) == 1: + parser.print_help(sys.stderr) + raise ExpectedExit() + + # This is for the command required constraint in 2.0 + try: + args, unknown_args = parser.parse_known_args() + except (TypeError, SystemExit) as e: + # https://github.com/commitizen-tools/commitizen/issues/429 + # argparse raises TypeError when non exist command is provided on Python < 3.9 + # but raise SystemExit with exit code == 2 on Python 3.9 + if isinstance(e, TypeError) or (isinstance(e, SystemExit) and e.code == 2): + raise NoCommandFoundError() + raise e + + arguments = vars(args) + if unknown_args: + # Raise error for extra-args without -- separation + if "--" not in unknown_args: + raise InvalidCommandArgumentError( + f"Invalid commitizen arguments were found: `{' '.join(unknown_args)}`. " + "Please use -- separator for extra git args" + ) + # Raise error for extra-args before -- + elif unknown_args[0] != "--": + pos = unknown_args.index("--") + raise InvalidCommandArgumentError( + f"Invalid commitizen arguments were found before -- separator: `{' '.join(unknown_args[:pos])}`. " + ) + # Log warning for -- without any extra args + elif len(unknown_args) == 1: + logger.warning( + "\nWARN: Incomplete commit command: received -- separator without any following git arguments\n" + ) + extra_args = " ".join(unknown_args[1:]) + arguments["extra_cli_args"] = extra_args + + if args.config: + conf = config.read_cfg(args.config) + else: + conf = config.read_cfg() + + if args.name: + conf.update({"name": args.name}) + elif not args.name and not conf.path: + conf.update({"name": "cz_conventional_commits"}) + + if args.debug: + logging.getLogger("commitizen").setLevel(logging.DEBUG) + sys.excepthook = commitizen_debug_excepthook + elif args.no_raise: + no_raise_exit_codes = parse_no_raise(args.no_raise) + no_raise_debug_excepthook = partial( + commitizen_excepthook, no_raise=no_raise_exit_codes + ) + sys.excepthook = no_raise_debug_excepthook + + args.func(conf, arguments)() + + +if __name__ == "__main__": + main() diff --git a/commitizen/cmd.py b/commitizen/cmd.py new file mode 100644 index 0000000..ba48ac7 --- /dev/null +++ b/commitizen/cmd.py @@ -0,0 +1,50 @@ +import os +import subprocess +from typing import NamedTuple + +from charset_normalizer import from_bytes + +from commitizen.exceptions import CharacterSetDecodeError + + +class Command(NamedTuple): + out: str + err: str + stdout: bytes + stderr: bytes + return_code: int + + +def _try_decode(bytes_: bytes) -> str: + try: + return bytes_.decode("utf-8") + except UnicodeDecodeError: + charset_match = from_bytes(bytes_).best() + if charset_match is None: + raise CharacterSetDecodeError() + try: + return bytes_.decode(charset_match.encoding) + except UnicodeDecodeError as e: + raise CharacterSetDecodeError() from e + + +def run(cmd: str, env=None) -> Command: + if env is not None: + env = {**os.environ, **env} + process = subprocess.Popen( + cmd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=subprocess.PIPE, + env=env, + ) + stdout, stderr = process.communicate() + return_code = process.returncode + return Command( + _try_decode(stdout), + _try_decode(stderr), + stdout, + stderr, + return_code, + ) diff --git a/commitizen/commands/__init__.py b/commitizen/commands/__init__.py new file mode 100644 index 0000000..806e384 --- /dev/null +++ b/commitizen/commands/__init__.py @@ -0,0 +1,23 @@ +from .bump import Bump +from .changelog import Changelog +from .check import Check +from .commit import Commit +from .example import Example +from .info import Info +from .init import Init +from .list_cz import ListCz +from .schema import Schema +from .version import Version + +__all__ = ( + "Bump", + "Check", + "Commit", + "Changelog", + "Example", + "Info", + "ListCz", + "Schema", + "Version", + "Init", +) diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py new file mode 100644 index 0000000..6085309 --- /dev/null +++ b/commitizen/commands/bump.py @@ -0,0 +1,439 @@ +from __future__ import annotations + +import warnings +from logging import getLogger +from typing import cast + +import questionary + +from commitizen import bump, factory, git, hooks, out +from commitizen.changelog_formats import get_changelog_format +from commitizen.commands.changelog import Changelog +from commitizen.config import BaseConfig +from commitizen.defaults import Settings +from commitizen.exceptions import ( + BumpCommitFailedError, + BumpTagFailedError, + DryRunExit, + ExpectedExit, + GetNextExit, + InvalidManualVersion, + NoCommitsFoundError, + NoneIncrementExit, + NoPatternMapError, + NotAGitProjectError, + NotAllowed, + NoVersionSpecifiedError, +) +from commitizen.providers import get_provider +from commitizen.tags import TagRules +from commitizen.version_schemes import ( + Increment, + InvalidVersion, + Prerelease, + get_version_scheme, +) + +logger = getLogger("commitizen") + + +class Bump: + """Show prompt for the user to create a guided commit.""" + + def __init__(self, config: BaseConfig, arguments: dict): + if not git.is_git_project(): + raise NotAGitProjectError() + + self.config: BaseConfig = config + self.encoding = config.settings["encoding"] + self.arguments: dict = arguments + self.bump_settings: dict = { + **config.settings, + **{ + key: arguments[key] + for key in [ + "tag_format", + "prerelease", + "increment", + "increment_mode", + "bump_message", + "gpg_sign", + "annotated_tag", + "annotated_tag_message", + "major_version_zero", + "prerelease_offset", + "template", + "file_name", + ] + if arguments[key] is not None + }, + } + self.cz = factory.commiter_factory(self.config) + self.changelog_flag = arguments["changelog"] + self.changelog_config = self.config.settings.get("update_changelog_on_bump") + self.changelog_to_stdout = arguments["changelog_to_stdout"] + self.git_output_to_stderr = arguments["git_output_to_stderr"] + self.no_verify = arguments["no_verify"] + self.check_consistency = arguments["check_consistency"] + self.retry = arguments["retry"] + self.pre_bump_hooks = self.config.settings["pre_bump_hooks"] + self.post_bump_hooks = self.config.settings["post_bump_hooks"] + deprecated_version_type = arguments.get("version_type") + if deprecated_version_type: + warnings.warn( + DeprecationWarning( + "`--version-type` parameter is deprecated and will be removed in commitizen 4. " + "Please use `--version-scheme` instead" + ) + ) + self.scheme = get_version_scheme( + self.config.settings, arguments["version_scheme"] or deprecated_version_type + ) + self.file_name = arguments["file_name"] or self.config.settings.get( + "changelog_file" + ) + self.changelog_format = get_changelog_format(self.config, self.file_name) + + self.template = ( + arguments["template"] + or self.config.settings.get("template") + or self.changelog_format.template + ) + self.extras = arguments["extras"] + + def is_initial_tag( + self, current_tag: git.GitTag | None, is_yes: bool = False + ) -> bool: + """Check if reading the whole git tree up to HEAD is needed.""" + is_initial = False + if not current_tag: + if is_yes: + is_initial = True + else: + out.info("No tag matching configuration could not be found.") + out.info( + "Possible causes:\n" + "- version in configuration is not the current version\n" + "- tag_format or legacy_tag_formats is missing, check them using 'git tag --list'\n" + ) + is_initial = questionary.confirm("Is this the first tag created?").ask() + return is_initial + + def find_increment(self, commits: list[git.GitCommit]) -> Increment | None: + # Update the bump map to ensure major version doesn't increment. + is_major_version_zero: bool = self.bump_settings["major_version_zero"] + # self.cz.bump_map = defaults.bump_map_major_version_zero + bump_map = ( + self.cz.bump_map_major_version_zero + if is_major_version_zero + else self.cz.bump_map + ) + bump_pattern = self.cz.bump_pattern + + if not bump_map or not bump_pattern: + raise NoPatternMapError( + f"'{self.config.settings['name']}' rule does not support bump" + ) + increment = bump.find_increment( + commits, regex=bump_pattern, increments_map=bump_map + ) + return increment + + def __call__(self) -> None: # noqa: C901 + """Steps executed to bump.""" + provider = get_provider(self.config) + + try: + current_version = self.scheme(provider.get_version()) + except TypeError: + raise NoVersionSpecifiedError() + + bump_commit_message: str = self.bump_settings["bump_message"] + version_files: list[str] = self.bump_settings["version_files"] + major_version_zero: bool = self.bump_settings["major_version_zero"] + prerelease_offset: int = self.bump_settings["prerelease_offset"] + + dry_run: bool = self.arguments["dry_run"] + is_yes: bool = self.arguments["yes"] + increment: Increment | None = self.arguments["increment"] + prerelease: Prerelease | None = self.arguments["prerelease"] + devrelease: int | None = self.arguments["devrelease"] + is_files_only: bool | None = self.arguments["files_only"] + is_local_version: bool = self.arguments["local_version"] + manual_version = self.arguments["manual_version"] + build_metadata = self.arguments["build_metadata"] + increment_mode: str = self.arguments["increment_mode"] + get_next: bool = self.arguments["get_next"] + allow_no_commit: bool | None = self.arguments["allow_no_commit"] + + if manual_version: + if increment: + raise NotAllowed("--increment cannot be combined with MANUAL_VERSION") + + if prerelease: + raise NotAllowed("--prerelease cannot be combined with MANUAL_VERSION") + + if devrelease is not None: + raise NotAllowed("--devrelease cannot be combined with MANUAL_VERSION") + + if is_local_version: + raise NotAllowed( + "--local-version cannot be combined with MANUAL_VERSION" + ) + + if build_metadata: + raise NotAllowed( + "--build-metadata cannot be combined with MANUAL_VERSION" + ) + + if major_version_zero: + raise NotAllowed( + "--major-version-zero cannot be combined with MANUAL_VERSION" + ) + + if get_next: + raise NotAllowed("--get-next cannot be combined with MANUAL_VERSION") + + if major_version_zero: + if not current_version.release[0] == 0: + raise NotAllowed( + f"--major-version-zero is meaningless for current version {current_version}" + ) + + if build_metadata: + if is_local_version: + raise NotAllowed( + "--local-version cannot be combined with --build-metadata" + ) + + if get_next: + # if trying to use --get-next, we should not allow --changelog or --changelog-to-stdout + if self.changelog_flag or bool(self.changelog_to_stdout): + raise NotAllowed( + "--changelog or --changelog-to-stdout is not allowed with --get-next" + ) + # --get-next is a special case, taking precedence over config for 'update_changelog_on_bump' + self.changelog_config = False + # Setting dry_run to prevent any unwanted changes to the repo or files + self.dry_run = True + else: + # If user specified changelog_to_stdout, they probably want the + # changelog to be generated as well, this is the most intuitive solution + self.changelog_flag = ( + self.changelog_flag + or bool(self.changelog_to_stdout) + or self.changelog_config + ) + + rules = TagRules.from_settings(cast(Settings, self.bump_settings)) + current_tag = rules.find_tag_for(git.get_tags(), current_version) + current_tag_version = getattr( + current_tag, "name", rules.normalize_tag(current_version) + ) + + is_initial = self.is_initial_tag(current_tag, is_yes) + + if manual_version: + try: + new_version = self.scheme(manual_version) + except InvalidVersion as exc: + raise InvalidManualVersion( + "[INVALID_MANUAL_VERSION]\n" + f"Invalid manual version: '{manual_version}'" + ) from exc + else: + if increment is None: + if current_tag: + commits = git.get_commits(current_tag.name) + else: + commits = git.get_commits() + + # No commits, there is no need to create an empty tag. + # Unless we previously had a prerelease. + if ( + not commits + and not current_version.is_prerelease + and not allow_no_commit + ): + raise NoCommitsFoundError( + "[NO_COMMITS_FOUND]\nNo new commits found." + ) + + increment = self.find_increment(commits) + + # It may happen that there are commits, but they are not eligible + # for an increment, this generates a problem when using prerelease (#281) + if prerelease and increment is None and not current_version.is_prerelease: + raise NoCommitsFoundError( + "[NO_COMMITS_FOUND]\n" + "No commits found to generate a pre-release.\n" + "To avoid this error, manually specify the type of increment with `--increment`" + ) + + # we create an empty PATCH increment for empty tag + if increment is None and allow_no_commit: + increment = "PATCH" + + new_version = current_version.bump( + increment, + prerelease=prerelease, + prerelease_offset=prerelease_offset, + devrelease=devrelease, + is_local_version=is_local_version, + build_metadata=build_metadata, + exact_increment=increment_mode == "exact", + ) + + new_tag_version = rules.normalize_tag(new_version) + message = bump.create_commit_message( + current_version, new_version, bump_commit_message + ) + + if get_next: + if increment is None and new_tag_version == current_tag_version: + raise NoneIncrementExit( + "[NO_COMMITS_TO_BUMP]\n" + "The commits found are not eligible to be bumped" + ) + + out.write(str(new_version)) + raise GetNextExit() + + # Report found information + information = f"{message}\ntag to create: {new_tag_version}\n" + if increment: + information += f"increment detected: {increment}\n" + + if self.changelog_to_stdout: + # When the changelog goes to stdout, we want to send + # the bump information to stderr, this way the + # changelog output can be captured + out.diagnostic(information) + else: + out.write(information) + + if increment is None and new_tag_version == current_tag_version: + raise NoneIncrementExit( + "[NO_COMMITS_TO_BUMP]\nThe commits found are not eligible to be bumped" + ) + + files: list[str] = [] + if self.changelog_flag: + args = { + "unreleased_version": new_tag_version, + "template": self.template, + "extras": self.extras, + "incremental": True, + "dry_run": dry_run, + } + if self.changelog_to_stdout: + changelog_cmd = Changelog(self.config, {**args, "dry_run": True}) + try: + changelog_cmd() + except DryRunExit: + pass + + args["file_name"] = self.file_name + changelog_cmd = Changelog(self.config, args) + changelog_cmd() + files.append(changelog_cmd.file_name) + + # Do not perform operations over files or git. + if dry_run: + raise DryRunExit() + + files.extend( + bump.update_version_in_files( + str(current_version), + str(new_version), + version_files, + check_consistency=self.check_consistency, + encoding=self.encoding, + ) + ) + + provider.set_version(str(new_version)) + + if self.pre_bump_hooks: + hooks.run( + self.pre_bump_hooks, + _env_prefix="CZ_PRE_", + is_initial=is_initial, + current_version=str(current_version), + current_tag_version=current_tag_version, + new_version=new_version.public, + new_tag_version=new_tag_version, + message=message, + increment=increment, + changelog_file_name=changelog_cmd.file_name + if self.changelog_flag + else None, + ) + + if is_files_only: + raise ExpectedExit() + + # FIXME: check if any changes have been staged + git.add(*files) + c = git.commit(message, args=self._get_commit_args()) + if self.retry and c.return_code != 0 and self.changelog_flag: + # Maybe pre-commit reformatted some files? Retry once + logger.debug("1st git.commit error: %s", c.err) + logger.info("1st commit attempt failed; retrying once") + git.add(*files) + c = git.commit(message, args=self._get_commit_args()) + if c.return_code != 0: + err = c.err.strip() or c.out + raise BumpCommitFailedError(f'2nd git.commit error: "{err}"') + + if c.out: + if self.git_output_to_stderr: + out.diagnostic(c.out) + else: + out.write(c.out) + if c.err: + if self.git_output_to_stderr: + out.diagnostic(c.err) + else: + out.write(c.err) + + c = git.tag( + new_tag_version, + signed=self.bump_settings.get("gpg_sign", False) + or bool(self.config.settings.get("gpg_sign", False)), + annotated=self.bump_settings.get("annotated_tag", False) + or bool(self.config.settings.get("annotated_tag", False)) + or bool(self.bump_settings.get("annotated_tag_message", False)), + msg=self.bump_settings.get("annotated_tag_message", None), + # TODO: also get from self.config.settings? + ) + if c.return_code != 0: + raise BumpTagFailedError(c.err) + + if self.post_bump_hooks: + hooks.run( + self.post_bump_hooks, + _env_prefix="CZ_POST_", + was_initial=is_initial, + previous_version=str(current_version), + previous_tag_version=current_tag_version, + current_version=new_version.public, + current_tag_version=new_tag_version, + message=message, + increment=increment, + changelog_file_name=changelog_cmd.file_name + if self.changelog_flag + else None, + ) + + # TODO: For v3 output this only as diagnostic and remove this if + if self.changelog_to_stdout: + out.diagnostic("Done!") + else: + out.success("Done!") + + def _get_commit_args(self) -> str: + commit_args = ["-a"] + if self.no_verify: + commit_args.append("--no-verify") + return " ".join(commit_args) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py new file mode 100644 index 0000000..80a7265 --- /dev/null +++ b/commitizen/commands/changelog.py @@ -0,0 +1,232 @@ +from __future__ import annotations + +import os.path +from difflib import SequenceMatcher +from operator import itemgetter +from pathlib import Path +from typing import Callable, cast + +from commitizen import changelog, defaults, factory, git, out +from commitizen.changelog_formats import get_changelog_format +from commitizen.config import BaseConfig +from commitizen.cz.base import ChangelogReleaseHook, MessageBuilderHook +from commitizen.cz.utils import strip_local_version +from commitizen.exceptions import ( + DryRunExit, + NoCommitsFoundError, + NoPatternMapError, + NoRevisionError, + NotAGitProjectError, + NotAllowed, +) +from commitizen.git import GitTag, smart_open +from commitizen.tags import TagRules +from commitizen.version_schemes import get_version_scheme + + +class Changelog: + """Generate a changelog based on the commit history.""" + + def __init__(self, config: BaseConfig, args): + if not git.is_git_project(): + raise NotAGitProjectError() + + self.config: BaseConfig = config + self.encoding = self.config.settings["encoding"] + self.cz = factory.commiter_factory(self.config) + + self.start_rev = args.get("start_rev") or self.config.settings.get( + "changelog_start_rev" + ) + self.file_name = args.get("file_name") or cast( + str, self.config.settings.get("changelog_file") + ) + if not isinstance(self.file_name, str): + raise NotAllowed( + "Changelog file name is broken.\n" + "Check the flag `--file-name` in the terminal " + f"or the setting `changelog_file` in {self.config.path}" + ) + self.changelog_format = get_changelog_format(self.config, self.file_name) + + self.incremental = args["incremental"] or self.config.settings.get( + "changelog_incremental" + ) + self.dry_run = args["dry_run"] + + self.scheme = get_version_scheme( + self.config.settings, args.get("version_scheme") + ) + + current_version = ( + args.get("current_version", config.settings.get("version")) or "" + ) + self.current_version = self.scheme(current_version) if current_version else None + + self.unreleased_version = args["unreleased_version"] + self.change_type_map = ( + self.config.settings.get("change_type_map") or self.cz.change_type_map + ) + self.change_type_order = ( + self.config.settings.get("change_type_order") + or self.cz.change_type_order + or defaults.change_type_order + ) + self.rev_range = args.get("rev_range") + self.tag_format: str = ( + args.get("tag_format") or self.config.settings["tag_format"] + ) + self.tag_rules = TagRules( + scheme=self.scheme, + tag_format=self.tag_format, + legacy_tag_formats=self.config.settings["legacy_tag_formats"], + ignored_tag_formats=self.config.settings["ignored_tag_formats"], + merge_prereleases=args.get("merge_prerelease") + or self.config.settings["changelog_merge_prerelease"], + ) + + self.template = ( + args.get("template") + or self.config.settings.get("template") + or self.changelog_format.template + ) + self.extras = args.get("extras") or {} + self.export_template_to = args.get("export_template") + + def _find_incremental_rev(self, latest_version: str, tags: list[GitTag]) -> str: + """Try to find the 'start_rev'. + + We use a similarity approach. We know how to parse the version from the markdown + changelog, but not the whole tag, we don't even know how's the tag made. + + This 'smart' function tries to find a similarity between the found version number + and the available tag. + + The SIMILARITY_THRESHOLD is an empirical value, it may have to be adjusted based + on our experience. + """ + SIMILARITY_THRESHOLD = 0.89 + tag_ratio = map( + lambda tag: ( + SequenceMatcher( + None, latest_version, strip_local_version(tag.name) + ).ratio(), + tag, + ), + tags, + ) + try: + score, tag = max(tag_ratio, key=itemgetter(0)) + except ValueError: + raise NoRevisionError() + if score < SIMILARITY_THRESHOLD: + raise NoRevisionError() + start_rev = tag.name + return start_rev + + def write_changelog( + self, changelog_out: str, lines: list[str], changelog_meta: changelog.Metadata + ): + changelog_hook: Callable | None = self.cz.changelog_hook + with smart_open(self.file_name, "w", encoding=self.encoding) as changelog_file: + partial_changelog: str | None = None + if self.incremental: + new_lines = changelog.incremental_build( + changelog_out, lines, changelog_meta + ) + changelog_out = "".join(new_lines) + partial_changelog = changelog_out + + if changelog_hook: + changelog_out = changelog_hook(changelog_out, partial_changelog) + + changelog_file.write(changelog_out) + + def export_template(self): + tpl = changelog.get_changelog_template(self.cz.template_loader, self.template) + src = Path(tpl.filename) + Path(self.export_template_to).write_text(src.read_text()) + + def __call__(self): + commit_parser = self.cz.commit_parser + changelog_pattern = self.cz.changelog_pattern + start_rev = self.start_rev + unreleased_version = self.unreleased_version + changelog_meta = changelog.Metadata() + change_type_map: dict | None = self.change_type_map + changelog_message_builder_hook: MessageBuilderHook | None = ( + self.cz.changelog_message_builder_hook + ) + changelog_release_hook: ChangelogReleaseHook | None = ( + self.cz.changelog_release_hook + ) + + if self.export_template_to: + return self.export_template() + + if not changelog_pattern or not commit_parser: + raise NoPatternMapError( + f"'{self.config.settings['name']}' rule does not support changelog" + ) + + if self.incremental and self.rev_range: + raise NotAllowed("--incremental cannot be combined with a rev_range") + + # Don't continue if no `file_name` specified. + assert self.file_name + + tags = self.tag_rules.get_version_tags(git.get_tags(), warn=True) + end_rev = "" + if self.incremental: + changelog_meta = self.changelog_format.get_metadata(self.file_name) + if changelog_meta.latest_version: + start_rev = self._find_incremental_rev( + strip_local_version(changelog_meta.latest_version_tag), tags + ) + if self.rev_range: + start_rev, end_rev = changelog.get_oldest_and_newest_rev( + tags, + self.rev_range, + self.tag_rules, + ) + commits = git.get_commits(start=start_rev, end=end_rev, args="--topo-order") + if not commits and ( + self.current_version is None or not self.current_version.is_prerelease + ): + raise NoCommitsFoundError("No commits found") + tree = changelog.generate_tree_from_commits( + commits, + tags, + commit_parser, + changelog_pattern, + unreleased_version, + change_type_map=change_type_map, + changelog_message_builder_hook=changelog_message_builder_hook, + changelog_release_hook=changelog_release_hook, + rules=self.tag_rules, + ) + if self.change_type_order: + tree = changelog.order_changelog_tree(tree, self.change_type_order) + + extras = self.cz.template_extras.copy() + extras.update(self.config.settings["extras"]) + extras.update(self.extras) + changelog_out = changelog.render_changelog( + tree, loader=self.cz.template_loader, template=self.template, **extras + ) + changelog_out = changelog_out.lstrip("\n") + + # Dry_run is executed here to avoid checking and reading the files + if self.dry_run: + changelog_hook: Callable | None = self.cz.changelog_hook + if changelog_hook: + changelog_out = changelog_hook(changelog_out, "") + out.write(changelog_out) + raise DryRunExit() + + lines = [] + if self.incremental and os.path.isfile(self.file_name): + with open(self.file_name, encoding=self.encoding) as changelog_file: + lines = changelog_file.readlines() + + self.write_changelog(changelog_out, lines, changelog_meta) diff --git a/commitizen/commands/check.py b/commitizen/commands/check.py new file mode 100644 index 0000000..e22155c --- /dev/null +++ b/commitizen/commands/check.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import os +import re +import sys +from typing import Any + +from commitizen import factory, git, out +from commitizen.config import BaseConfig +from commitizen.exceptions import ( + InvalidCommandArgumentError, + InvalidCommitMessageError, + NoCommitsFoundError, +) + + +class Check: + """Check if the current commit msg matches the commitizen format.""" + + def __init__(self, config: BaseConfig, arguments: dict[str, Any], cwd=os.getcwd()): + """Initial check command. + + Args: + config: The config object required for the command to perform its action + arguments: All the flags provided by the user + cwd: Current work directory + """ + self.commit_msg_file: str | None = arguments.get("commit_msg_file") + self.commit_msg: str | None = arguments.get("message") + self.rev_range: str | None = arguments.get("rev_range") + self.allow_abort: bool = bool( + arguments.get("allow_abort", config.settings["allow_abort"]) + ) + self.max_msg_length: int = arguments.get("message_length_limit", 0) + + # we need to distinguish between None and [], which is a valid value + + allowed_prefixes = arguments.get("allowed_prefixes") + self.allowed_prefixes: list[str] = ( + allowed_prefixes + if allowed_prefixes is not None + else config.settings["allowed_prefixes"] + ) + + self._valid_command_argument() + + self.config: BaseConfig = config + self.encoding = config.settings["encoding"] + self.cz = factory.commiter_factory(self.config) + + def _valid_command_argument(self): + num_exclusive_args_provided = sum( + arg is not None + for arg in (self.commit_msg_file, self.commit_msg, self.rev_range) + ) + if num_exclusive_args_provided == 0 and not sys.stdin.isatty(): + self.commit_msg = sys.stdin.read() + elif num_exclusive_args_provided != 1: + raise InvalidCommandArgumentError( + "Only one of --rev-range, --message, and --commit-msg-file is permitted by check command! " + "See 'cz check -h' for more information" + ) + + def __call__(self): + """Validate if commit messages follows the conventional pattern. + + Raises: + InvalidCommitMessageError: if the commit provided not follows the conventional pattern + """ + commits = self._get_commits() + if not commits: + raise NoCommitsFoundError(f"No commit found with range: '{self.rev_range}'") + + pattern = self.cz.schema_pattern() + ill_formated_commits = [ + commit + for commit in commits + if not self.validate_commit_message(commit.message, pattern) + ] + displayed_msgs_content = "\n".join( + [ + f'commit "{commit.rev}": "{commit.message}"' + for commit in ill_formated_commits + ] + ) + if displayed_msgs_content: + raise InvalidCommitMessageError( + "commit validation: failed!\n" + "please enter a commit message in the commitizen format.\n" + f"{displayed_msgs_content}\n" + f"pattern: {pattern}" + ) + out.success("Commit validation: successful!") + + def _get_commits(self): + msg = None + # Get commit message from file (--commit-msg-file) + if self.commit_msg_file is not None: + # Enter this branch if commit_msg_file is "". + with open(self.commit_msg_file, encoding=self.encoding) as commit_file: + msg = commit_file.read() + # Get commit message from command line (--message) + elif self.commit_msg is not None: + msg = self.commit_msg + if msg is not None: + msg = self._filter_comments(msg) + return [git.GitCommit(rev="", title="", body=msg)] + + # Get commit messages from git log (--rev-range) + if self.rev_range: + return git.get_commits(end=self.rev_range) + return git.get_commits() + + @staticmethod + def _filter_comments(msg: str) -> str: + """Filter the commit message by removing comments. + + When using `git commit --verbose`, we exclude the diff that is going to + generated, like the following example: + + ```bash + ... + # ------------------------ >8 ------------------------ + # Do not modify or remove the line above. + # Everything below it will be ignored. + diff --git a/... b/... + ... + ``` + + Args: + msg: The commit message to filter. + + Returns: + The filtered commit message without comments. + """ + + lines = [] + for line in msg.split("\n"): + if "# ------------------------ >8 ------------------------" in line: + break + if not line.startswith("#"): + lines.append(line) + return "\n".join(lines) + + def validate_commit_message(self, commit_msg: str, pattern: str) -> bool: + if not commit_msg: + return self.allow_abort + + if any(map(commit_msg.startswith, self.allowed_prefixes)): + return True + if self.max_msg_length: + msg_len = len(commit_msg.partition("\n")[0].strip()) + if msg_len > self.max_msg_length: + return False + return bool(re.match(pattern, commit_msg)) diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py new file mode 100644 index 0000000..abecb3b --- /dev/null +++ b/commitizen/commands/commit.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +import contextlib +import os +import shutil +import subprocess +import tempfile + +import questionary + +from commitizen import factory, git, out +from commitizen.config import BaseConfig +from commitizen.cz.exceptions import CzException +from commitizen.cz.utils import get_backup_file_path +from commitizen.exceptions import ( + CommitError, + CommitMessageLengthExceededError, + CustomError, + DryRunExit, + NoAnswersError, + NoCommitBackupError, + NotAGitProjectError, + NotAllowed, + NothingToCommitError, +) +from commitizen.git import smart_open + + +class Commit: + """Show prompt for the user to create a guided commit.""" + + def __init__(self, config: BaseConfig, arguments: dict): + if not git.is_git_project(): + raise NotAGitProjectError() + + self.config: BaseConfig = config + self.encoding = config.settings["encoding"] + self.cz = factory.commiter_factory(self.config) + self.arguments = arguments + self.temp_file: str = get_backup_file_path() + + def read_backup_message(self) -> str | None: + # Check the commit backup file exists + if not os.path.isfile(self.temp_file): + return None + + # Read commit message from backup + with open(self.temp_file, encoding=self.encoding) as f: + return f.read().strip() + + def prompt_commit_questions(self) -> str: + # Prompt user for the commit message + cz = self.cz + questions = cz.questions() + for question in filter(lambda q: q["type"] == "list", questions): + question["use_shortcuts"] = self.config.settings["use_shortcuts"] + try: + answers = questionary.prompt(questions, style=cz.style) + except ValueError as err: + root_err = err.__context__ + if isinstance(root_err, CzException): + raise CustomError(root_err.__str__()) + raise err + + if not answers: + raise NoAnswersError() + + message = cz.message(answers) + message_len = len(message.partition("\n")[0].strip()) + message_length_limit: int = self.arguments.get("message_length_limit", 0) + if 0 < message_length_limit < message_len: + raise CommitMessageLengthExceededError( + f"Length of commit message exceeds limit ({message_len}/{message_length_limit})" + ) + + return message + + def manual_edit(self, message: str) -> str: + editor = git.get_core_editor() + if editor is None: + raise RuntimeError("No 'editor' value given and no default available.") + exec_path = shutil.which(editor) + if exec_path is None: + raise RuntimeError(f"Editor '{editor}' not found.") + with tempfile.NamedTemporaryFile(mode="w", delete=False) as file: + file.write(message) + file_path = file.name + argv = [exec_path, file_path] + subprocess.call(argv) + with open(file_path) as temp_file: + message = temp_file.read().strip() + file.unlink() + return message + + def __call__(self): + extra_args: str = self.arguments.get("extra_cli_args", "") + + allow_empty: bool = "--allow-empty" in extra_args + + dry_run: bool = self.arguments.get("dry_run") + write_message_to_file: bool = self.arguments.get("write_message_to_file") + manual_edit: bool = self.arguments.get("edit") + + is_all: bool = self.arguments.get("all") + if is_all: + c = git.add("-u") + + if git.is_staging_clean() and not (dry_run or allow_empty): + raise NothingToCommitError("No files added to staging!") + + if write_message_to_file is not None and write_message_to_file.is_dir(): + raise NotAllowed(f"{write_message_to_file} is a directory") + + retry: bool = self.arguments.get("retry") + no_retry: bool = self.arguments.get("no_retry") + retry_after_failure: bool = self.config.settings.get("retry_after_failure") + + if retry: + m = self.read_backup_message() + if m is None: + raise NoCommitBackupError() + elif retry_after_failure and not no_retry: + m = self.read_backup_message() + if m is None: + m = self.prompt_commit_questions() + else: + m = self.prompt_commit_questions() + + if manual_edit: + m = self.manual_edit(m) + + out.info(f"\n{m}\n") + + if write_message_to_file: + with smart_open(write_message_to_file, "w", encoding=self.encoding) as file: + file.write(m) + + if dry_run: + raise DryRunExit() + + always_signoff: bool = self.config.settings["always_signoff"] + signoff: bool = self.arguments.get("signoff") + + if signoff: + out.warn( + "signoff mechanic is deprecated, please use `cz commit -- -s` instead." + ) + + if always_signoff or signoff: + extra_args = f"{extra_args} -s".strip() + + c = git.commit(m, args=extra_args) + + if c.return_code != 0: + out.error(c.err) + + # Create commit backup + with smart_open(self.temp_file, "w", encoding=self.encoding) as f: + f.write(m) + + raise CommitError() + + if "nothing added" in c.out or "no changes added to commit" in c.out: + out.error(c.out) + else: + with contextlib.suppress(FileNotFoundError): + os.remove(self.temp_file) + out.write(c.err) + out.write(c.out) + out.success("Commit successful!") diff --git a/commitizen/commands/example.py b/commitizen/commands/example.py new file mode 100644 index 0000000..e7abe7b --- /dev/null +++ b/commitizen/commands/example.py @@ -0,0 +1,13 @@ +from commitizen import factory, out +from commitizen.config import BaseConfig + + +class Example: + """Show an example so people understands the rules.""" + + def __init__(self, config: BaseConfig, *args): + self.config: BaseConfig = config + self.cz = factory.commiter_factory(self.config) + + def __call__(self): + out.write(self.cz.example()) diff --git a/commitizen/commands/info.py b/commitizen/commands/info.py new file mode 100644 index 0000000..afac979 --- /dev/null +++ b/commitizen/commands/info.py @@ -0,0 +1,13 @@ +from commitizen import factory, out +from commitizen.config import BaseConfig + + +class Info: + """Show in depth explanation of your rules.""" + + def __init__(self, config: BaseConfig, *args): + self.config: BaseConfig = config + self.cz = factory.commiter_factory(self.config) + + def __call__(self): + out.write(self.cz.info()) diff --git a/commitizen/commands/init.py b/commitizen/commands/init.py new file mode 100644 index 0000000..df872ec --- /dev/null +++ b/commitizen/commands/init.py @@ -0,0 +1,374 @@ +from __future__ import annotations + +import os +import shutil +from typing import Any + +import questionary +import yaml + +from commitizen import cmd, factory, out +from commitizen.__version__ import __version__ +from commitizen.config import BaseConfig, JsonConfig, TomlConfig, YAMLConfig +from commitizen.cz import registry +from commitizen.defaults import DEFAULT_SETTINGS, config_files +from commitizen.exceptions import InitFailedError, NoAnswersError +from commitizen.git import get_latest_tag_name, get_tag_names, smart_open +from commitizen.version_schemes import KNOWN_SCHEMES, Version, get_version_scheme + + +class ProjectInfo: + """Discover information about the current folder.""" + + @property + def has_pyproject(self) -> bool: + return os.path.isfile("pyproject.toml") + + @property + def has_uv_lock(self) -> bool: + return os.path.isfile("uv.lock") + + @property + def has_setup(self) -> bool: + return os.path.isfile("setup.py") + + @property + def has_pre_commit_config(self) -> bool: + return os.path.isfile(".pre-commit-config.yaml") + + @property + def is_python_uv(self) -> bool: + return self.has_pyproject and self.has_uv_lock + + @property + def is_python_poetry(self) -> bool: + if not self.has_pyproject: + return False + with open("pyproject.toml") as f: + return "[tool.poetry]" in f.read() + + @property + def is_python(self) -> bool: + return self.has_pyproject or self.has_setup + + @property + def is_rust_cargo(self) -> bool: + return os.path.isfile("Cargo.toml") + + @property + def is_npm_package(self) -> bool: + return os.path.isfile("package.json") + + @property + def is_php_composer(self) -> bool: + return os.path.isfile("composer.json") + + @property + def latest_tag(self) -> str | None: + return get_latest_tag_name() + + def tags(self) -> list | None: + """Not a property, only use if necessary""" + if self.latest_tag is None: + return None + return get_tag_names() + + @property + def is_pre_commit_installed(self) -> bool: + return bool(shutil.which("pre-commit")) + + +class Init: + def __init__(self, config: BaseConfig, *args): + self.config: BaseConfig = config + self.encoding = config.settings["encoding"] + self.cz = factory.commiter_factory(self.config) + self.project_info = ProjectInfo() + + def __call__(self): + if self.config.path: + out.line(f"Config file {self.config.path} already exists") + return + + out.info("Welcome to commitizen!\n") + out.line( + "Answer the questions to configure your project.\n" + "For further configuration visit:\n" + "\n" + "https://commitizen-tools.github.io/commitizen/config/" + "\n" + ) + + # Collect information + try: + config_path = self._ask_config_path() # select + cz_name = self._ask_name() # select + version_provider = self._ask_version_provider() # select + tag = self._ask_tag() # confirm & select + version_scheme = self._ask_version_scheme() # select + version = get_version_scheme(self.config.settings, version_scheme)(tag) + tag_format = self._ask_tag_format(tag) # confirm & text + update_changelog_on_bump = self._ask_update_changelog_on_bump() # confirm + major_version_zero = self._ask_major_version_zero(version) # confirm + except KeyboardInterrupt: + raise InitFailedError("Stopped by user") + + # Initialize configuration + if "toml" in config_path: + self.config = TomlConfig(data="", path=config_path) + elif "json" in config_path: + self.config = JsonConfig(data="{}", path=config_path) + elif "yaml" in config_path: + self.config = YAMLConfig(data="", path=config_path) + values_to_add = {} + values_to_add["name"] = cz_name + values_to_add["tag_format"] = tag_format + values_to_add["version_scheme"] = version_scheme + + if version_provider == "commitizen": + values_to_add["version"] = version.public + else: + values_to_add["version_provider"] = version_provider + + if update_changelog_on_bump: + values_to_add["update_changelog_on_bump"] = update_changelog_on_bump + + if major_version_zero: + values_to_add["major_version_zero"] = major_version_zero + + # Collect hook data + hook_types = questionary.checkbox( + "What types of pre-commit hook you want to install? (Leave blank if you don't want to install)", + choices=[ + questionary.Choice("commit-msg", checked=False), + questionary.Choice("pre-push", checked=False), + ], + ).unsafe_ask() + if hook_types: + try: + self._install_pre_commit_hook(hook_types) + except InitFailedError as e: + raise InitFailedError(f"Failed to install pre-commit hook.\n{e}") + + # Create and initialize config + self.config.init_empty_config_content() + self._update_config_file(values_to_add) + + out.write("\nYou can bump the version running:\n") + out.info("\tcz bump\n") + out.success("Configuration complete ๐Ÿš€") + + def _ask_config_path(self) -> str: + default_path = ".cz.toml" + if self.project_info.has_pyproject: + default_path = "pyproject.toml" + + name: str = questionary.select( + "Please choose a supported config file: ", + choices=config_files, + default=default_path, + style=self.cz.style, + ).unsafe_ask() + return name + + def _ask_name(self) -> str: + name: str = questionary.select( + "Please choose a cz (commit rule): (default: cz_conventional_commits)", + choices=list(registry.keys()), + default="cz_conventional_commits", + style=self.cz.style, + ).unsafe_ask() + return name + + def _ask_tag(self) -> str: + latest_tag = self.project_info.latest_tag + if not latest_tag: + out.error("No Existing Tag. Set tag to v0.0.1") + return "0.0.1" + + is_correct_tag = questionary.confirm( + f"Is {latest_tag} the latest tag?", style=self.cz.style, default=False + ).unsafe_ask() + if not is_correct_tag: + tags = self.project_info.tags() + if not tags: + out.error("No Existing Tag. Set tag to v0.0.1") + return "0.0.1" + + # the latest tag is most likely with the largest number. Thus list the tags in reverse order makes more sense + sorted_tags = sorted(tags, reverse=True) + latest_tag = questionary.select( + "Please choose the latest tag: ", + choices=sorted_tags, + style=self.cz.style, + ).unsafe_ask() + + if not latest_tag: + raise NoAnswersError("Tag is required!") + return latest_tag + + def _ask_tag_format(self, latest_tag) -> str: + is_correct_format = False + if latest_tag.startswith("v"): + tag_format = r"v$version" + is_correct_format = questionary.confirm( + f'Is "{tag_format}" the correct tag format?', style=self.cz.style + ).unsafe_ask() + + default_format = DEFAULT_SETTINGS["tag_format"] + if not is_correct_format: + tag_format = questionary.text( + f'Please enter the correct version format: (default: "{default_format}")', + style=self.cz.style, + ).unsafe_ask() + + if not tag_format: + tag_format = default_format + return tag_format + + def _ask_version_provider(self) -> str: + """Ask for setting: version_provider""" + + OPTS = { + "commitizen": "commitizen: Fetch and set version in commitizen config (default)", + "cargo": "cargo: Get and set version from Cargo.toml:project.version field", + "composer": "composer: Get and set version from composer.json:project.version field", + "npm": "npm: Get and set version from package.json:project.version field", + "pep621": "pep621: Get and set version from pyproject.toml:project.version field", + "poetry": "poetry: Get and set version from pyproject.toml:tool.poetry.version field", + "uv": "uv: Get and Get and set version from pyproject.toml and uv.lock", + "scm": "scm: Fetch the version from git and does not need to set it back", + } + + default_val = "commitizen" + if self.project_info.is_python: + if self.project_info.is_python_poetry: + default_val = "poetry" + elif self.project_info.is_python_uv: + default_val = "uv" + else: + default_val = "pep621" + elif self.project_info.is_rust_cargo: + default_val = "cargo" + elif self.project_info.is_npm_package: + default_val = "npm" + elif self.project_info.is_php_composer: + default_val = "composer" + + choices = [ + questionary.Choice(title=title, value=value) + for value, title in OPTS.items() + ] + default = next(filter(lambda x: x.value == default_val, choices)) + version_provider: str = questionary.select( + "Choose the source of the version:", + choices=choices, + style=self.cz.style, + default=default, + ).unsafe_ask() + return version_provider + + def _ask_version_scheme(self) -> str: + """Ask for setting: version_scheme""" + default = "semver" + if self.project_info.is_python: + default = "pep440" + + scheme: str = questionary.select( + "Choose version scheme: ", + choices=list(KNOWN_SCHEMES), + style=self.cz.style, + default=default, + ).unsafe_ask() + return scheme + + def _ask_major_version_zero(self, version: Version) -> bool: + """Ask for setting: major_version_zero""" + if version.major > 0: + return False + major_version_zero: bool = questionary.confirm( + "Keep major version zero (0.x) during breaking changes", + default=True, + auto_enter=True, + ).unsafe_ask() + return major_version_zero + + def _ask_update_changelog_on_bump(self) -> bool: + "Ask for setting: update_changelog_on_bump" + update_changelog_on_bump: bool = questionary.confirm( + "Create changelog automatically on bump", + default=True, + auto_enter=True, + ).unsafe_ask() + return update_changelog_on_bump + + def _exec_install_pre_commit_hook(self, hook_types: list[str]): + cmd_str = self._gen_pre_commit_cmd(hook_types) + c = cmd.run(cmd_str) + if c.return_code != 0: + err_msg = ( + f"Error running {cmd_str}." + "Outputs are attached below:\n" + f"stdout: {c.out}\n" + f"stderr: {c.err}" + ) + raise InitFailedError(err_msg) + + def _gen_pre_commit_cmd(self, hook_types: list[str]) -> str: + """Generate pre-commit command according to given hook types""" + if not hook_types: + raise ValueError("At least 1 hook type should be provided.") + cmd_str = "pre-commit install " + " ".join( + f"--hook-type {ty}" for ty in hook_types + ) + return cmd_str + + def _install_pre_commit_hook(self, hook_types: list[str] | None = None): + pre_commit_config_filename = ".pre-commit-config.yaml" + cz_hook_config = { + "repo": "https://github.com/commitizen-tools/commitizen", + "rev": f"v{__version__}", + "hooks": [ + {"id": "commitizen"}, + {"id": "commitizen-branch", "stages": ["push"]}, + ], + } + + config_data = {} + if not self.project_info.has_pre_commit_config: + # .pre-commit-config.yaml does not exist + config_data["repos"] = [cz_hook_config] + else: + with open( + pre_commit_config_filename, encoding=self.encoding + ) as config_file: + yaml_data = yaml.safe_load(config_file) + if yaml_data: + config_data = yaml_data + + if "repos" in config_data: + for pre_commit_hook in config_data["repos"]: + if "commitizen" in pre_commit_hook["repo"]: + out.write("commitizen already in pre-commit config") + break + else: + config_data["repos"].append(cz_hook_config) + else: + # .pre-commit-config.yaml exists but there's no "repos" key + config_data["repos"] = [cz_hook_config] + + with smart_open( + pre_commit_config_filename, "w", encoding=self.encoding + ) as config_file: + yaml.safe_dump(config_data, stream=config_file) + + if not self.project_info.is_pre_commit_installed: + raise InitFailedError("pre-commit is not installed in current environment.") + if hook_types is None: + hook_types = ["commit-msg", "pre-push"] + self._exec_install_pre_commit_hook(hook_types) + out.write("commitizen pre-commit hook is now installed in your '.git'\n") + + def _update_config_file(self, values: dict[str, Any]): + for key, value in values.items(): + self.config.set_key(key, value) diff --git a/commitizen/commands/list_cz.py b/commitizen/commands/list_cz.py new file mode 100644 index 0000000..9970186 --- /dev/null +++ b/commitizen/commands/list_cz.py @@ -0,0 +1,13 @@ +from commitizen import out +from commitizen.config import BaseConfig +from commitizen.cz import registry + + +class ListCz: + """List currently installed rules.""" + + def __init__(self, config: BaseConfig, *args): + self.config: BaseConfig = config + + def __call__(self): + out.write("\n".join(registry.keys())) diff --git a/commitizen/commands/schema.py b/commitizen/commands/schema.py new file mode 100644 index 0000000..0940648 --- /dev/null +++ b/commitizen/commands/schema.py @@ -0,0 +1,13 @@ +from commitizen import factory, out +from commitizen.config import BaseConfig + + +class Schema: + """Show structure of the rule.""" + + def __init__(self, config: BaseConfig, *args): + self.config: BaseConfig = config + self.cz = factory.commiter_factory(self.config) + + def __call__(self): + out.write(self.cz.schema()) diff --git a/commitizen/commands/version.py b/commitizen/commands/version.py new file mode 100644 index 0000000..45d553c --- /dev/null +++ b/commitizen/commands/version.py @@ -0,0 +1,39 @@ +import platform +import sys + +from commitizen import out +from commitizen.__version__ import __version__ +from commitizen.config import BaseConfig +from commitizen.providers import get_provider + + +class Version: + """Get the version of the installed commitizen or the current project.""" + + def __init__(self, config: BaseConfig, *args): + self.config: BaseConfig = config + self.parameter = args[0] + self.operating_system = platform.system() + self.python_version = sys.version + + def __call__(self): + if self.parameter.get("report"): + out.write(f"Commitizen Version: {__version__}") + out.write(f"Python Version: {self.python_version}") + out.write(f"Operating System: {self.operating_system}") + elif self.parameter.get("project"): + version = get_provider(self.config).get_version() + if version: + out.write(f"{version}") + else: + out.error("No project information in this project.") + elif self.parameter.get("verbose"): + out.write(f"Installed Commitizen Version: {__version__}") + version = get_provider(self.config).get_version() + if version: + out.write(f"Project Version: {version}") + else: + out.error("No project information in this project.") + else: + # if no argument is given, show installed commitizen version + out.write(f"{__version__}") diff --git a/commitizen/config/__init__.py b/commitizen/config/__init__.py new file mode 100644 index 0000000..f3720bb --- /dev/null +++ b/commitizen/config/__init__.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from pathlib import Path + +from commitizen import defaults, git +from commitizen.exceptions import ConfigFileIsEmpty, ConfigFileNotFound + +from .base_config import BaseConfig +from .json_config import JsonConfig +from .toml_config import TomlConfig +from .yaml_config import YAMLConfig + + +def read_cfg(filepath: str | None = None) -> BaseConfig: + conf = BaseConfig() + + if filepath is not None: + if not Path(filepath).exists(): + raise ConfigFileNotFound() + + cfg_paths = (path for path in (Path(filepath),)) + else: + git_project_root = git.find_git_project_root() + cfg_search_paths = [Path(".")] + if git_project_root: + cfg_search_paths.append(git_project_root) + + cfg_paths = ( + path / Path(filename) + for path in cfg_search_paths + for filename in defaults.config_files + ) + + for filename in cfg_paths: + if not filename.exists(): + continue + + _conf: TomlConfig | JsonConfig | YAMLConfig + + with open(filename, "rb") as f: + data: bytes = f.read() + + if "toml" in filename.suffix: + _conf = TomlConfig(data=data, path=filename) + elif "json" in filename.suffix: + _conf = JsonConfig(data=data, path=filename) + elif "yaml" in filename.suffix: + _conf = YAMLConfig(data=data, path=filename) + + if filepath is not None and _conf.is_empty_config: + raise ConfigFileIsEmpty() + elif _conf.is_empty_config: + continue + else: + conf = _conf + break + + return conf diff --git a/commitizen/config/base_config.py b/commitizen/config/base_config.py new file mode 100644 index 0000000..478691a --- /dev/null +++ b/commitizen/config/base_config.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from pathlib import Path + +from commitizen.defaults import DEFAULT_SETTINGS, Settings + + +class BaseConfig: + def __init__(self): + self._settings: Settings = DEFAULT_SETTINGS.copy() + self.encoding = self.settings["encoding"] + self._path: Path | None = None + + @property + def settings(self) -> Settings: + return self._settings + + @property + def path(self) -> Path | None: + return self._path + + def set_key(self, key, value): + """Set or update a key in the conf. + + For now only strings are supported. + We use to update the version number. + """ + raise NotImplementedError() + + def update(self, data: Settings) -> None: + self._settings.update(data) + + def add_path(self, path: str | Path) -> None: + self._path = Path(path) + + def _parse_setting(self, data: bytes | str) -> None: + raise NotImplementedError() diff --git a/commitizen/config/json_config.py b/commitizen/config/json_config.py new file mode 100644 index 0000000..b6a07f4 --- /dev/null +++ b/commitizen/config/json_config.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from commitizen.exceptions import InvalidConfigurationError +from commitizen.git import smart_open + +from .base_config import BaseConfig + + +class JsonConfig(BaseConfig): + def __init__(self, *, data: bytes | str, path: Path | str): + super().__init__() + self.is_empty_config = False + self.add_path(path) + self._parse_setting(data) + + def init_empty_config_content(self): + with smart_open(self.path, "a", encoding=self.encoding) as json_file: + json.dump({"commitizen": {}}, json_file) + + def set_key(self, key, value): + """Set or update a key in the conf. + + For now only strings are supported. + We use to update the version number. + """ + with open(self.path, "rb") as f: + parser = json.load(f) + + parser["commitizen"][key] = value + with smart_open(self.path, "w", encoding=self.encoding) as f: + json.dump(parser, f, indent=2) + return self + + def _parse_setting(self, data: bytes | str) -> None: + """We expect to have a section in .cz.json looking like + + ``` + { + "commitizen": { + "name": "cz_conventional_commits" + } + } + ``` + """ + try: + doc = json.loads(data) + except json.JSONDecodeError as e: + raise InvalidConfigurationError(f"Failed to parse {self.path}: {e}") + + try: + self.settings.update(doc["commitizen"]) + except KeyError: + self.is_empty_config = True diff --git a/commitizen/config/toml_config.py b/commitizen/config/toml_config.py new file mode 100644 index 0000000..813389c --- /dev/null +++ b/commitizen/config/toml_config.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import os +from pathlib import Path + +from tomlkit import exceptions, parse, table + +from commitizen.exceptions import InvalidConfigurationError + +from .base_config import BaseConfig + + +class TomlConfig(BaseConfig): + def __init__(self, *, data: bytes | str, path: Path | str): + super().__init__() + self.is_empty_config = False + self.add_path(path) + self._parse_setting(data) + + def init_empty_config_content(self): + if os.path.isfile(self.path): + with open(self.path, "rb") as input_toml_file: + parser = parse(input_toml_file.read()) + else: + parser = parse("") + + with open(self.path, "wb") as output_toml_file: + if parser.get("tool") is None: + parser["tool"] = table() + parser["tool"]["commitizen"] = table() + output_toml_file.write(parser.as_string().encode(self.encoding)) + + def set_key(self, key, value): + """Set or update a key in the conf. + + For now only strings are supported. + We use to update the version number. + """ + with open(self.path, "rb") as f: + parser = parse(f.read()) + + parser["tool"]["commitizen"][key] = value + with open(self.path, "wb") as f: + f.write(parser.as_string().encode(self.encoding)) + return self + + def _parse_setting(self, data: bytes | str) -> None: + """We expect to have a section in pyproject looking like + + ``` + [tool.commitizen] + name = "cz_conventional_commits" + ``` + """ + try: + doc = parse(data) + except exceptions.ParseError as e: + raise InvalidConfigurationError(f"Failed to parse {self.path}: {e}") + + try: + self.settings.update(doc["tool"]["commitizen"]) # type: ignore + except exceptions.NonExistentKey: + self.is_empty_config = True diff --git a/commitizen/config/yaml_config.py b/commitizen/config/yaml_config.py new file mode 100644 index 0000000..2bb6fe3 --- /dev/null +++ b/commitizen/config/yaml_config.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from pathlib import Path + +import yaml + +from commitizen.exceptions import InvalidConfigurationError +from commitizen.git import smart_open + +from .base_config import BaseConfig + + +class YAMLConfig(BaseConfig): + def __init__(self, *, data: bytes | str, path: Path | str): + super().__init__() + self.is_empty_config = False + self.add_path(path) + self._parse_setting(data) + + def init_empty_config_content(self): + with smart_open(self.path, "a", encoding=self.encoding) as json_file: + yaml.dump({"commitizen": {}}, json_file, explicit_start=True) + + def _parse_setting(self, data: bytes | str) -> None: + """We expect to have a section in cz.yaml looking like + + ``` + commitizen: + name: cz_conventional_commits + ``` + """ + import yaml.scanner + + try: + doc = yaml.safe_load(data) + except yaml.YAMLError as e: + raise InvalidConfigurationError(f"Failed to parse {self.path}: {e}") + + try: + self.settings.update(doc["commitizen"]) + except (KeyError, TypeError): + self.is_empty_config = True + + def set_key(self, key, value): + """Set or update a key in the conf. + + For now only strings are supported. + We use to update the version number. + """ + with open(self.path, "rb") as yaml_file: + parser = yaml.load(yaml_file, Loader=yaml.FullLoader) + + parser["commitizen"][key] = value + with smart_open(self.path, "w", encoding=self.encoding) as yaml_file: + yaml.dump(parser, yaml_file, explicit_start=True) + + return self diff --git a/commitizen/cz/__init__.py b/commitizen/cz/__init__.py new file mode 100644 index 0000000..cb17fe3 --- /dev/null +++ b/commitizen/cz/__init__.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import importlib +import pkgutil +import sys +import warnings +from collections.abc import Iterable + +if sys.version_info >= (3, 10): + from importlib import metadata +else: + import importlib_metadata as metadata + +from commitizen.cz.base import BaseCommitizen + + +def discover_plugins( + path: Iterable[str] | None = None, +) -> dict[str, type[BaseCommitizen]]: + """Discover commitizen plugins on the path + + Args: + path (Path, optional): If provided, 'path' should be either None or a list of paths to look for + modules in. If path is None, all top-level modules on sys.path.. Defaults to None. + + Returns: + Dict[str, Type[BaseCommitizen]]: Registry with found plugins + """ + for _, name, _ in pkgutil.iter_modules(path): + if name.startswith("cz_"): + mod = importlib.import_module(name) + if hasattr(mod, "discover_this"): + warnings.warn( + UserWarning( + f"Legacy plugin '{name}' has been ignored: please expose it the 'commitizen.plugin' entrypoint" + ) + ) + + return { + ep.name: ep.load() for ep in metadata.entry_points(group="commitizen.plugin") + } + + +registry: dict[str, type[BaseCommitizen]] = discover_plugins() diff --git a/commitizen/cz/base.py b/commitizen/cz/base.py new file mode 100644 index 0000000..43455a7 --- /dev/null +++ b/commitizen/cz/base.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from abc import ABCMeta, abstractmethod +from collections.abc import Iterable +from typing import Any, Callable, Protocol + +from jinja2 import BaseLoader, PackageLoader +from prompt_toolkit.styles import Style, merge_styles + +from commitizen import git +from commitizen.config.base_config import BaseConfig +from commitizen.defaults import Questions + + +class MessageBuilderHook(Protocol): + def __call__( + self, message: dict[str, Any], commit: git.GitCommit + ) -> dict[str, Any] | Iterable[dict[str, Any]] | None: ... + + +class ChangelogReleaseHook(Protocol): + def __call__( + self, release: dict[str, Any], tag: git.GitTag | None + ) -> dict[str, Any]: ... + + +class BaseCommitizen(metaclass=ABCMeta): + bump_pattern: str | None = None + bump_map: dict[str, str] | None = None + bump_map_major_version_zero: dict[str, str] | None = None + default_style_config: list[tuple[str, str]] = [ + ("qmark", "fg:#ff9d00 bold"), + ("question", "bold"), + ("answer", "fg:#ff9d00 bold"), + ("pointer", "fg:#ff9d00 bold"), + ("highlighted", "fg:#ff9d00 bold"), + ("selected", "fg:#cc5454"), + ("separator", "fg:#cc5454"), + ("instruction", ""), + ("text", ""), + ("disabled", "fg:#858585 italic"), + ] + + # The whole subject will be parsed as message by default + # This allows supporting changelog for any rule system. + # It can be modified per rule + commit_parser: str | None = r"(?P<message>.*)" + changelog_pattern: str | None = r".*" + change_type_map: dict[str, str] | None = None + change_type_order: list[str] | None = None + + # Executed per message parsed by the commitizen + changelog_message_builder_hook: MessageBuilderHook | None = None + + # Executed only at the end of the changelog generation + changelog_hook: Callable[[str, str | None], str] | None = None + + # Executed for each release in the changelog + changelog_release_hook: ChangelogReleaseHook | None = None + + # Plugins can override templates and provide extra template data + template_loader: BaseLoader = PackageLoader("commitizen", "templates") + template_extras: dict[str, Any] = {} + + def __init__(self, config: BaseConfig) -> None: + self.config = config + if not self.config.settings.get("style"): + self.config.settings.update({"style": BaseCommitizen.default_style_config}) + + @abstractmethod + def questions(self) -> Questions: + """Questions regarding the commit message.""" + + @abstractmethod + def message(self, answers: dict) -> str: + """Format your git message.""" + + @property + def style(self): + return merge_styles( + [ + Style(BaseCommitizen.default_style_config), + Style(self.config.settings["style"]), + ] + ) + + def example(self) -> str: + """Example of the commit message.""" + raise NotImplementedError("Not Implemented yet") + + def schema(self) -> str: + """Schema definition of the commit message.""" + raise NotImplementedError("Not Implemented yet") + + def schema_pattern(self) -> str: + """Regex matching the schema used for message validation.""" + raise NotImplementedError("Not Implemented yet") + + def info(self) -> str: + """Information about the standardized commit message.""" + raise NotImplementedError("Not Implemented yet") + + def process_commit(self, commit: str) -> str: + """Process commit for changelog. + + If not overwritten, it returns the first line of commit. + """ + return commit.split("\n")[0] diff --git a/commitizen/cz/conventional_commits/__init__.py b/commitizen/cz/conventional_commits/__init__.py new file mode 100644 index 0000000..52624d2 --- /dev/null +++ b/commitizen/cz/conventional_commits/__init__.py @@ -0,0 +1 @@ +from .conventional_commits import ConventionalCommitsCz # noqa: F401 diff --git a/commitizen/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py new file mode 100644 index 0000000..c7b8825 --- /dev/null +++ b/commitizen/cz/conventional_commits/conventional_commits.py @@ -0,0 +1,212 @@ +import os +import re + +from commitizen import defaults +from commitizen.cz.base import BaseCommitizen +from commitizen.cz.utils import multiple_line_breaker, required_validator +from commitizen.defaults import Questions + +__all__ = ["ConventionalCommitsCz"] + + +def parse_scope(text): + if not text: + return "" + + scope = text.strip().split() + if len(scope) == 1: + return scope[0] + + return "-".join(scope) + + +def parse_subject(text): + if isinstance(text, str): + text = text.strip(".").strip() + + return required_validator(text, msg="Subject is required.") + + +class ConventionalCommitsCz(BaseCommitizen): + bump_pattern = defaults.bump_pattern + bump_map = defaults.bump_map + bump_map_major_version_zero = defaults.bump_map_major_version_zero + commit_parser = r"^((?P<change_type>feat|fix|refactor|perf|BREAKING CHANGE)(?:\((?P<scope>[^()\r\n]*)\)|\()?(?P<breaking>!)?|\w+!):\s(?P<message>.*)?" # noqa + change_type_map = { + "feat": "Feat", + "fix": "Fix", + "refactor": "Refactor", + "perf": "Perf", + } + changelog_pattern = defaults.bump_pattern + + def questions(self) -> Questions: + questions: Questions = [ + { + "type": "list", + "name": "prefix", + "message": "Select the type of change you are committing", + "choices": [ + { + "value": "fix", + "name": "fix: A bug fix. Correlates with PATCH in SemVer", + "key": "x", + }, + { + "value": "feat", + "name": "feat: A new feature. Correlates with MINOR in SemVer", + "key": "f", + }, + { + "value": "docs", + "name": "docs: Documentation only changes", + "key": "d", + }, + { + "value": "style", + "name": ( + "style: Changes that do not affect the " + "meaning of the code (white-space, formatting," + " missing semi-colons, etc)" + ), + "key": "s", + }, + { + "value": "refactor", + "name": ( + "refactor: A code change that neither fixes " + "a bug nor adds a feature" + ), + "key": "r", + }, + { + "value": "perf", + "name": "perf: A code change that improves performance", + "key": "p", + }, + { + "value": "test", + "name": ("test: Adding missing or correcting existing tests"), + "key": "t", + }, + { + "value": "build", + "name": ( + "build: Changes that affect the build system or " + "external dependencies (example scopes: pip, docker, npm)" + ), + "key": "b", + }, + { + "value": "ci", + "name": ( + "ci: Changes to CI configuration files and " + "scripts (example scopes: GitLabCI)" + ), + "key": "c", + }, + ], + }, + { + "type": "input", + "name": "scope", + "message": ( + "What is the scope of this change? (class or file name): (press [enter] to skip)\n" + ), + "filter": parse_scope, + }, + { + "type": "input", + "name": "subject", + "filter": parse_subject, + "message": ( + "Write a short and imperative summary of the code changes: (lower case and no period)\n" + ), + }, + { + "type": "input", + "name": "body", + "message": ( + "Provide additional contextual information about the code changes: (press [enter] to skip)\n" + ), + "filter": multiple_line_breaker, + }, + { + "type": "confirm", + "message": "Is this a BREAKING CHANGE? Correlates with MAJOR in SemVer", + "name": "is_breaking_change", + "default": False, + }, + { + "type": "input", + "name": "footer", + "message": ( + "Footer. Information about Breaking Changes and " + "reference issues that this commit closes: (press [enter] to skip)\n" + ), + }, + ] + return questions + + def message(self, answers: dict) -> str: + prefix = answers["prefix"] + scope = answers["scope"] + subject = answers["subject"] + body = answers["body"] + footer = answers["footer"] + is_breaking_change = answers["is_breaking_change"] + + if scope: + scope = f"({scope})" + if body: + body = f"\n\n{body}" + if is_breaking_change: + footer = f"BREAKING CHANGE: {footer}" + if footer: + footer = f"\n\n{footer}" + + message = f"{prefix}{scope}: {subject}{body}{footer}" + + return message + + def example(self) -> str: + return ( + "fix: correct minor typos in code\n" + "\n" + "see the issue for details on the typos fixed\n" + "\n" + "closes issue #12" + ) + + def schema(self) -> str: + return ( + "<type>(<scope>): <subject>\n" + "<BLANK LINE>\n" + "<body>\n" + "<BLANK LINE>\n" + "(BREAKING CHANGE: )<footer>" + ) + + def schema_pattern(self) -> str: + PATTERN = ( + r"(?s)" # To explicitly make . match new line + r"(build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert|bump)" # type + r"(\(\S+\))?!?:" # scope + r"( [^\n\r]+)" # subject + r"((\n\n.*)|(\s*))?$" + ) + return PATTERN + + def info(self) -> str: + dir_path = os.path.dirname(os.path.realpath(__file__)) + filepath = os.path.join(dir_path, "conventional_commits_info.txt") + with open(filepath, encoding=self.config.settings["encoding"]) as f: + content = f.read() + return content + + def process_commit(self, commit: str) -> str: + pat = re.compile(self.schema_pattern()) + m = re.match(pat, commit) + if m is None: + return "" + return m.group(3).strip() diff --git a/commitizen/cz/conventional_commits/conventional_commits_info.txt b/commitizen/cz/conventional_commits/conventional_commits_info.txt new file mode 100644 index 0000000..a076e4f --- /dev/null +++ b/commitizen/cz/conventional_commits/conventional_commits_info.txt @@ -0,0 +1,31 @@ +The commit contains the following structural elements, to communicate +intent to the consumers of your library: + +fix: a commit of the type fix patches a bug in your codebase +(this correlates with PATCH in semantic versioning). + +feat: a commit of the type feat introduces a new feature to the codebase +(this correlates with MINOR in semantic versioning). + +BREAKING CHANGE: a commit that has the text BREAKING CHANGE: at the beginning of +its optional body or footer section introduces a breaking API change +(correlating with MAJOR in semantic versioning). +A BREAKING CHANGE can be part of commits of any type. + +Others: commit types other than fix: and feat: are allowed, +like chore:, docs:, style:, refactor:, perf:, test:, and others. + +We also recommend improvement for commits that improve a current +implementation without adding a new feature or fixing a bug. + +Notice these types are not mandated by the conventional commits specification, +and have no implicit effect in semantic versioning (unless they include a BREAKING CHANGE). + +A scope may be provided to a commitโ€™s type, to provide additional contextual +information and is contained within parenthesis, e.g., feat(parser): add ability to parse arrays. + +<type>[optional scope]: <description> + +[optional body] + +[optional footer] diff --git a/commitizen/cz/customize/__init__.py b/commitizen/cz/customize/__init__.py new file mode 100644 index 0000000..c5af001 --- /dev/null +++ b/commitizen/cz/customize/__init__.py @@ -0,0 +1 @@ +from .customize import CustomizeCommitsCz # noqa diff --git a/commitizen/cz/customize/customize.py b/commitizen/cz/customize/customize.py new file mode 100644 index 0000000..d53ae29 --- /dev/null +++ b/commitizen/cz/customize/customize.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from jinja2 import Template +else: + try: + from jinja2 import Template + except ImportError: + from string import Template + + +from commitizen import defaults +from commitizen.config import BaseConfig +from commitizen.cz.base import BaseCommitizen +from commitizen.defaults import Questions +from commitizen.exceptions import MissingCzCustomizeConfigError + +__all__ = ["CustomizeCommitsCz"] + + +class CustomizeCommitsCz(BaseCommitizen): + bump_pattern = defaults.bump_pattern + bump_map = defaults.bump_map + bump_map_major_version_zero = defaults.bump_map_major_version_zero + change_type_order = defaults.change_type_order + + def __init__(self, config: BaseConfig): + super().__init__(config) + + if "customize" not in self.config.settings: + raise MissingCzCustomizeConfigError() + self.custom_settings = self.config.settings["customize"] + + custom_bump_pattern = self.custom_settings.get("bump_pattern") + if custom_bump_pattern: + self.bump_pattern = custom_bump_pattern + + custom_bump_map = self.custom_settings.get("bump_map") + if custom_bump_map: + self.bump_map = custom_bump_map + + custom_bump_map_major_version_zero = self.custom_settings.get( + "bump_map_major_version_zero" + ) + if custom_bump_map_major_version_zero: + self.bump_map_major_version_zero = custom_bump_map_major_version_zero + + custom_change_type_order = self.custom_settings.get("change_type_order") + if custom_change_type_order: + self.change_type_order = custom_change_type_order + + commit_parser = self.custom_settings.get("commit_parser") + if commit_parser: + self.commit_parser = commit_parser + + changelog_pattern = self.custom_settings.get("changelog_pattern") + if changelog_pattern: + self.changelog_pattern = changelog_pattern + + change_type_map = self.custom_settings.get("change_type_map") + if change_type_map: + self.change_type_map = change_type_map + + def questions(self) -> Questions: + return self.custom_settings.get("questions", [{}]) + + def message(self, answers: dict) -> str: + message_template = Template(self.custom_settings.get("message_template", "")) + if getattr(Template, "substitute", None): + return message_template.substitute(**answers) # type: ignore + else: + return message_template.render(**answers) + + def example(self) -> str: + return self.custom_settings.get("example") or "" + + def schema_pattern(self) -> str: + return self.custom_settings.get("schema_pattern") or "" + + def schema(self) -> str: + return self.custom_settings.get("schema") or "" + + def info(self) -> str: + info_path = self.custom_settings.get("info_path") + info = self.custom_settings.get("info") + if info_path: + with open(info_path, encoding=self.config.settings["encoding"]) as f: + content = f.read() + return content + elif info: + return info + return "" diff --git a/commitizen/cz/customize/customize_info.txt b/commitizen/cz/customize/customize_info.txt new file mode 100644 index 0000000..e69de29 diff --git a/commitizen/cz/exceptions.py b/commitizen/cz/exceptions.py new file mode 100644 index 0000000..d74f39a --- /dev/null +++ b/commitizen/cz/exceptions.py @@ -0,0 +1,4 @@ +class CzException(Exception): ... + + +class AnswerRequiredError(CzException): ... diff --git a/commitizen/cz/jira/__init__.py b/commitizen/cz/jira/__init__.py new file mode 100644 index 0000000..c4a98c0 --- /dev/null +++ b/commitizen/cz/jira/__init__.py @@ -0,0 +1,3 @@ +from .jira import JiraSmartCz + +__all__ = ["JiraSmartCz"] diff --git a/commitizen/cz/jira/jira.py b/commitizen/cz/jira/jira.py new file mode 100644 index 0000000..b8fd056 --- /dev/null +++ b/commitizen/cz/jira/jira.py @@ -0,0 +1,81 @@ +import os + +from commitizen.cz.base import BaseCommitizen +from commitizen.defaults import Questions + +__all__ = ["JiraSmartCz"] + + +class JiraSmartCz(BaseCommitizen): + def questions(self) -> Questions: + questions = [ + { + "type": "input", + "name": "message", + "message": "Git commit message (required):\n", + # 'validate': RequiredValidator, + "filter": lambda x: x.strip(), + }, + { + "type": "input", + "name": "issues", + "message": "Jira Issue ID(s) separated by spaces (required):\n", + # 'validate': RequiredValidator, + "filter": lambda x: x.strip(), + }, + { + "type": "input", + "name": "workflow", + "message": "Workflow command (testing, closed, etc.) (optional):\n", + "filter": lambda x: "#" + x.strip().replace(" ", "-") if x else "", + }, + { + "type": "input", + "name": "time", + "message": "Time spent (i.e. 3h 15m) (optional):\n", + "filter": lambda x: "#time " + x if x else "", + }, + { + "type": "input", + "name": "comment", + "message": "Jira comment (optional):\n", + "filter": lambda x: "#comment " + x if x else "", + }, + ] + return questions + + def message(self, answers: dict) -> str: + return " ".join( + filter( + bool, + [ + answers["message"], + answers["issues"], + answers["workflow"], + answers["time"], + answers["comment"], + ], + ) + ) + + def example(self) -> str: + return ( + "JRA-34 #comment corrected indent issue\n" + "JRA-35 #time 1w 2d 4h 30m Total work logged\n" + "JRA-123 JRA-234 JRA-345 #resolve\n" + "JRA-123 JRA-234 JRA-345 #resolve #time 2d 5h #comment Task completed " + "ahead of schedule" + ) + + def schema(self) -> str: + return "<ignored text> <ISSUE_KEY> <ignored text> #<COMMAND> <optional COMMAND_ARGUMENTS>" # noqa + + def schema_pattern(self) -> str: + return r".*[A-Z]{2,}\-[0-9]+( #| .* #).+( #.+)*" + + def info(self) -> str: + dir_path = os.path.dirname(os.path.realpath(__file__)) + filepath = os.path.join(dir_path, "jira_info.txt") + with open(filepath, encoding=self.config.settings["encoding"]) as f: + content = f.read() + return content diff --git a/commitizen/cz/jira/jira_info.txt b/commitizen/cz/jira/jira_info.txt new file mode 100644 index 0000000..f32bc12 --- /dev/null +++ b/commitizen/cz/jira/jira_info.txt @@ -0,0 +1,84 @@ +Smart Commits allow repository committers to perform actions such as transitioning JIRA Software +issues or creating Crucible code reviews by embedding specific commands into their commit messages. + +You can: +* comment on issues +* record time tracking information against issues +* transition issues to any status defined in the JIRA Software project's workflow. + +There are other actions available if you use Crucible. + +Each Smart Commit message must not span more than one line (i.e. you cannot use a carriage return in +the command), but you can add multiple commands to the same line. See this example below. + +Smart Commit commands +--------------------- + +The basic command line syntax for a Smart Commit message is: + +<ignored text> <ISSUE_KEY> <ignored text> #<COMMAND> <optional COMMAND_ARGUMENTS> + +Any text between the issue key and the Smart Commit command is ignored. + +There are three Smart Commit commands you can use in your commit messages: +* comment +* time +* transition + + +Comment +------- + +Description + +Adds a comment to a JIRA Software issue. + +Syntax + +<ignored text> ISSUE_KEY <ignored text> #comment <comment_string> + +Example + +JRA-34 #comment corrected indent issue + +Notes + +The committer's email address must match the email address of a single JIRA Software user with +permission to comment on issues in that particular project. + + +Time +---- + +Description + +Records time tracking information against an issue. + +Syntax + +<ignored text> ISSUE_KEY <ignored text> #time <value>w <value>d <value>h <value>m <comment_string> + +Example + +JRA-34 #time 1w 2d 4h 30m Total work logged + + +Workflow transitions +-------------------- + +Description + +Transitions a JIRA Software issue to a particular workflow state. + +Syntax + +<ignored text> ISSUE_KEY <ignored text> #<transition_name> <comment_string> + +Example + +JRA-090 #close Fixed this today + + +More information +---------------- +https://confluence.atlassian.com/fisheye/using-smart-commits-298976812.html diff --git a/commitizen/cz/utils.py b/commitizen/cz/utils.py new file mode 100644 index 0000000..7bc8967 --- /dev/null +++ b/commitizen/cz/utils.py @@ -0,0 +1,32 @@ +import os +import re +import tempfile + +from commitizen import git +from commitizen.cz import exceptions + + +def required_validator(answer, msg=None): + if not answer: + raise exceptions.AnswerRequiredError(msg) + return answer + + +def multiple_line_breaker(answer, sep="|"): + return "\n".join(line.strip() for line in answer.split(sep) if line) + + +def strip_local_version(version: str) -> str: + return re.sub(r"\+.+", "", version) + + +def get_backup_file_path() -> str: + project_root = git.find_git_project_root() + + if project_root is None: + project = "" + else: + project = project_root.as_posix().replace("/", "%") + + user = os.environ.get("USER", "") + return os.path.join(tempfile.gettempdir(), f"cz.commit%{user}%{project}.backup") diff --git a/commitizen/defaults.py b/commitizen/defaults.py new file mode 100644 index 0000000..0b78e1b --- /dev/null +++ b/commitizen/defaults.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +import pathlib +from collections import OrderedDict +from collections.abc import Iterable, MutableMapping, Sequence +from typing import Any, TypedDict + +# Type +Questions = Iterable[MutableMapping[str, Any]] + + +class CzSettings(TypedDict, total=False): + bump_pattern: str + bump_map: OrderedDict[str, str] + bump_map_major_version_zero: OrderedDict[str, str] + change_type_order: list[str] + + questions: Questions + example: str | None + schema_pattern: str | None + schema: str | None + info_path: str | pathlib.Path + info: str + message_template: str + commit_parser: str | None + changelog_pattern: str | None + change_type_map: dict[str, str] | None + + +class Settings(TypedDict, total=False): + name: str + version: str | None + version_files: list[str] + version_provider: str | None + version_scheme: str | None + version_type: str | None + tag_format: str + legacy_tag_formats: Sequence[str] + ignored_tag_formats: Sequence[str] + bump_message: str | None + retry_after_failure: bool + allow_abort: bool + allowed_prefixes: list[str] + changelog_file: str + changelog_format: str | None + changelog_incremental: bool + changelog_start_rev: str | None + changelog_merge_prerelease: bool + update_changelog_on_bump: bool + use_shortcuts: bool + style: list[tuple[str, str]] + customize: CzSettings + major_version_zero: bool + pre_bump_hooks: list[str] | None + post_bump_hooks: list[str] | None + prerelease_offset: int + encoding: str + always_signoff: bool + template: str | None + extras: dict[str, Any] + + +name: str = "cz_conventional_commits" +config_files: list[str] = [ + "pyproject.toml", + ".cz.toml", + ".cz.json", + "cz.json", + ".cz.yaml", + "cz.yaml", + "cz.toml", +] +encoding: str = "utf-8" + +DEFAULT_SETTINGS: Settings = { + "name": name, + "version": None, + "version_files": [], + "version_provider": "commitizen", + "version_scheme": None, + "tag_format": "$version", # example v$version + "legacy_tag_formats": [], + "ignored_tag_formats": [], + "bump_message": None, # bumped v$current_version to $new_version + "retry_after_failure": False, + "allow_abort": False, + "allowed_prefixes": [ + "Merge", + "Revert", + "Pull request", + "fixup!", + "squash!", + ], + "changelog_file": "CHANGELOG.md", + "changelog_format": None, # default guessed from changelog_file + "changelog_incremental": False, + "changelog_start_rev": None, + "changelog_merge_prerelease": False, + "update_changelog_on_bump": False, + "use_shortcuts": False, + "major_version_zero": False, + "pre_bump_hooks": [], + "post_bump_hooks": [], + "prerelease_offset": 0, + "encoding": encoding, + "always_signoff": False, + "template": None, # default provided by plugin + "extras": {}, +} + +MAJOR = "MAJOR" +MINOR = "MINOR" +PATCH = "PATCH" + +CHANGELOG_FORMAT = "markdown" + +bump_pattern = r"^((BREAKING[\-\ ]CHANGE|\w+)(\(.+\))?!?):" +bump_map = OrderedDict( + ( + (r"^.+!$", MAJOR), + (r"^BREAKING[\-\ ]CHANGE", MAJOR), + (r"^feat", MINOR), + (r"^fix", PATCH), + (r"^refactor", PATCH), + (r"^perf", PATCH), + ) +) +bump_map_major_version_zero = OrderedDict( + ( + (r"^.+!$", MINOR), + (r"^BREAKING[\-\ ]CHANGE", MINOR), + (r"^feat", MINOR), + (r"^fix", PATCH), + (r"^refactor", PATCH), + (r"^perf", PATCH), + ) +) +change_type_order = ["BREAKING CHANGE", "Feat", "Fix", "Refactor", "Perf"] +bump_message = "bump: version $current_version โ†’ $new_version" + + +def get_tag_regexes( + version_regex: str, +) -> dict[str, str]: + regexs = { + "version": version_regex, + "major": r"(?P<major>\d+)", + "minor": r"(?P<minor>\d+)", + "patch": r"(?P<patch>\d+)", + "prerelease": r"(?P<prerelease>\w+\d+)?", + "devrelease": r"(?P<devrelease>\.dev\d+)?", + } + return { + **{f"${k}": v for k, v in regexs.items()}, + **{f"${{{k}}}": v for k, v in regexs.items()}, + } diff --git a/commitizen/exceptions.py b/commitizen/exceptions.py new file mode 100644 index 0000000..29733b6 --- /dev/null +++ b/commitizen/exceptions.py @@ -0,0 +1,213 @@ +import enum + +from commitizen import out + + +class ExitCode(enum.IntEnum): + EXPECTED_EXIT = 0 + NO_COMMITIZEN_FOUND = 1 + NOT_A_GIT_PROJECT = 2 + NO_COMMITS_FOUND = 3 + NO_VERSION_SPECIFIED = 4 + NO_PATTERN_MAP = 5 + BUMP_COMMIT_FAILED = 6 + BUMP_TAG_FAILED = 7 + NO_ANSWERS = 8 + COMMIT_ERROR = 9 + NO_COMMIT_BACKUP = 10 + NOTHING_TO_COMMIT = 11 + CUSTOM_ERROR = 12 + NO_COMMAND_FOUND = 13 + INVALID_COMMIT_MSG = 14 + MISSING_CZ_CUSTOMIZE_CONFIG = 15 + NO_REVISION = 16 + CURRENT_VERSION_NOT_FOUND = 17 + INVALID_COMMAND_ARGUMENT = 18 + INVALID_CONFIGURATION = 19 + NOT_ALLOWED = 20 + NO_INCREMENT = 21 + UNRECOGNIZED_CHARACTERSET_ENCODING = 22 + GIT_COMMAND_ERROR = 23 + INVALID_MANUAL_VERSION = 24 + INIT_FAILED = 25 + RUN_HOOK_FAILED = 26 + VERSION_PROVIDER_UNKNOWN = 27 + VERSION_SCHEME_UNKNOWN = 28 + CHANGELOG_FORMAT_UNKNOWN = 29 + CONFIG_FILE_NOT_FOUND = 30 + CONFIG_FILE_IS_EMPTY = 31 + COMMIT_MESSAGE_LENGTH_LIMIT_EXCEEDED = 32 + + +class CommitizenException(Exception): + def __init__(self, *args, **kwargs): + self.output_method = kwargs.get("output_method") or out.error + self.exit_code: ExitCode = self.__class__.exit_code + if args: + self.message = args[0] + elif hasattr(self.__class__, "message"): + self.message = self.__class__.message + else: + self.message = "" + + def __str__(self): + return self.message + + +class ExpectedExit(CommitizenException): + exit_code = ExitCode.EXPECTED_EXIT + + def __init__(self, *args, **kwargs): + output_method = kwargs.get("output_method") or out.write + kwargs["output_method"] = output_method + super().__init__(*args, **kwargs) + + +class DryRunExit(ExpectedExit): + pass + + +class GetNextExit(ExpectedExit): + pass + + +class NoneIncrementExit(CommitizenException): + exit_code = ExitCode.NO_INCREMENT + + +class NoCommitizenFoundException(CommitizenException): + exit_code = ExitCode.NO_COMMITIZEN_FOUND + + +class NotAGitProjectError(CommitizenException): + exit_code = ExitCode.NOT_A_GIT_PROJECT + message = "fatal: not a git repository (or any of the parent directories): .git" + + +class MissingCzCustomizeConfigError(CommitizenException): + exit_code = ExitCode.MISSING_CZ_CUSTOMIZE_CONFIG + message = "fatal: customize is not set in configuration file." + + +class NoCommitsFoundError(CommitizenException): + exit_code = ExitCode.NO_COMMITS_FOUND + + +class NoVersionSpecifiedError(CommitizenException): + exit_code = ExitCode.NO_VERSION_SPECIFIED + message = ( + "[NO_VERSION_SPECIFIED]\n" + "Check if current version is specified in config file, like:\n" + "version = 0.4.3\n" + ) + + +class NoPatternMapError(CommitizenException): + exit_code = ExitCode.NO_PATTERN_MAP + + +class BumpCommitFailedError(CommitizenException): + exit_code = ExitCode.BUMP_COMMIT_FAILED + + +class BumpTagFailedError(CommitizenException): + exit_code = ExitCode.BUMP_TAG_FAILED + + +class CurrentVersionNotFoundError(CommitizenException): + exit_code = ExitCode.CURRENT_VERSION_NOT_FOUND + + +class NoAnswersError(CommitizenException): + exit_code = ExitCode.NO_ANSWERS + + +class CommitError(CommitizenException): + exit_code = ExitCode.COMMIT_ERROR + + +class NoCommitBackupError(CommitizenException): + exit_code = ExitCode.NO_COMMIT_BACKUP + message = "No commit backup found" + + +class NothingToCommitError(CommitizenException): + exit_code = ExitCode.NOTHING_TO_COMMIT + + +class CustomError(CommitizenException): + exit_code = ExitCode.CUSTOM_ERROR + + +class InvalidCommitMessageError(CommitizenException): + exit_code = ExitCode.INVALID_COMMIT_MSG + + +class NoRevisionError(CommitizenException): + exit_code = ExitCode.NO_REVISION + message = "No tag found to do an incremental changelog" + + +class NoCommandFoundError(CommitizenException): + exit_code = ExitCode.NO_COMMAND_FOUND + message = "Command is required" + + +class InvalidCommandArgumentError(CommitizenException): + exit_code = ExitCode.INVALID_COMMAND_ARGUMENT + + +class InvalidConfigurationError(CommitizenException): + exit_code = ExitCode.INVALID_CONFIGURATION + + +class NotAllowed(CommitizenException): + exit_code = ExitCode.NOT_ALLOWED + + +class CharacterSetDecodeError(CommitizenException): + exit_code = ExitCode.UNRECOGNIZED_CHARACTERSET_ENCODING + + +class GitCommandError(CommitizenException): + exit_code = ExitCode.GIT_COMMAND_ERROR + + +class InvalidManualVersion(CommitizenException): + exit_code = ExitCode.INVALID_MANUAL_VERSION + + +class InitFailedError(CommitizenException): + exit_code = ExitCode.INIT_FAILED + + +class RunHookError(CommitizenException): + exit_code = ExitCode.RUN_HOOK_FAILED + + +class VersionProviderUnknown(CommitizenException): + exit_code = ExitCode.VERSION_PROVIDER_UNKNOWN + + +class VersionSchemeUnknown(CommitizenException): + exit_code = ExitCode.VERSION_SCHEME_UNKNOWN + + +class ChangelogFormatUnknown(CommitizenException): + exit_code = ExitCode.CHANGELOG_FORMAT_UNKNOWN + message = "Unknown changelog format identifier" + + +class ConfigFileNotFound(CommitizenException): + exit_code = ExitCode.CONFIG_FILE_NOT_FOUND + message = "Cannot found the config file, please check your file path again." + + +class ConfigFileIsEmpty(CommitizenException): + exit_code = ExitCode.CONFIG_FILE_IS_EMPTY + message = "Config file is empty, please check your file path again." + + +class CommitMessageLengthExceededError(CommitizenException): + exit_code = ExitCode.COMMIT_MESSAGE_LENGTH_LIMIT_EXCEEDED + message = "Length of commit message exceeds the given limit." diff --git a/commitizen/factory.py b/commitizen/factory.py new file mode 100644 index 0000000..09af5fd --- /dev/null +++ b/commitizen/factory.py @@ -0,0 +1,19 @@ +from commitizen import BaseCommitizen +from commitizen.config import BaseConfig +from commitizen.cz import registry +from commitizen.exceptions import NoCommitizenFoundException + + +def commiter_factory(config: BaseConfig) -> BaseCommitizen: + """Return the correct commitizen existing in the registry.""" + name: str = config.settings["name"] + try: + _cz = registry[name](config) + except KeyError: + msg_error = ( + "The committer has not been found in the system.\n\n" + f"Try running 'pip install {name}'\n" + ) + raise NoCommitizenFoundException(msg_error) + else: + return _cz diff --git a/commitizen/git.py b/commitizen/git.py new file mode 100644 index 0000000..19ca46b --- /dev/null +++ b/commitizen/git.py @@ -0,0 +1,312 @@ +from __future__ import annotations + +import os +from enum import Enum +from os import linesep +from pathlib import Path +from tempfile import NamedTemporaryFile + +from commitizen import cmd, out +from commitizen.exceptions import GitCommandError + +UNIX_EOL = "\n" +WINDOWS_EOL = "\r\n" + + +class EOLTypes(Enum): + """The EOL type from `git config core.eol`.""" + + LF = "lf" + CRLF = "crlf" + NATIVE = "native" + + def get_eol_for_open(self) -> str: + """Get the EOL character for `open()`.""" + map = { + EOLTypes.CRLF: WINDOWS_EOL, + EOLTypes.LF: UNIX_EOL, + EOLTypes.NATIVE: linesep, + } + + return map[self] + + +class GitObject: + rev: str + name: str + date: str + + def __eq__(self, other) -> bool: + if not hasattr(other, "rev"): + return False + return self.rev == other.rev # type: ignore + + +class GitCommit(GitObject): + def __init__( + self, + rev, + title, + body: str = "", + author: str = "", + author_email: str = "", + parents: list[str] | None = None, + ): + self.rev = rev.strip() + self.title = title.strip() + self.body = body.strip() + self.author = author.strip() + self.author_email = author_email.strip() + self.parents = parents or [] + + @property + def message(self): + return f"{self.title}\n\n{self.body}".strip() + + def __repr__(self): + return f"{self.title} ({self.rev})" + + +class GitTag(GitObject): + def __init__(self, name, rev, date): + self.rev = rev.strip() + self.name = name.strip() + self._date = date.strip() + + def __repr__(self): + return f"GitTag('{self.name}', '{self.rev}', '{self.date}')" + + @property + def date(self): + return self._date + + @classmethod + def from_line(cls, line: str, inner_delimiter: str) -> GitTag: + name, objectname, date, obj = line.split(inner_delimiter) + if not obj: + obj = objectname + + return cls(name=name, rev=obj, date=date) + + +def tag( + tag: str, annotated: bool = False, signed: bool = False, msg: str | None = None +) -> cmd.Command: + _opt = "" + if annotated: + _opt = f"-a {tag} -m" + if signed: + _opt = f"-s {tag} -m" + + # according to https://git-scm.com/book/en/v2/Git-Basics-Tagging, + # we're not able to create lightweight tag with message. + # by adding message, we make it a annotated tags + c = cmd.run(f'git tag {_opt} "{tag if _opt == "" or msg is None else msg}"') + return c + + +def add(*args: str) -> cmd.Command: + c = cmd.run(f"git add {' '.join(args)}") + return c + + +def commit( + message: str, + args: str = "", + committer_date: str | None = None, +) -> cmd.Command: + f = NamedTemporaryFile("wb", delete=False) + f.write(message.encode("utf-8")) + f.close() + + command = f'git commit {args} -F "{f.name}"' + + if committer_date and os.name == "nt": # pragma: no cover + # Using `cmd /v /c "{command}"` sets environment variables only for that command + command = f'cmd /v /c "set GIT_COMMITTER_DATE={committer_date}&& {command}"' + elif committer_date: + command = f"GIT_COMMITTER_DATE={committer_date} {command}" + + c = cmd.run(command) + os.unlink(f.name) + return c + + +def get_commits( + start: str | None = None, + end: str = "HEAD", + *, + args: str = "", +) -> list[GitCommit]: + """Get the commits between start and end.""" + git_log_entries = _get_log_as_str_list(start, end, args) + git_commits = [] + for rev_and_commit in git_log_entries: + if not rev_and_commit: + continue + rev, parents, title, author, author_email, *body_list = rev_and_commit.split( + "\n" + ) + if rev_and_commit: + git_commit = GitCommit( + rev=rev.strip(), + title=title.strip(), + body="\n".join(body_list).strip(), + author=author, + author_email=author_email, + parents=[p for p in parents.strip().split(" ") if p], + ) + git_commits.append(git_commit) + return git_commits + + +def get_filenames_in_commit(git_reference: str = ""): + """Get the list of files that were committed in the requested git reference. + + :param git_reference: a git reference as accepted by `git show`, default: the last commit + + :returns: file names committed in the last commit by default or inside the passed git reference + """ + c = cmd.run(f"git show --name-only --pretty=format: {git_reference}") + if c.return_code == 0: + return c.out.strip().split("\n") + else: + raise GitCommandError(c.err) + + +def get_tags( + dateformat: str = "%Y-%m-%d", reachable_only: bool = False +) -> list[GitTag]: + inner_delimiter = "---inner_delimiter---" + formatter = ( + f'"%(refname:lstrip=2){inner_delimiter}' + f"%(objectname){inner_delimiter}" + f"%(creatordate:format:{dateformat}){inner_delimiter}" + f'%(object)"' + ) + extra = "--merged" if reachable_only else "" + # Force the default language for parsing + env = {"LC_ALL": "C", "LANG": "C", "LANGUAGE": "C"} + c = cmd.run(f"git tag --format={formatter} --sort=-creatordate {extra}", env=env) + if c.return_code != 0: + if reachable_only and c.err == "fatal: malformed object name HEAD\n": + # this can happen if there are no commits in the repo yet + return [] + raise GitCommandError(c.err) + + if c.err: + out.warn(f"Attempting to proceed after: {c.err}") + + if not c.out: + return [] + + git_tags = [ + GitTag.from_line(line=line, inner_delimiter=inner_delimiter) + for line in c.out.split("\n")[:-1] + ] + + return git_tags + + +def tag_exist(tag: str) -> bool: + c = cmd.run(f"git tag --list {tag}") + return tag in c.out + + +def is_signed_tag(tag: str) -> bool: + return cmd.run(f"git tag -v {tag}").return_code == 0 + + +def get_latest_tag_name() -> str | None: + c = cmd.run("git describe --abbrev=0 --tags") + if c.err: + return None + return c.out.strip() + + +def get_tag_message(tag: str) -> str | None: + c = cmd.run(f"git tag -l --format='%(contents:subject)' {tag}") + if c.err: + return None + return c.out.strip() + + +def get_tag_names() -> list[str | None]: + c = cmd.run("git tag --list") + if c.err: + return [] + return [tag.strip() for tag in c.out.split("\n") if tag.strip()] + + +def find_git_project_root() -> Path | None: + c = cmd.run("git rev-parse --show-toplevel") + if not c.err: + return Path(c.out.strip()) + return None + + +def is_staging_clean() -> bool: + """Check if staging is clean.""" + c = cmd.run("git diff --no-ext-diff --cached --name-only") + return not bool(c.out) + + +def is_git_project() -> bool: + c = cmd.run("git rev-parse --is-inside-work-tree") + if c.out.strip() == "true": + return True + return False + + +def get_eol_style() -> EOLTypes: + c = cmd.run("git config core.eol") + eol = c.out.strip().lower() + + # We enumerate the EOL types of the response of + # `git config core.eol`, and map it to our enumration EOLTypes. + # + # It is just like the variant of the "match" syntax. + map = { + "lf": EOLTypes.LF, + "crlf": EOLTypes.CRLF, + "native": EOLTypes.NATIVE, + } + + # If the response of `git config core.eol` is in the map: + if eol in map: + return map[eol] + else: + # The default value is "native". + # https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreeol + return map["native"] + + +def get_core_editor() -> str | None: + c = cmd.run("git var GIT_EDITOR") + if c.out: + return c.out.strip() + return None + + +def smart_open(*args, **kargs): + """Open a file with the EOL style determined from Git.""" + return open(*args, newline=get_eol_style().get_eol_for_open(), **kargs) + + +def _get_log_as_str_list(start: str | None, end: str, args: str) -> list[str]: + """Get string representation of each log entry""" + delimiter = "----------commit-delimiter----------" + log_format: str = "%H%n%P%n%s%n%an%n%ae%n%b" + git_log_cmd = ( + f"git -c log.showSignature=False log --pretty={log_format}{delimiter} {args}" + ) + if start: + command = f"{git_log_cmd} {start}..{end}" + else: + command = f"{git_log_cmd} {end}" + c = cmd.run(command) + if c.return_code != 0: + raise GitCommandError(c.err) + if not c.out: + return [] + return c.out.split(f"{delimiter}\n") diff --git a/commitizen/hooks.py b/commitizen/hooks.py new file mode 100644 index 0000000..f5505d0 --- /dev/null +++ b/commitizen/hooks.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import os + +from commitizen import cmd, out +from commitizen.exceptions import RunHookError + + +def run(hooks, _env_prefix="CZ_", **env): + if isinstance(hooks, str): + hooks = [hooks] + + for hook in hooks: + out.info(f"Running hook '{hook}'") + + c = cmd.run(hook, env=_format_env(_env_prefix, env)) + + if c.out: + out.write(c.out) + if c.err: + out.error(c.err) + + if c.return_code != 0: + raise RunHookError(f"Running hook '{hook}' failed") + + +def _format_env(prefix: str, env: dict[str, str]) -> dict[str, str]: + """_format_env() prefixes all given environment variables with the given + prefix so it can be passed directly to cmd.run().""" + penv = dict(os.environ) + for name, value in env.items(): + name = prefix + name.upper() + value = str(value) if value is not None else "" + penv[name] = value + + return penv diff --git a/commitizen/out.py b/commitizen/out.py new file mode 100644 index 0000000..40342e9 --- /dev/null +++ b/commitizen/out.py @@ -0,0 +1,42 @@ +import io +import sys + +from termcolor import colored + +if sys.platform == "win32": + if isinstance(sys.stdout, io.TextIOWrapper) and sys.version_info >= (3, 7): + sys.stdout.reconfigure(encoding="utf-8") + + +def write(value: str, *args) -> None: + """Intended to be used when value is multiline.""" + print(value, *args) + + +def line(value: str, *args, **kwargs) -> None: + """Wrapper in case I want to do something different later.""" + print(value, *args, **kwargs) + + +def error(value: str) -> None: + message = colored(value, "red") + line(message, file=sys.stderr) + + +def success(value: str) -> None: + message = colored(value, "green") + line(message) + + +def info(value: str) -> None: + message = colored(value, "blue") + line(message) + + +def diagnostic(value: str): + line(value, file=sys.stderr) + + +def warn(value: str) -> None: + message = colored(value, "magenta") + line(message, file=sys.stderr) diff --git a/commitizen/providers/__init__.py b/commitizen/providers/__init__.py new file mode 100644 index 0000000..9cf4ce5 --- /dev/null +++ b/commitizen/providers/__init__.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import sys +from typing import cast + +if sys.version_info >= (3, 10): + from importlib import metadata +else: + import importlib_metadata as metadata + +from commitizen.config.base_config import BaseConfig +from commitizen.exceptions import VersionProviderUnknown +from commitizen.providers.base_provider import VersionProvider +from commitizen.providers.cargo_provider import CargoProvider +from commitizen.providers.commitizen_provider import CommitizenProvider +from commitizen.providers.composer_provider import ComposerProvider +from commitizen.providers.npm_provider import NpmProvider +from commitizen.providers.pep621_provider import Pep621Provider +from commitizen.providers.poetry_provider import PoetryProvider +from commitizen.providers.scm_provider import ScmProvider +from commitizen.providers.uv_provider import UvProvider + +__all__ = [ + "get_provider", + "CargoProvider", + "CommitizenProvider", + "ComposerProvider", + "NpmProvider", + "Pep621Provider", + "PoetryProvider", + "ScmProvider", + "UvProvider", +] + +PROVIDER_ENTRYPOINT = "commitizen.provider" +DEFAULT_PROVIDER = "commitizen" + + +def get_provider(config: BaseConfig) -> VersionProvider: + """ + Get the version provider as defined in the configuration + + :raises VersionProviderUnknown: if the provider named by `version_provider` is not found. + """ + provider_name = config.settings["version_provider"] or DEFAULT_PROVIDER + try: + (ep,) = metadata.entry_points(name=provider_name, group=PROVIDER_ENTRYPOINT) + except ValueError: + raise VersionProviderUnknown(f'Version Provider "{provider_name}" unknown.') + provider_cls = ep.load() + return cast(VersionProvider, provider_cls(config)) diff --git a/commitizen/providers/base_provider.py b/commitizen/providers/base_provider.py new file mode 100644 index 0000000..3404831 --- /dev/null +++ b/commitizen/providers/base_provider.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import json +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Any, ClassVar + +import tomlkit + +from commitizen.config.base_config import BaseConfig + + +class VersionProvider(ABC): + """ + Abstract base class for version providers. + + Each version provider should inherit and implement this class. + """ + + config: BaseConfig + + def __init__(self, config: BaseConfig): + self.config = config + + @abstractmethod + def get_version(self) -> str: + """ + Get the current version + """ + + @abstractmethod + def set_version(self, version: str): + """ + Set the new current version + """ + + +class FileProvider(VersionProvider): + """ + Base class for file-based version providers + """ + + filename: ClassVar[str] + + @property + def file(self) -> Path: + return Path() / self.filename + + +class JsonProvider(FileProvider): + """ + Base class for JSON-based version providers + """ + + indent: ClassVar[int] = 2 + + def get_version(self) -> str: + document = json.loads(self.file.read_text()) + return self.get(document) + + def set_version(self, version: str): + document = json.loads(self.file.read_text()) + self.set(document, version) + self.file.write_text(json.dumps(document, indent=self.indent) + "\n") + + def get(self, document: dict[str, Any]) -> str: + return document["version"] # type: ignore + + def set(self, document: dict[str, Any], version: str): + document["version"] = version + + +class TomlProvider(FileProvider): + """ + Base class for TOML-based version providers + """ + + def get_version(self) -> str: + document = tomlkit.parse(self.file.read_text()) + return self.get(document) + + def set_version(self, version: str): + document = tomlkit.parse(self.file.read_text()) + self.set(document, version) + self.file.write_text(tomlkit.dumps(document)) + + def get(self, document: tomlkit.TOMLDocument) -> str: + return document["project"]["version"] # type: ignore + + def set(self, document: tomlkit.TOMLDocument, version: str): + document["project"]["version"] = version # type: ignore diff --git a/commitizen/providers/cargo_provider.py b/commitizen/providers/cargo_provider.py new file mode 100644 index 0000000..cee687c --- /dev/null +++ b/commitizen/providers/cargo_provider.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import tomlkit + +from commitizen.providers.base_provider import TomlProvider + + +class CargoProvider(TomlProvider): + """ + Cargo version management + + With support for `workspaces` + """ + + filename = "Cargo.toml" + + def get(self, document: tomlkit.TOMLDocument) -> str: + try: + return document["package"]["version"] # type: ignore + except tomlkit.exceptions.NonExistentKey: + ... + return document["workspace"]["package"]["version"] # type: ignore + + def set(self, document: tomlkit.TOMLDocument, version: str): + try: + document["workspace"]["package"]["version"] = version # type: ignore + return + except tomlkit.exceptions.NonExistentKey: + ... + document["package"]["version"] = version # type: ignore diff --git a/commitizen/providers/commitizen_provider.py b/commitizen/providers/commitizen_provider.py new file mode 100644 index 0000000..a1da25f --- /dev/null +++ b/commitizen/providers/commitizen_provider.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from commitizen.providers.base_provider import VersionProvider + + +class CommitizenProvider(VersionProvider): + """ + Default version provider: Fetch and set version in commitizen config. + """ + + def get_version(self) -> str: + return self.config.settings["version"] # type: ignore + + def set_version(self, version: str): + self.config.set_key("version", version) diff --git a/commitizen/providers/composer_provider.py b/commitizen/providers/composer_provider.py new file mode 100644 index 0000000..495357d --- /dev/null +++ b/commitizen/providers/composer_provider.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from commitizen.providers.base_provider import JsonProvider + + +class ComposerProvider(JsonProvider): + """ + Composer version management + """ + + filename = "composer.json" + indent = 4 diff --git a/commitizen/providers/npm_provider.py b/commitizen/providers/npm_provider.py new file mode 100644 index 0000000..12900ff --- /dev/null +++ b/commitizen/providers/npm_provider.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, ClassVar + +from commitizen.providers.base_provider import VersionProvider + + +class NpmProvider(VersionProvider): + """ + npm package.json and package-lock.json version management + """ + + indent: ClassVar[int] = 2 + package_filename = "package.json" + lock_filename = "package-lock.json" + shrinkwrap_filename = "npm-shrinkwrap.json" + + @property + def package_file(self) -> Path: + return Path() / self.package_filename + + @property + def lock_file(self) -> Path: + return Path() / self.lock_filename + + @property + def shrinkwrap_file(self) -> Path: + return Path() / self.shrinkwrap_filename + + def get_version(self) -> str: + """ + Get the current version from package.json + """ + package_document = json.loads(self.package_file.read_text()) + return self.get_package_version(package_document) + + def set_version(self, version: str) -> None: + package_document = self.set_package_version( + json.loads(self.package_file.read_text()), version + ) + self.package_file.write_text( + json.dumps(package_document, indent=self.indent) + "\n" + ) + if self.lock_file.exists(): + lock_document = self.set_lock_version( + json.loads(self.lock_file.read_text()), version + ) + self.lock_file.write_text( + json.dumps(lock_document, indent=self.indent) + "\n" + ) + if self.shrinkwrap_file.exists(): + shrinkwrap_document = self.set_shrinkwrap_version( + json.loads(self.shrinkwrap_file.read_text()), version + ) + self.shrinkwrap_file.write_text( + json.dumps(shrinkwrap_document, indent=self.indent) + "\n" + ) + + def get_package_version(self, document: dict[str, Any]) -> str: + return document["version"] # type: ignore + + def set_package_version( + self, document: dict[str, Any], version: str + ) -> dict[str, Any]: + document["version"] = version + return document + + def set_lock_version( + self, document: dict[str, Any], version: str + ) -> dict[str, Any]: + document["version"] = version + document["packages"][""]["version"] = version + return document + + def set_shrinkwrap_version( + self, document: dict[str, Any], version: str + ) -> dict[str, Any]: + document["version"] = version + document["packages"][""]["version"] = version + return document diff --git a/commitizen/providers/pep621_provider.py b/commitizen/providers/pep621_provider.py new file mode 100644 index 0000000..0b8b5c4 --- /dev/null +++ b/commitizen/providers/pep621_provider.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from commitizen.providers.base_provider import TomlProvider + + +class Pep621Provider(TomlProvider): + """ + PEP-621 version management + """ + + filename = "pyproject.toml" diff --git a/commitizen/providers/poetry_provider.py b/commitizen/providers/poetry_provider.py new file mode 100644 index 0000000..7aa28f5 --- /dev/null +++ b/commitizen/providers/poetry_provider.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +import tomlkit + +from commitizen.providers.base_provider import TomlProvider + + +class PoetryProvider(TomlProvider): + """ + Poetry version management + """ + + filename = "pyproject.toml" + + def get(self, pyproject: tomlkit.TOMLDocument) -> str: + return pyproject["tool"]["poetry"]["version"] # type: ignore + + def set(self, pyproject: tomlkit.TOMLDocument, version: str): + pyproject["tool"]["poetry"]["version"] = version # type: ignore diff --git a/commitizen/providers/scm_provider.py b/commitizen/providers/scm_provider.py new file mode 100644 index 0000000..cb57514 --- /dev/null +++ b/commitizen/providers/scm_provider.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from commitizen.git import get_tags +from commitizen.providers.base_provider import VersionProvider +from commitizen.tags import TagRules + + +class ScmProvider(VersionProvider): + """ + A provider fetching the current/last version from the repository history + + The version is fetched using `git describe` and is never set. + + It is meant for `setuptools-scm` or any package manager `*-scm` provider. + """ + + def get_version(self) -> str: + rules = TagRules.from_settings(self.config.settings) + tags = get_tags(reachable_only=True) + version_tags = rules.get_version_tags(tags) + versions = sorted(rules.extract_version(t) for t in version_tags) + if not versions: + return "0.0.0" + return str(versions[-1]) + + def set_version(self, version: str): + # Not necessary + pass diff --git a/commitizen/providers/uv_provider.py b/commitizen/providers/uv_provider.py new file mode 100644 index 0000000..36c8a49 --- /dev/null +++ b/commitizen/providers/uv_provider.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING + +import tomlkit + +from commitizen.providers.base_provider import TomlProvider + +if TYPE_CHECKING: + import tomlkit.items + + +class UvProvider(TomlProvider): + """ + uv.lock and pyproject.tom version management + """ + + filename = "pyproject.toml" + lock_filename = "uv.lock" + + @property + def lock_file(self) -> Path: + return Path() / self.lock_filename + + def set_version(self, version: str) -> None: + super().set_version(version) + self.set_lock_version(version) + + def set_lock_version(self, version: str) -> None: + pyproject_toml_content = tomlkit.parse(self.file.read_text()) + project_name = pyproject_toml_content["project"]["name"] # type: ignore[index] + + document = tomlkit.parse(self.lock_file.read_text()) + + packages: tomlkit.items.AoT = document["package"] # type: ignore[assignment] + for i, package in enumerate(packages): + if package["name"] == project_name: + document["package"][i]["version"] = version # type: ignore[index] + break + self.lock_file.write_text(tomlkit.dumps(document)) diff --git a/commitizen/py.typed b/commitizen/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/commitizen/tags.py b/commitizen/tags.py new file mode 100644 index 0000000..5724bb2 --- /dev/null +++ b/commitizen/tags.py @@ -0,0 +1,267 @@ +from __future__ import annotations + +import re +import warnings +from collections.abc import Sequence +from dataclasses import dataclass, field +from functools import cached_property +from string import Template +from typing import TYPE_CHECKING, NamedTuple + +from commitizen import out +from commitizen.defaults import DEFAULT_SETTINGS, Settings, get_tag_regexes +from commitizen.git import GitTag +from commitizen.version_schemes import ( + DEFAULT_SCHEME, + InvalidVersion, + Version, + VersionScheme, + get_version_scheme, +) + +if TYPE_CHECKING: + import sys + + from commitizen.version_schemes import VersionScheme + + # Self is Python 3.11+ but backported in typing-extensions + if sys.version_info < (3, 11): + from typing_extensions import Self + else: + from typing import Self + + +class VersionTag(NamedTuple): + """Represent a version and its matching tag form.""" + + version: str + tag: str + + +@dataclass +class TagRules: + """ + Encapsulate tag-related rules. + + It allows to filter or match tags according to rules provided in settings: + - `tag_format`: the current format of the tags generated on `bump` + - `legacy_tag_formats`: previous known formats of the tag + - `ignored_tag_formats`: known formats that should be ignored + - `merge_prereleases`: if `True`, prereleases will be merged with their release counterpart + - `version_scheme`: the version scheme to use, which will be used to parse and format versions + + This class is meant to abstract and centralize all the logic related to tags. + To ensure consistency, it is recommended to use this class to handle tags. + + Example: + + ```python + settings = DEFAULT_SETTINGS.clone() + settings.update( + { + "tag_format": "v{version}", + "legacy_tag_formats": ["version{version}", "ver{version}"], + "ignored_tag_formats": ["ignored{version}"], + } + ) + + rules = TagRules.from_settings(settings) + + assert rules.is_version_tag("v1.0.0") + assert rules.is_version_tag("version1.0.0") + assert rules.is_version_tag("ver1.0.0") + assert not rules.is_version_tag("ignored1.0.0", warn=True) # Does not warn + assert not rules.is_version_tag("warn1.0.0", warn=True) # Does warn + + assert rules.search_version("# My v1.0.0 version").version == "1.0.0" + assert rules.extract_version("v1.0.0") == Version("1.0.0") + try: + assert rules.extract_version("not-a-v1.0.0") + except InvalidVersion: + print("Does not match a tag format") + ``` + """ + + scheme: VersionScheme = DEFAULT_SCHEME + tag_format: str = DEFAULT_SETTINGS["tag_format"] + legacy_tag_formats: Sequence[str] = field(default_factory=list) + ignored_tag_formats: Sequence[str] = field(default_factory=list) + merge_prereleases: bool = False + + @cached_property + def version_regexes(self) -> Sequence[re.Pattern]: + """Regexes for all legit tag formats, current and legacy""" + tag_formats = [self.tag_format, *self.legacy_tag_formats] + regexes = (self._format_regex(p) for p in tag_formats) + return [re.compile(r) for r in regexes] + + @cached_property + def ignored_regexes(self) -> Sequence[re.Pattern]: + """Regexes for known but ignored tag formats""" + regexes = (self._format_regex(p, star=True) for p in self.ignored_tag_formats) + return [re.compile(r) for r in regexes] + + def _format_regex(self, tag_pattern: str, star: bool = False) -> str: + """ + Format a tag pattern into a regex pattern. + + If star is `True`, the `*` character will be considered as a wildcard. + """ + tag_regexes = get_tag_regexes(self.scheme.parser.pattern) + format_regex = tag_pattern.replace("*", "(?:.*?)") if star else tag_pattern + for pattern, regex in tag_regexes.items(): + format_regex = format_regex.replace(pattern, regex) + return format_regex + + def is_version_tag(self, tag: str | GitTag, warn: bool = False) -> bool: + """ + True if a given tag is a legit version tag. + + if `warn` is `True`, it will print a warning message if the tag is not a version tag. + """ + tag = tag.name if isinstance(tag, GitTag) else tag + is_legit = any(regex.match(tag) for regex in self.version_regexes) + if warn and not is_legit and not self.is_ignored_tag(tag): + out.warn(f"InvalidVersion {tag} doesn't match any configured tag format") + return is_legit + + def is_ignored_tag(self, tag: str | GitTag) -> bool: + """True if a given tag can be ignored""" + tag = tag.name if isinstance(tag, GitTag) else tag + return any(regex.match(tag) for regex in self.ignored_regexes) + + def get_version_tags( + self, tags: Sequence[GitTag], warn: bool = False + ) -> Sequence[GitTag]: + """Filter in version tags and warn on unexpected tags""" + return [tag for tag in tags if self.is_version_tag(tag, warn)] + + def extract_version(self, tag: GitTag) -> Version: + """ + Extract a version from the tag as defined in tag formats. + + Raises `InvalidVersion` if the tag does not match any format. + """ + candidates = ( + m for regex in self.version_regexes if (m := regex.fullmatch(tag.name)) + ) + if not (m := next(candidates, None)): + raise InvalidVersion( + f"Invalid version tag: '{tag.name}' does not match any configured tag format" + ) + if "version" in m.groupdict(): + return self.scheme(m.group("version")) + + parts = m.groupdict() + version = parts["major"] + + if minor := parts.get("minor"): + version = f"{version}.{minor}" + if patch := parts.get("patch"): + version = f"{version}.{patch}" + + if parts.get("prerelease"): + version = f"{version}-{parts['prerelease']}" + if parts.get("devrelease"): + version = f"{version}{parts['devrelease']}" + return self.scheme(version) + + def include_in_changelog(self, tag: GitTag) -> bool: + """Check if a tag should be included in the changelog""" + try: + version = self.extract_version(tag) + except InvalidVersion: + return False + + if self.merge_prereleases and version.is_prerelease: + return False + + return True + + def search_version(self, text: str, last: bool = False) -> VersionTag | None: + """ + Search the first or last version tag occurrence in text. + + It searches for complete versions only (aka `major`, `minor` and `patch`) + """ + candidates = ( + m for regex in self.version_regexes if len(m := list(regex.finditer(text))) + ) + if not (matches := next(candidates, [])): + return None + + match = matches[-1 if last else 0] + + if "version" in match.groupdict(): + return VersionTag(match.group("version"), match.group(0)) + + parts = match.groupdict() + try: + version = f"{parts['major']}.{parts['minor']}.{parts['patch']}" + except KeyError: + return None + + if parts.get("prerelease"): + version = f"{version}-{parts['prerelease']}" + if parts.get("devrelease"): + version = f"{version}{parts['devrelease']}" + return VersionTag(version, match.group(0)) + + def normalize_tag( + self, version: Version | str, tag_format: str | None = None + ) -> str: + """ + The tag and the software version might be different. + + That's why this function exists. + + Example: + | tag | version (PEP 0440) | + | --- | ------- | + | v0.9.0 | 0.9.0 | + | ver1.0.0 | 1.0.0 | + | ver1.0.0.a0 | 1.0.0a0 | + """ + version = self.scheme(version) if isinstance(version, str) else version + tag_format = tag_format or self.tag_format + + major, minor, patch = version.release + prerelease = version.prerelease or "" + + t = Template(tag_format) + return t.safe_substitute( + version=version, + major=major, + minor=minor, + patch=patch, + prerelease=prerelease, + ) + + def find_tag_for( + self, tags: Sequence[GitTag], version: Version | str + ) -> GitTag | None: + """Find the first matching tag for a given version.""" + version = self.scheme(version) if isinstance(version, str) else version + possible_tags = [ + self.normalize_tag(version, f) + for f in (self.tag_format, *self.legacy_tag_formats) + ] + candidates = [t for t in tags if any(t.name == p for p in possible_tags)] + if len(candidates) > 1: + warnings.warn( + UserWarning( + f"Multiple tags found for version {version}: {', '.join(t.name for t in candidates)}" + ) + ) + return next(iter(candidates), None) + + @classmethod + def from_settings(cls, settings: Settings) -> Self: + """Extract tag rules from settings""" + return cls( + scheme=get_version_scheme(settings), + tag_format=settings["tag_format"], + legacy_tag_formats=settings["legacy_tag_formats"], + ignored_tag_formats=settings["ignored_tag_formats"], + merge_prereleases=settings["changelog_merge_prerelease"], + ) diff --git a/commitizen/templates/CHANGELOG.adoc.j2 b/commitizen/templates/CHANGELOG.adoc.j2 new file mode 100644 index 0000000..fe16c5d --- /dev/null +++ b/commitizen/templates/CHANGELOG.adoc.j2 @@ -0,0 +1,19 @@ +{% for entry in tree %} + +== {{ entry.version }}{% if entry.date %} ({{ entry.date }}){% endif %} + +{% for change_key, changes in entry.changes.items() %} + +{% if change_key %} +=== {{ change_key }} +{% endif %} + +{% for change in changes %} +{% if change.scope %} +* *{{ change.scope }}*: {{ change.message }} +{% elif change.message %} +* {{ change.message }} +{% endif %} +{% endfor %} +{% endfor %} +{% endfor %} diff --git a/commitizen/templates/CHANGELOG.md.j2 b/commitizen/templates/CHANGELOG.md.j2 new file mode 100644 index 0000000..de63880 --- /dev/null +++ b/commitizen/templates/CHANGELOG.md.j2 @@ -0,0 +1,19 @@ +{% for entry in tree %} + +## {{ entry.version }}{% if entry.date %} ({{ entry.date }}){% endif %} + +{% for change_key, changes in entry.changes.items() %} + +{% if change_key %} +### {{ change_key }} +{% endif %} + +{% for change in changes %} +{% if change.scope %} +- **{{ change.scope }}**: {{ change.message }} +{% elif change.message %} +- {{ change.message }} +{% endif %} +{% endfor %} +{% endfor %} +{% endfor %} diff --git a/commitizen/templates/CHANGELOG.rst.j2 b/commitizen/templates/CHANGELOG.rst.j2 new file mode 100644 index 0000000..4287108 --- /dev/null +++ b/commitizen/templates/CHANGELOG.rst.j2 @@ -0,0 +1,23 @@ +{% for entry in tree %} + +{% set entry_title -%} +{{ entry.version }}{% if entry.date %} ({{ entry.date }}){% endif -%} +{%- endset %} +{{ entry_title }} +{{ "=" * entry_title|length }} +{% for change_key, changes in entry.changes.items() %} + +{% if change_key -%} +{{ change_key }} +{{ "-" * change_key|length }} +{% endif %} + +{% for change in changes %} +{% if change.scope %} +- **{{ change.scope }}**: {{ change.message }} +{% elif change.message %} +- {{ change.message }} +{% endif %} +{% endfor %} +{% endfor %} +{% endfor %} diff --git a/commitizen/templates/CHANGELOG.textile.j2 b/commitizen/templates/CHANGELOG.textile.j2 new file mode 100644 index 0000000..db55f4c --- /dev/null +++ b/commitizen/templates/CHANGELOG.textile.j2 @@ -0,0 +1,19 @@ +{% for entry in tree %} + +h2. {{ entry.version }}{% if entry.date %} ({{ entry.date }}){% endif %} + +{% for change_key, changes in entry.changes.items() %} + +{% if change_key %} +h3. {{ change_key }} +{% endif %} + +{% for change in changes %} +{% if change.scope %} +- *{{ change.scope }}*: {{ change.message }} +{% elif change.message %} +- {{ change.message }} +{% endif %} +{% endfor %} +{% endfor %} +{% endfor %} diff --git a/commitizen/version_schemes.py b/commitizen/version_schemes.py new file mode 100644 index 0000000..2486be5 --- /dev/null +++ b/commitizen/version_schemes.py @@ -0,0 +1,439 @@ +from __future__ import annotations + +import re +import sys +import warnings +from itertools import zip_longest +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Literal, + Protocol, + cast, + runtime_checkable, +) + +if sys.version_info >= (3, 10): + from importlib import metadata +else: + import importlib_metadata as metadata + +from packaging.version import InvalidVersion # noqa: F401: Rexpose the common exception +from packaging.version import Version as _BaseVersion + +from commitizen.defaults import MAJOR, MINOR, PATCH, Settings +from commitizen.exceptions import VersionSchemeUnknown + +if TYPE_CHECKING: + # TypeAlias is Python 3.10+ but backported in typing-extensions + if sys.version_info >= (3, 10): + from typing import TypeAlias + else: + from typing_extensions import TypeAlias + + # Self is Python 3.11+ but backported in typing-extensions + if sys.version_info < (3, 11): + from typing_extensions import Self + else: + from typing import Self + + +Increment: TypeAlias = Literal["MAJOR", "MINOR", "PATCH"] +Prerelease: TypeAlias = Literal["alpha", "beta", "rc"] +DEFAULT_VERSION_PARSER = r"v?(?P<version>([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.]+)?(\w+)?)" + + +@runtime_checkable +class VersionProtocol(Protocol): + parser: ClassVar[re.Pattern] + """Regex capturing this version scheme into a `version` group""" + + def __init__(self, version: str): + """ + Initialize a version object from its string representation. + + :raises InvalidVersion: If the ``version`` does not conform to the scheme in any way. + """ + raise NotImplementedError("must be implemented") + + def __str__(self) -> str: + """A string representation of the version that can be rounded-tripped.""" + raise NotImplementedError("must be implemented") + + @property + def scheme(self) -> VersionScheme: + """The version scheme this version follows.""" + raise NotImplementedError("must be implemented") + + @property + def release(self) -> tuple[int, ...]: + """The components of the "release" segment of the version.""" + raise NotImplementedError("must be implemented") + + @property + def is_prerelease(self) -> bool: + """Whether this version is a pre-release.""" + raise NotImplementedError("must be implemented") + + @property + def prerelease(self) -> str | None: + """The prelease potion of the version is this is a prerelease.""" + raise NotImplementedError("must be implemented") + + @property + def public(self) -> str: + """The public portion of the version.""" + raise NotImplementedError("must be implemented") + + @property + def local(self) -> str | None: + """The local version segment of the version.""" + raise NotImplementedError("must be implemented") + + @property + def major(self) -> int: + """The first item of :attr:`release` or ``0`` if unavailable.""" + raise NotImplementedError("must be implemented") + + @property + def minor(self) -> int: + """The second item of :attr:`release` or ``0`` if unavailable.""" + raise NotImplementedError("must be implemented") + + @property + def micro(self) -> int: + """The third item of :attr:`release` or ``0`` if unavailable.""" + raise NotImplementedError("must be implemented") + + def __lt__(self, other: Any) -> bool: + raise NotImplementedError("must be implemented") + + def __le__(self, other: Any) -> bool: + raise NotImplementedError("must be implemented") + + def __eq__(self, other: object) -> bool: + raise NotImplementedError("must be implemented") + + def __ge__(self, other: Any) -> bool: + raise NotImplementedError("must be implemented") + + def __gt__(self, other: Any) -> bool: + raise NotImplementedError("must be implemented") + + def __ne__(self, other: object) -> bool: + raise NotImplementedError("must be implemented") + + def bump( + self, + increment: Increment | None, + prerelease: Prerelease | None = None, + prerelease_offset: int = 0, + devrelease: int | None = None, + is_local_version: bool = False, + build_metadata: str | None = None, + exact_increment: bool = False, + ) -> Self: + """ + Based on the given increment, generate the next bumped version according to the version scheme + + Args: + increment: The component to increase + prerelease: The type of prerelease, if Any + is_local_version: Whether to increment the local version instead + exact_increment: Treat the increment and prerelease arguments explicitly. Disables logic + that attempts to deduce the correct increment when a prelease suffix is present. + """ + + +# With PEP 440 and SemVer semantic, Scheme is the type, Version is an instance +Version: TypeAlias = VersionProtocol +VersionScheme: TypeAlias = type[VersionProtocol] + + +class BaseVersion(_BaseVersion): + """ + A base class implementing the `VersionProtocol` for PEP440-like versions. + """ + + parser: ClassVar[re.Pattern] = re.compile(DEFAULT_VERSION_PARSER) + """Regex capturing this version scheme into a `version` group""" + + @property + def scheme(self) -> VersionScheme: + return self.__class__ + + @property + def prerelease(self) -> str | None: + # version.pre is needed for mypy check + if self.is_prerelease and self.pre: + return f"{self.pre[0]}{self.pre[1]}" + return None + + def generate_prerelease( + self, prerelease: str | None = None, offset: int = 0 + ) -> str: + """Generate prerelease + + X.YaN # Alpha release + X.YbN # Beta release + X.YrcN # Release Candidate + X.Y # Final + + This function might return something like 'alpha1' + but it will be handled by Version. + """ + if not prerelease: + return "" + + # prevent down-bumping the pre-release phase, e.g. from 'b1' to 'a2' + # https://packaging.python.org/en/latest/specifications/version-specifiers/#pre-releases + # https://semver.org/#spec-item-11 + if self.is_prerelease and self.pre: + prerelease = max(prerelease, self.pre[0]) + + # version.pre is needed for mypy check + if self.is_prerelease and self.pre and prerelease.startswith(self.pre[0]): + prev_prerelease: int = self.pre[1] + new_prerelease_number = prev_prerelease + 1 + else: + new_prerelease_number = offset + pre_version = f"{prerelease}{new_prerelease_number}" + return pre_version + + def generate_devrelease(self, devrelease: int | None) -> str: + """Generate devrelease + + The devrelease version should be passed directly and is not + inferred based on the previous version. + """ + if devrelease is None: + return "" + + return f"dev{devrelease}" + + def generate_build_metadata(self, build_metadata: str | None) -> str: + """Generate build-metadata + + Build-metadata (local version) is not used in version calculations + but added after + statically. + """ + if build_metadata is None: + return "" + + return f"+{build_metadata}" + + def increment_base(self, increment: Increment | None = None) -> str: + prev_release = list(self.release) + increments = [MAJOR, MINOR, PATCH] + base = dict(zip_longest(increments, prev_release, fillvalue=0)) + + if increment == MAJOR: + base[MAJOR] += 1 + base[MINOR] = 0 + base[PATCH] = 0 + elif increment == MINOR: + base[MINOR] += 1 + base[PATCH] = 0 + elif increment == PATCH: + base[PATCH] += 1 + + return f"{base[MAJOR]}.{base[MINOR]}.{base[PATCH]}" + + def bump( + self, + increment: Increment | None, + prerelease: Prerelease | None = None, + prerelease_offset: int = 0, + devrelease: int | None = None, + is_local_version: bool = False, + build_metadata: str | None = None, + exact_increment: bool = False, + ) -> Self: + """Based on the given increment a proper semver will be generated. + + For now the rules and versioning scheme is based on + python's PEP 0440. + More info: https://www.python.org/dev/peps/pep-0440/ + + Example: + PATCH 1.0.0 -> 1.0.1 + MINOR 1.0.0 -> 1.1.0 + MAJOR 1.0.0 -> 2.0.0 + """ + + if self.local and is_local_version: + local_version = self.scheme(self.local).bump(increment) + return self.scheme(f"{self.public}+{local_version}") # type: ignore + else: + if not self.is_prerelease: + base = self.increment_base(increment) + elif exact_increment: + base = self.increment_base(increment) + else: + base = f"{self.major}.{self.minor}.{self.micro}" + if increment == PATCH: + pass + elif increment == MINOR: + if self.micro != 0: + base = self.increment_base(increment) + elif increment == MAJOR: + if self.minor != 0 or self.micro != 0: + base = self.increment_base(increment) + dev_version = self.generate_devrelease(devrelease) + + release = list(self.release) + if len(release) < 3: + release += [0] * (3 - len(release)) + current_base = ".".join(str(part) for part in release) + if base == current_base: + pre_version = self.generate_prerelease( + prerelease, offset=prerelease_offset + ) + else: + base_version = cast(BaseVersion, self.scheme(base)) + pre_version = base_version.generate_prerelease( + prerelease, offset=prerelease_offset + ) + build_metadata = self.generate_build_metadata(build_metadata) + # TODO: post version + return self.scheme(f"{base}{pre_version}{dev_version}{build_metadata}") # type: ignore + + +class Pep440(BaseVersion): + """ + PEP 440 Version Scheme + + See: https://peps.python.org/pep-0440/ + """ + + +class SemVer(BaseVersion): + """ + Semantic Versioning (SemVer) scheme + + See: https://semver.org/spec/v1.0.0.html + """ + + def __str__(self) -> str: + parts = [] + + # Epoch + if self.epoch != 0: + parts.append(f"{self.epoch}!") + + # Release segment + parts.append(".".join(str(x) for x in self.release)) + + # Pre-release + if self.prerelease: + parts.append(f"-{self.prerelease}") + + # Post-release + if self.post is not None: + parts.append(f"-post{self.post}") + + # Development release + if self.dev is not None: + parts.append(f"-dev{self.dev}") + + # Local version segment + if self.local: + parts.append(f"+{self.local}") + + return "".join(parts) + + +class SemVer2(SemVer): + """ + Semantic Versioning 2.0 (SemVer2) schema + + See: https://semver.org/spec/v2.0.0.html + """ + + _STD_PRELEASES = { + "a": "alpha", + "b": "beta", + } + + @property + def prerelease(self) -> str | None: + if self.is_prerelease and self.pre: + prerelease_type = self._STD_PRELEASES.get(self.pre[0], self.pre[0]) + return f"{prerelease_type}.{self.pre[1]}" + return None + + def __str__(self) -> str: + parts = [] + + # Epoch + if self.epoch != 0: + parts.append(f"{self.epoch}!") + + # Release segment + parts.append(".".join(str(x) for x in self.release)) + + # Pre-release identifiers + # See: https://semver.org/spec/v2.0.0.html#spec-item-9 + prerelease_parts = [] + if self.prerelease: + prerelease_parts.append(f"{self.prerelease}") + + # Post-release + if self.post is not None: + prerelease_parts.append(f"post.{self.post}") + + # Development release + if self.dev is not None: + prerelease_parts.append(f"dev.{self.dev}") + + if prerelease_parts: + parts.append("-") + parts.append(".".join(prerelease_parts)) + + # Local version segment + if self.local: + parts.append(f"+{self.local}") + + return "".join(parts) + + +DEFAULT_SCHEME: VersionScheme = Pep440 + +SCHEMES_ENTRYPOINT = "commitizen.scheme" +"""Schemes entrypoints group""" + +KNOWN_SCHEMES = [ep.name for ep in metadata.entry_points(group=SCHEMES_ENTRYPOINT)] +"""All known registered version schemes""" + + +def get_version_scheme(settings: Settings, name: str | None = None) -> VersionScheme: + """ + Get the version scheme as defined in the configuration + or from an overridden `name` + + :raises VersionSchemeUnknown: if the version scheme is not found. + """ + # TODO: Remove the deprecated `version_type` handling + deprecated_setting: str | None = settings.get("version_type") + if deprecated_setting: + warnings.warn( + DeprecationWarning( + "`version_type` setting is deprecated and will be removed in commitizen 4. " + "Please use `version_scheme` instead" + ) + ) + name = name or settings.get("version_scheme") or deprecated_setting + if not name: + return DEFAULT_SCHEME + + try: + (ep,) = metadata.entry_points(name=name, group=SCHEMES_ENTRYPOINT) + except ValueError: + raise VersionSchemeUnknown(f'Version scheme "{name}" unknown.') + scheme = cast(VersionScheme, ep.load()) + + if not isinstance(scheme, VersionProtocol): + warnings.warn(f"Version scheme {name} does not implement the VersionProtocol") + + return scheme diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..128602d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,183 @@ +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/commitizen-tools/commitizen/pythonpackage.yml?label=python%20package&logo=github&logoColor=white&style=flat-square)](https://github.com/commitizen-tools/commitizen/actions) +[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=flat-square)](https://conventionalcommits.org) +[![PyPI Package latest release](https://img.shields.io/pypi/v/commitizen.svg?style=flat-square)](https://pypi.org/project/commitizen/) +[![PyPI Package download count (per month)](https://img.shields.io/pypi/dm/commitizen?style=flat-square)](https://pypi.org/project/commitizen/) +[![Supported versions](https://img.shields.io/pypi/pyversions/commitizen.svg?style=flat-square)](https://pypi.org/project/commitizen/) +[![Conda Version](https://img.shields.io/conda/vn/conda-forge/commitizen?style=flat-square)](https://anaconda.org/conda-forge/commitizen) +[![homebrew](https://img.shields.io/homebrew/v/commitizen?color=teal&style=flat-square)](https://formulae.brew.sh/formula/commitizen) +[![Codecov](https://img.shields.io/codecov/c/github/commitizen-tools/commitizen.svg?style=flat-square)](https://codecov.io/gh/commitizen-tools/commitizen) +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?style=flat-square&logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) + +![Using commitizen cli](images/demo.gif) + +--- + +**Documentation:** [https://commitizen-tools.github.io/commitizen/](https://commitizen-tools.github.io/commitizen/) + +--- + +## About + +Commitizen is release management tool designed for teams. + +Commitizen assumes your team uses a standard way of committing rules +and from that foundation, it can bump your project's version, create +the changelog, and update files. + +By default, commitizen uses [conventional commits][conventional_commits], but you +can build your own set of rules, and publish them. + +Using a standardized set of rules to write commits, makes commits easier to read, and enforces writing +descriptive commits. + +### Features + +- Command-line utility to create commits with your rules. Defaults: [Conventional commits][conventional_commits] +- Bump version automatically using [semantic versioning][semver] based on the commits. [Read More](./commands/bump.md) +- Generate a changelog using [Keep a changelog][keepchangelog] +- Update your project's version files automatically +- Display information about your commit rules (commands: schema, example, info) +- Create your own set of rules and publish them to pip. Read more on [Customization](./customization.md) + +## Requirements + +[Python](https://www.python.org/downloads/) `3.9+` + +[Git][gitscm] `1.8.5.2+` + +## Installation + +Install commitizen in your system using `pipx` (Recommended, <https://pypa.github.io/pipx/installation/>): + +```bash +pipx ensurepath +pipx install commitizen +pipx upgrade commitizen +``` + +Install commitizen using `pip` with `--user` flag: + +```bash +pip install --user -U commitizen +``` + +### Python project + +You can add it to your local project using one of the following. + +With `pip`: + +```bash +pip install -U commitizen +``` + +With `conda`: + +```bash +conda install -c conda-forge commitizen +``` + +With Poetry >= 1.2.0: + +```bash +poetry add commitizen --group dev +``` + +With Poetry < 1.2.0: + +```bash +poetry add commitizen --dev +``` + +### macOS + +via [homebrew](https://formulae.brew.sh/formula/commitizen): + +```bash +brew install commitizen +``` + +## Usage + +Most of the time this is the only command you'll run: + +```sh +cz bump +``` + +On top of that, you can use commitizen to assist you with the creation of commits: + +```sh +cz commit +``` + +Read more in the section [Getting Started](./getting_started.md). + +### Help + +```sh +$ cz --help +usage: cz [-h] [--debug] [-n NAME] [-nr NO_RAISE] {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} ... + +Commitizen is a cli tool to generate conventional commits. +For more information about the topic go to https://conventionalcommits.org/ + +optional arguments: + -h, --help show this help message and exit + --config the path of configuration file + --debug use debug mode + -n NAME, --name NAME use the given commitizen (default: cz_conventional_commits) + -nr NO_RAISE, --no-raise NO_RAISE + comma separated error codes that won't rise error, e.g: cz -nr 1,2,3 bump. See codes at https://commitizen- + tools.github.io/commitizen/exit_codes/ + +commands: + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} + init init commitizen configuration + commit (c) create new commit + ls show available commitizens + example show commit example + info show information about the cz + schema show commit schema + bump bump semantic version based on the git log + changelog (ch) generate changelog (note that it will overwrite existing file) + check validates that a commit message matches the commitizen schema + version get the version of the installed commitizen or the current project (default: installed commitizen) +``` + +## Setting up bash completion + +When using bash as your shell (limited support for zsh, fish, and tcsh is available), Commitizen can use [argcomplete](https://kislyuk.github.io/argcomplete/) for auto-completion. For this argcomplete needs to be enabled. + +argcomplete is installed when you install Commitizen since it's a dependency. + +If Commitizen is installed globally, global activation can be executed: + +```bash +sudo activate-global-python-argcomplete +``` + +For permanent (but not global) Commitizen activation, use: + +```bash +register-python-argcomplete cz >> ~/.bashrc +``` + +For one-time activation of argcomplete for Commitizen only, use: + +```bash +eval "$(register-python-argcomplete cz)" +``` + +For further information on activation, please visit the [argcomplete website](https://kislyuk.github.io/argcomplete/). + +## Sponsors + +These are our cool sponsors! + +<!-- sponsors --><!-- sponsors --> + +[conventional_commits]: https://www.conventionalcommits.org +[semver]: https://semver.org/ +[keepchangelog]: https://keepachangelog.com/ +[gitscm]: https://git-scm.com/downloads diff --git a/docs/commands/bump.md b/docs/commands/bump.md new file mode 100644 index 0000000..efdba76 --- /dev/null +++ b/docs/commands/bump.md @@ -0,0 +1,636 @@ +![Bump version](../images/bump.gif) + +## About + +`cz bump` **automatically** increases the version, based on the commits. + +The commits should follow the rules established by the committer in order to be parsed correctly. + +**prerelease** versions are supported (alpha, beta, release candidate). + +The version can also be **manually** bumped. + +The version format follows [PEP 0440][pep440] and [semantic versioning][semver]. + +This means `MAJOR.MINOR.PATCH` + +| Increment | Description | Conventional commit map | +| --------- | --------------------------- | ----------------------- | +| `MAJOR` | Breaking changes introduced | `BREAKING CHANGE` | +| `MINOR` | New features | `feat` | +| `PATCH` | Fixes | `fix` + everything else | + +[PEP 0440][pep440] is the default, you can switch by using the setting `version_scheme` or the cli: + +```sh +cz bump --version-scheme semver +``` + +Some examples of pep440: + +```bash +0.9.0 +0.9.1 +0.9.2 +0.9.10 +0.9.11 +1.0.0a0 # alpha +1.0.0a1 +1.0.0b0 # beta +1.0.0rc0 # release candidate +1.0.0rc1 +1.0.0 +1.0.1 +1.1.0 +2.0.0 +2.0.1a +``` + +`post` releases are not supported yet. + +## Usage + +![cz bump --help](../images/cli_help/cz_bump___help.svg) + +### `--files-only` + +Bumps the version in the files defined in `version_files` without creating a commit and tag on the git repository, + +```bash +cz bump --files-only +``` + +### `--changelog` + +Generate a **changelog** along with the new version and tag when bumping. + +```bash +cz bump --changelog +``` + +### `--prerelease` + +The bump is a pre-release bump, meaning that in addition to a possible version bump the new version receives a +pre-release segment compatible with the bumpโ€™s version scheme, where the segment consist of a _phase_ and a +non-negative number. Supported options for `--prerelease` are the following phase names `alpha`, `beta`, or +`rc` (release candidate). For more details, refer to the +[Python Packaging User Guide](https://packaging.python.org/en/latest/specifications/version-specifiers/#pre-releases). + +Note that as per [semantic versioning spec](https://semver.org/#spec-item-9) + +> Pre-release versions have a lower precedence than the associated normal version. A pre-release version +> indicates that the version is unstable and might not satisfy the intended compatibility requirements +> as denoted by its associated normal version. + +For example, the following versions (using the [PEP 440](https://peps.python.org/pep-0440/) scheme) are ordered +by their precedence and showcase how a release might flow through a development cycle: + +- `1.0.0` is the current published version +- `1.0.1a0` after committing a `fix:` for pre-release +- `1.1.0a1` after committing an additional `feat:` for pre-release +- `1.1.0b0` after bumping a beta release +- `1.1.0rc0` after bumping the release candidate +- `1.1.0` next feature release + +### `--increment-mode` + +By default, `--increment-mode` is set to `linear`, which ensures that bumping pre-releases _maintains linearity_: +bumping of a pre-release with lower precedence than the current pre-release phase maintains the current phase of +higher precedence. For example, if the current version is `1.0.0b1` then bumping with `--prerelease alpha` will +continue to bump the โ€œbetaโ€ phase. + +Setting `--increment-mode` to `exact` instructs `cz bump` to instead apply the +exact changes that have been specified with `--increment` or determined from the commit log. For example, +`--prerelease beta` will always result in a `b` tag, and `--increment PATCH` will always increase the patch component. + +Below are some examples that illustrate the difference in behavior: + +| Increment | Pre-release | Start Version | `--increment-mode=linear` | `--increment-mode=exact` | +|-----------|-------------|---------------|---------------------------|--------------------------| +| `MAJOR` | | `2.0.0b0` | `2.0.0` | `3.0.0` | +| `MINOR` | | `2.0.0b0` | `2.0.0` | `2.1.0` | +| `PATCH` | | `2.0.0b0` | `2.0.0` | `2.0.1` | +| `MAJOR` | `alpha` | `2.0.0b0` | `3.0.0a0` | `3.0.0a0` | +| `MINOR` | `alpha` | `2.0.0b0` | `2.0.0b1` | `2.1.0a0` | +| `PATCH` | `alpha` | `2.0.0b0` | `2.0.0b1` | `2.0.1a0` | + +### `--check-consistency` + +Check whether the versions defined in `version_files` and the version in commitizen +configuration are consistent before bumping version. + +```bash +cz bump --check-consistency +``` + +For example, if we have `pyproject.toml` + +```toml +[tool.commitizen] +version = "1.21.0" +version_files = [ + "src/__version__.py", + "setup.py", +] +``` + +`src/__version__.py`, + +```python +__version__ = "1.21.0" +``` + +and `setup.py`. + +```python +from setuptools import setup + +setup(..., version="1.0.5", ...) +``` + +If `--check-consistency` is used, commitizen will check whether the current version in `pyproject.toml` +exists in all version_files and find out it does not exist in `setup.py` and fails. +However, it will still update `pyproject.toml` and `src/__version__.py`. + +To fix it, you'll first `git checkout .` to reset to the status before trying to bump and update +the version in `setup.py` to `1.21.0` + +### `--local-version` + +Bump the local portion of the version. + +```bash +cz bump --local-version +``` + +For example, if we have `pyproject.toml` + +```toml +[tool.commitizen] +version = "5.3.5+0.1.0" +``` + +If `--local-version` is used, it will bump only the local version `0.1.0` and keep the public version `5.3.5` intact, bumping to the version `5.3.5+0.2.0`. + +### `--annotated-tag` + +If `--annotated-tag` is used, commitizen will create annotated tags. Also available via configuration, in `pyproject.toml` or `.cz.toml`. + +### `--annotated-tag-message` + +If `--annotated-tag-message` is used, commitizen will create annotated tags with the given message. + +### `--changelog-to-stdout` + +If `--changelog-to-stdout` is used, the incremental changelog generated by the bump +will be sent to the stdout, and any other message generated by the bump will be +sent to stderr. + +If `--changelog` is not used with this command, it is still smart enough to +understand that the user wants to create a changelog. It is recommended to be +explicit and use `--changelog` (or the setting `update_changelog_on_bump`). + +This command is useful to "transport" the newly created changelog. +It can be sent to an auditing system, or to create a Github Release. + +Example: + +```bash +cz bump --changelog --changelog-to-stdout > body.md +``` + +### `--git-output-to-stderr` + +If `--git-output-to-stderr` is used, git commands output is redirected to stderr. + +This command is useful when used with `--changelog-to-stdout` and piping the output to a file, +and you don't want the `git commit` output polluting the stdout. + +### `--retry` + +If you use tools like [pre-commit](https://pre-commit.com/), add this flag. +It will retry the commit if it fails the 1st time. + +Useful to combine with code formatters, like [Prettier](https://prettier.io/). + +### `--major-version-zero` + +A project in its initial development should have a major version zero, and even breaking changes +should not bump that major version from zero. This command ensures that behavior. + +If `--major-version-zero` is used for projects that have a version number greater than zero it fails. +If used together with a manual version the command also fails. + +We recommend setting `major_version_zero = true` in your configuration file while a project +is in its initial development. Remove that configuration using a breaking-change commit to bump +your projectโ€™s major version to `v1.0.0` once your project has reached maturity. + +### `--version-scheme` + +Choose the version format, options: `pep440`, `semver`. + +Default: `pep440` + +Recommended for python: `pep440` + +Recommended for other: `semver` + +You can also set this in the [configuration](#version_scheme) with `version_scheme = "semver"`. + +[pep440][pep440] and [semver][semver] are quite similar, their difference lies in +how the prereleases look. + +| schemes | pep440 | semver | +| -------------- | -------------- | --------------- | +| non-prerelease | `0.1.0` | `0.1.0` | +| prerelease | `0.3.1a0` | `0.3.1-a0` | +| devrelease | `0.1.1.dev1` | `0.1.1-dev1` | +| dev and pre | `1.0.0a3.dev1` | `1.0.0-a3-dev1` | + +Can I transition from one to the other? + +Yes, you shouldn't have any issues. + +### `--template` + +Provides your own changelog jinja template. +See [the template customization section](../customization.md#customizing-the-changelog-template) + +### `--extra` + +Provides your own changelog extra variables by using the `extras` settings or the `--extra/-e` parameter. + +```bash +cz bump --changelog --extra key=value -e short="quoted value" +``` + +See [the template customization section](../customization.md#customizing-the-changelog-template). + +### `--build-metadata` + +Provides a way to specify additional metadata in the version string. This parameter is not compatible with `--local-version` as it uses the same part of the version string. + +```bash +cz bump --build-metadata yourmetadata +``` + +Will create a version like `1.1.2+yourmetadata`. +This can be useful for multiple things +- Git hash in version +- Labeling the version with additional metadata. + +Note that Commitizen ignores everything after `+` when it bumps the version. It is therefore safe to write different build-metadata between versions. + +You should normally not use this functionality, but if you decide to do, keep in mind that +- Version `1.2.3+a`, and `1.2.3+b` are the same version! Tools should not use the string after `+` for version calculation. This is probably not a guarantee (example in helm) even tho it is in the spec. +- It might be problematic having the metadata in place when doing upgrades depending on what tool you use. + +### `--get-next` + +Provides a way to determine the next version and write it to stdout. This parameter is not compatible with `--changelog` +and `manual version`. + +```bash +cz bump --get-next +``` + +Will output the next version, e.g., `1.2.3`. This can be useful for determining the next version based on CI for non +production environments/builds. + +This behavior differs from the `--dry-run` flag. The `--dry-run` flag provides a more detailed output and can also show +the changes as they would appear in the changelog file. + +The following output is the result of `cz bump --dry-run`: + +``` +bump: version 3.28.0 โ†’ 3.29.0 +tag to create: v3.29.0 +increment detected: MINOR +``` + +The following output is the result of `cz bump --get-next`: + +``` +3.29.0 +``` + +The `--get-next` flag will raise a `NoneIncrementExit` if the found commits are not eligible for a version bump. + +For information on how to suppress this exit, see [avoid raising errors](#avoid-raising-errors). + +### `--allow-no-commit` + +Allow the project version to be bumped even when there's no eligible version. This is most useful when used with `--increment {MAJOR,MINOR,PATCH}` or `[MANUL_VERSION]` + +```sh +# bump a minor version even when there's only bug fixes, documentation changes or even no commits +cz bump --incremental MINOR --allow-no-commit + +# bump version to 2.0.0 even when there's no breaking changes changes or even no commits +cz bump --allow-no-commit 2.0.0 +``` + +## Avoid raising errors + +Some situations from commitizen raise an exit code different than 0. +If the error code is different than 0, any CI or script running commitizen might be interrupted. + +If you have a special use case, where you don't want to raise one of this error codes, you can +tell commitizen to not raise them. + +### Recommended use case + +At the moment, we've identified that the most common error code to skip is + +| Error name | Exit code | +| ----------------- | --------- | +| NoneIncrementExit | 21 | + +There are some situations where you don't want to get an error code when some +commits do not match your rules, you just want those commits to be skipped. + +```sh +cz -nr 21 bump +``` + +### Easy way + +Check which error code was raised by commitizen by running in the terminal + +```sh +echo $? +``` + +The output should be an integer like this + +```sh +3 +``` + +And then you can tell commitizen to ignore it: + +```sh +cz --no-raise 3 +``` + +You can tell commitizen to skip more than one if needed: + +```sh +cz --no-raise 3,4,5 +``` + +### Longer way + +Check the list of [exit_codes](../exit_codes.md) and understand which one you have +to skip and why. + +Remember to document somewhere this, because you'll forget. + +For example if the system raises a `NoneIncrementExit` error, you look it up +on the list and then you can use the exit code: + +```sh +cz -nr 21 bump +``` + +## Configuration + +### `tag_format` + +`tag_format` and `version_scheme` are combined to make Git tag names from versions. + +These are used in: + +- `cz bump`: Find previous release tag (exact match) and generate new tag. +- Find previous release tags in `cz changelog`. + - If `--incremental`: Using latest version found in the changelog, scan existing Git tags with 89\% similarity match. + - `--rev-range` is converted to Git tag names with `tag_format` before searching Git history. +- If the `scm` `version_provider` is used, it uses different regexes to find the previous version tags: + - If `tag_format` is set to `$version` (default): `VersionProtocol.parser` (allows `v` prefix) + - If `tag_format` is set: Custom regex similar to SemVer (not as lenient as PEP440 e.g. on dev-releases) + +Commitizen supports 2 types of formats, a simple and a more complex. + +```bash +cz bump --tag-format="v$version" +``` + +```bash +cz bump --tag-format="v$minor.$major.$patch$prerelease.$devrelease" +``` + +In your `pyproject.toml` or `.cz.toml` + +```toml +[tool.commitizen] +tag_format = "v$major.$minor.$patch$prerelease" +``` + +The variables must be preceded by a `$` sign and optionally can be wrapped in `{}` . Default is `$version`. + +Supported variables: + +| Variable | Description | +|--------------------------------|---------------------------------------------| +| `$version`, `${version}` | full generated version | +| `$major`, `${major}` | MAJOR increment | +| `$minor`, `${minor}` | MINOR increment | +| `$patch`, `${patch}` | PATCH increment | +| `$prerelease`, `${prerelease}` | Prerelease (alpha, beta, release candidate) | +| `$devrelease`, ${devrelease}` | Development release | + +--- + +### `version_files` \* + +It is used to identify the files which should be updated with the new version. +It is also possible to provide a pattern for each file, separated by colons (`:`). + +Commitizen will update its configuration file automatically (`pyproject.toml`, `.cz`) when bumping, +regarding if the file is present or not in `version_files`. + +\* Renamed from `files` to `version_files`. + +Some examples + +`pyproject.toml`, `.cz.toml` or `cz.toml` + +```toml +[tool.commitizen] +version_files = [ + "src/__version__.py", + "setup.py:version" +] +``` + +In the example above, we can see the reference `"setup.py:version"`. +This means that it will find a file `setup.py` and will only make a change +in a line containing the `version` substring. + +!!! note + Files can be specified using relative (to the execution) paths, absolute paths + or glob patterns. + +--- + +### `bump_message` + +Template used to specify the commit message generated when bumping. + +defaults to: `bump: version $current_version โ†’ $new_version` + +| Variable | Description | +| ------------------ | ----------------------------------- | +| `$current_version` | the version existing before bumping | +| `$new_version` | version generated after bumping | + +Some examples + +`pyproject.toml`, `.cz.toml` or `cz.toml` + +```toml +[tool.commitizen] +bump_message = "release $current_version โ†’ $new_version [skip-ci]" +``` + +--- + +### `update_changelog_on_bump` + +When set to `true` the changelog is always updated incrementally when running `cz bump`, so the user does not have to provide the `--changelog` flag every time. + +defaults to: `false` + +```toml +[tool.commitizen] +update_changelog_on_bump = true +``` + +--- + +### `annotated_tag` + +When set to `true` commitizen will create annotated tags. + +```toml +[tool.commitizen] +annotated_tag = true +``` + +--- + +### `gpg_sign` + +When set to `true` commitizen will create gpg signed tags. + +```toml +[tool.commitizen] +gpg_sign = true +``` + +--- + +### `major_version_zero` + +When set to `true` commitizen will keep the major version at zero. +Useful during the initial development stage of your project. + +Defaults to: `false` + +```toml +[tool.commitizen] +major_version_zero = true +``` + +--- + +### `pre_bump_hooks` + +A list of optional commands that will run right _after_ updating `version_files` +and _before_ actual committing and tagging the release. + +Useful when you need to generate documentation based on the new version. During +execution of the script, some environment variables are available: + +| Variable | Description | +| ---------------------------- | ---------------------------------------------------------- | +| `CZ_PRE_IS_INITIAL` | `True` when this is the initial release, `False` otherwise | +| `CZ_PRE_CURRENT_VERSION` | Current version, before the bump | +| `CZ_PRE_CURRENT_TAG_VERSION` | Current version tag, before the bump | +| `CZ_PRE_NEW_VERSION` | New version, after the bump | +| `CZ_PRE_NEW_TAG_VERSION` | New version tag, after the bump | +| `CZ_PRE_MESSAGE` | Commit message of the bump | +| `CZ_PRE_INCREMENT` | Whether this is a `MAJOR`, `MINOR` or `PATH` release | +| `CZ_PRE_CHANGELOG_FILE_NAME` | Path to the changelog file, if available | + +```toml +[tool.commitizen] +pre_bump_hooks = [ + "scripts/generate_documentation.sh" +] +``` + +--- + +### `post_bump_hooks` + +A list of optional commands that will run right _after_ committing and tagging the release. + +Useful when you need to send notifications about a release, or further automate deploying the +release. During execution of the script, some environment variables are available: + +| Variable | Description | +| ------------------------------ | ----------------------------------------------------------- | +| `CZ_POST_WAS_INITIAL` | `True` when this was the initial release, `False` otherwise | +| `CZ_POST_PREVIOUS_VERSION` | Previous version, before the bump | +| `CZ_POST_PREVIOUS_TAG_VERSION` | Previous version tag, before the bump | +| `CZ_POST_CURRENT_VERSION` | Current version, after the bump | +| `CZ_POST_CURRENT_TAG_VERSION` | Current version tag, after the bump | +| `CZ_POST_MESSAGE` | Commit message of the bump | +| `CZ_POST_INCREMENT` | Whether this was a `MAJOR`, `MINOR` or `PATH` release | +| `CZ_POST_CHANGELOG_FILE_NAME` | Path to the changelog file, if available | + +```toml +[tool.commitizen] +post_bump_hooks = [ + "scripts/slack_notification.sh" +] +``` + +### `prerelease_offset` + +Offset with which to start counting prereleases. + +Defaults to: `0` + +```toml +[tool.commitizen] +prerelease_offset = 1 +``` + +### `version_scheme` + +Choose version scheme + +| schemes | pep440 | semver | semver2 | +| -------------- | -------------- | --------------- | --------------------- | +| non-prerelease | `0.1.0` | `0.1.0` | `0.1.0` | +| prerelease | `0.3.1a0` | `0.3.1-a0` | `0.3.1-alpha.0` | +| devrelease | `0.1.1.dev1` | `0.1.1-dev1` | `0.1.1-dev.1` | +| dev and pre | `1.0.0a3.dev1` | `1.0.0-a3-dev1` | `1.0.0-alpha.3.dev.1` | + +Options: `pep440`, `semver`, `semver2` + +Defaults to: `pep440` + +```toml +[tool.commitizen] +version_scheme = "semver" +``` + +## Custom bump + +Read the [customizing section](../customization.md). + +[pep440]: https://www.python.org/dev/peps/pep-0440/ +[semver]: https://semver.org/ diff --git a/docs/commands/changelog.md b/docs/commands/changelog.md new file mode 100644 index 0000000..cbf22b1 --- /dev/null +++ b/docs/commands/changelog.md @@ -0,0 +1,195 @@ +## About + +This command will generate a changelog following the committing rules established. + +To create the changelog automatically on bump, add the setting [update_changelog_on_bump](./bump.md#update_changelog_on_bump) + +```toml +[tool.commitizen] +update_changelog_on_bump = true +``` + +## Usage + +![cz changelog --help](../images/cli_help/cz_changelog___help.svg) + +### Examples + +#### Generate full changelog + +```bash +cz changelog +``` + +```bash +cz ch +``` + +#### Get the changelog for the given version + +```bash +cz changelog 0.3.0 --dry-run +``` + +#### Get the changelog for the given version range + +```bash +cz changelog 0.3.0..0.4.0 --dry-run +``` + +## Constrains + +changelog generation is constrained only to **markdown** files. + +## Description + +These are the variables used by the changelog generator. + +```md +# <version> (<date>) + +## <change_type> + +- **<scope>**: <message> +``` + +It will create a full block like above per version found in the tags. +And it will create a list of the commits found. +The `change_type` and the `scope` are optional, they don't need to be provided, +but if your regex does they will be rendered. + +The format followed by the changelog is the one from [keep a changelog][keepachangelog] +and the following variables are expected: + +| Variable | Description | Source | +| ------------- | ---------------------------------------------------------------------------------------------- | -------------- | +| `version` | Version number which should follow [semver][semver] | `tags` | +| `date` | Date in which the tag was created | `tags` | +| `change_type` | The group where the commit belongs to, this is optional. Example: fix | `commit regex` | +| `message`\* | Information extracted from the commit message | `commit regex` | +| `scope` | Contextual information. Should be parsed using the regex from the message, it will be **bold** | `commit regex` | +| `breaking` | Whether is a breaking change or not | `commit regex` | + +- **required**: is the only one required to be parsed by the regex + +## Configuration + +### `unreleased_version` + +There is usually a chicken and egg situation when automatically +bumping the version and creating the changelog. +If you bump the version first, you have no changelog, you have to +create it later, and it won't be included in +the release of the created version. + +If you create the changelog before bumping the version, then you +usually don't have the latest tag, and the _Unreleased_ title appears. + +By introducing `unreleased_version` you can prevent this situation. + +Before bumping you can run: + +```bash +cz changelog --unreleased-version="v1.0.0" +``` + +Remember to use the tag instead of the raw version number + +For example if the format of your tag includes a `v` (`v1.0.0`), then you should use that, +if your tag is the same as the raw version, then ignore this. + +Alternatively you can directly bump the version and create the changelog by doing + +```bash +cz bump --changelog +``` + +### `file-name` + +This value can be updated in the `toml` file with the key `changelog_file` under `tools.commitizen` + +Specify the name of the output file, remember that changelog only works with markdown. + +```bash +cz changelog --file-name="CHANGES.md" +``` + +### `incremental` + +This flag can be set in the `toml` file with the key `changelog_incremental` under `tools.commitizen` + +Benefits: + +- Build from latest version found in changelog, this is useful if you have a different changelog and want to use commitizen +- Update unreleased area +- Allows users to manually touch the changelog without being rewritten. + +```bash +cz changelog --incremental +``` + +```toml +[tools.commitizen] +# ... +changelog_incremental = true +``` + +### `start-rev` + +This value can be set in the `toml` file with the key `changelog_start_rev` under `tools.commitizen` + +Start from a given git rev to generate the changelog. Commits before that rev will not be considered. This is especially useful for long-running projects adopting conventional commits, where old commit messages might fail to be parsed for changelog generation. + +```bash +cz changelog --start-rev="v0.2.0" +``` + +```toml +[tools.commitizen] +# ... +changelog_start_rev = "v0.2.0" +``` + +### merge-prerelease + +This flag can be set in the `toml` file with the key `changelog_merge_prerelease` under `tools.commitizen` + +Collects changes from prereleases into the next non-prerelease. This means that if you have a prerelease version, and then a normal release, the changelog will show the prerelease changes as part of the changes of the normal release. If not set, it will include prereleases in the changelog. + +```bash +cz changelog --merge-prerelease +``` + +```toml +[tools.commitizen] +# ... +changelog_merge_prerelease = true +``` + +### `template` + +Provides your own changelog jinja template by using the `template` settings or the `--template` parameter. +See [the template customization section](../customization.md#customizing-the-changelog-template) + +### `extras` + +Provides your own changelog extra variables by using the `extras` settings or the `--extra/-e` parameter. + +```bash +cz changelog --extra key=value -e short="quoted value" +``` + +See [the template customization section](../customization.md#customizing-the-changelog-template) + +## Hooks + +Supported hook methods: + +- per parsed message: useful to add links +- end of changelog generation: useful to send slack or chat message, or notify another department + +Read more about hooks in the [customization page][customization] + +[keepachangelog]: https://keepachangelog.com/ +[semver]: https://semver.org/ +[customization]: ../customization.md diff --git a/docs/commands/check.md b/docs/commands/check.md new file mode 100644 index 0000000..e45ecd8 --- /dev/null +++ b/docs/commands/check.md @@ -0,0 +1,87 @@ +# Check + +## About + +This feature checks whether the commit message follows the given committing rules. And comment in git message will be ignored. + +If you want to setup an automatic check before every git commit, please refer to +[Automatically check message before commit](../tutorials/auto_check.md). + +## Usage + +![cz check --help](../images/cli_help/cz_check___help.svg) + +There are three mutually exclusive ways to use `cz check`: + +- with `--rev-range` to check a range of pre-existing commits +- with `--message` or by piping the message to it to check a given string +- or with `--commit-msg-file` to read the commit message from a file + +### Git Rev Range + +If you'd like to check a commit's message after it has already been created, then you can specify the range of commits to check with `--rev-range REV_RANGE`. + +```bash +$ cz check --rev-range REV_RANGE +``` + +For example, if you'd like to check all commits on a branch, you can use `--rev-range master..HEAD`. Or, if you'd like to check all commits starting from when you first implemented commit message linting, you can use `--rev-range <first_commit_sha>..HEAD`. + +For more info on how git commit ranges work, you can check the [git documentation](https://git-scm.com/book/en/v2/Git-Tools-Revision-Selection#_commit_ranges). + +### Commit Message + +There are two ways you can provide your plain message and check it. + +#### Method 1: use -m or --message + +```bash +$ cz check --message MESSAGE +``` + +In this option, MESSAGE is the commit message to be checked. + +#### Method 2: use pipe to pipe it to `cz check` + +```bash +$ echo MESSAGE | cz check +``` + +In this option, MESSAGE is piped to cz check and would be checked. + +### Commit Message File + +```bash +$ cz check --commit-msg-file COMMIT_MSG_FILE +``` + +In this option, COMMIT_MSG_FILE is the path of the temporal file that contains the commit message. +This argument can be useful when cooperating with git hook, please check [Automatically check message before commit](../tutorials/auto_check.md) for more information about how to use this argument with git hook. + +### Allow Abort + +```bash +cz check --message MESSAGE --allow-abort +``` + +Empty commit messages typically instruct Git to abort a commit, so you can pass `--allow-abort` to +permit them. Since `git commit` accepts an `--allow-empty-message` flag (primarily for wrapper scripts), you may wish to disallow such commits in CI. `--allow-abort` may be used in conjunction with any of the other options. + +### Allowed Prefixes + +If the commit message starts by some specific prefixes, `cz check` returns `True` without checkign the regex. +By default, the the following prefixes are allowed: `Merge`, `Revert`, `Pull request`, `fixup!` and `squash!`. + +```bash +cz check --message MESSAGE --allowed-prefixes 'Merge' 'Revert' 'Custom Prefix' +``` + +### Commit message length limit + +The argument `-l` (or `--message-length-limmit`) followed by a positive number, can limit the length of commit messages. +For example, `cz check --message MESSAGE -l 3` would fail the check, since `MESSAGE` is more than 3 characters long. +By default, the limit is set to 0, which means no limit on the length. + +**Note that the limit applies only to the first line of the message.*** +Specifically, for `ConventionalCommitsCz` the length only counts from the type of change to the subject, +while the body, and the footer are not counted. diff --git a/docs/commands/commit.md b/docs/commands/commit.md new file mode 100644 index 0000000..7760a2b --- /dev/null +++ b/docs/commands/commit.md @@ -0,0 +1,52 @@ +![Using commitizen cli](../images/demo.gif) + +## About + +In your terminal run `cz commit` or the shortcut `cz c` to generate a guided git commit. + +You can run `cz commit --write-message-to-file COMMIT_MSG_FILE` to additionally save the +generated message to a file. This can be combined with the `--dry-run` flag to only +write the message to a file and not modify files and create a commit. A possible use +case for this is to [automatically prepare a commit message](../tutorials/auto_prepare_commit_message.md). + + +!!! note + To maintain platform compatibility, the `commit` command disable ANSI escaping in its output. + In particular pre-commit hooks coloring will be deactivated as discussed in [commitizen-tools/commitizen#417](https://github.com/commitizen-tools/commitizen/issues/417). + +## Usage + +![cz commit --help](../images/cli_help/cz_commit___help.svg) + +### git options + +`git` command options that are not implemented by commitizen can be use via the `--` syntax for the `commit` command. +The syntax separates commitizen arguments from `git commit` arguments by a double dash. This is the resulting syntax: +```sh +cz commit <commitizen-args> -- <git-cli-args> + +# e.g., cz commit --dry-run -- -a -S +``` +For example, using the `-S` option on `git commit` to sign a commit is now commitizen compatible: `cz c -- -S` + +!!! note + Deprecation warning: A commit can be signed off using `cz commit --signoff` or the shortcut `cz commit -s`. + This syntax is now deprecated in favor of the new `cz commit -- -s` syntax. + +### Retry + +You can use `cz commit --retry` to reuse the last commit message when the previous commit attempt failed. +To automatically retry when running `cz commit`, you can set the `retry_after_failure` +configuration option to `true`. Running `cz commit --no-retry` makes commitizen ignore `retry_after_failure`, forcing +a new commit message to be prompted. + +### Commit message length limit + +The argument `-l` (or `--message-length-limit`) followed by a positive number can limit the length of commit messages. +An exception would be raised when the message length exceeds the limit. +For example, `cz commit -l 72` will limit the length of commit messages to 72 characters. +By default the limit is set to 0, which means no limit on the length. + +**Note that the limit applies only to the first line of the message.** +Specifically, for `ConventionalCommitsCz` the length only counts from the type of change to the subject, +while the body and the footer are not counted. diff --git a/docs/commands/example.md b/docs/commands/example.md new file mode 100644 index 0000000..8243453 --- /dev/null +++ b/docs/commands/example.md @@ -0,0 +1,5 @@ +Show commit example + +## Usage + +![cz example --help](../images/cli_help/cz_example___help.svg) diff --git a/docs/commands/info.md b/docs/commands/info.md new file mode 100644 index 0000000..5f816ba --- /dev/null +++ b/docs/commands/info.md @@ -0,0 +1,5 @@ +Show information about the cz + +## Usage + +![cz info --help](../images/cli_help/cz_info___help.svg) diff --git a/docs/commands/init.md b/docs/commands/init.md new file mode 100644 index 0000000..01e1db6 --- /dev/null +++ b/docs/commands/init.md @@ -0,0 +1,27 @@ +## Usage + +![cz init --help](../images/cli_help/cz_init___help.svg) + +## Example + +To start using commitizen, the recommended approach is to run + +```sh +cz init +``` + +![init](../images/init.gif) + +This command will ask you for information about the project and will +configure the selected file type (`pyproject.toml`, `.cz.toml`, etc.). + +The `init` will help you with + +1. Choose a convention rules (`name`) +2. Choosing a version provider (`commitizen` or for example `Cargo.toml`) +3. Detecting your project's version +4. Detecting the tag format used +5. Choosing a version type (`semver` or `pep440`) +6. Whether to create the changelog automatically or not during bump +7. Whether you want to keep the major as zero while building alpha software. +8. Whether to setup pre-commit hooks. diff --git a/docs/commands/ls.md b/docs/commands/ls.md new file mode 100644 index 0000000..f255ca5 --- /dev/null +++ b/docs/commands/ls.md @@ -0,0 +1,3 @@ +## Usage + +![cz ls --help](../images/cli_help/cz_ls___help.svg) diff --git a/docs/commands/schema.md b/docs/commands/schema.md new file mode 100644 index 0000000..bd6fa85 --- /dev/null +++ b/docs/commands/schema.md @@ -0,0 +1,5 @@ +Show commit schema + +## Usage + +![cz schema --help](../images/cli_help/cz_schema___help.svg) diff --git a/docs/commands/version.md b/docs/commands/version.md new file mode 100644 index 0000000..9a8176b --- /dev/null +++ b/docs/commands/version.md @@ -0,0 +1,5 @@ +Get the version of the installed commitizen or the current project (default: installed commitizen) + +## Usage + +![cz version --help](../images/cli_help/cz_version___help.svg) diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 0000000..d1ae90b --- /dev/null +++ b/docs/config.md @@ -0,0 +1,423 @@ +# Configuration + +## Settings + +### `name` + +Type: `str` + +Default: `"cz_conventional_commits"` + +Name of the committing rules to use + +### `version` + +Type: `str` + +Default: `None` + +Current version. Example: "0.1.2". Required if you use `version_provider = "commitizen"`. + +### `version_files` + +Type: `list` + +Default: `[ ]` + +Files were the version will be updated. A pattern to match a line, can also be specified, separated by `:` [Read more][version_files] + +### `version_provider` + +Type: `str` + +Default: `commitizen` + +Version provider used to read and write version [Read more](#version-providers) + +### `version_scheme` + +Type: `str` + +Default: `pep440` + +Select a version scheme from the following options [`pep440`, `semver`, `semver2`]. +Useful for non-python projects. [Read more][version-scheme] + +### `tag_format` + +Type: `str` + +Default: `$version` + +Format for the git tag, useful for old projects, that use a convention like `"v1.2.1"`. [Read more][tag_format] + +### `legacy_tag_formats` + +Type: `list` + +Default: `[ ]` + +Legacy git tag formats, useful for old projects that changed tag format. +Tags matching those formats will be recognized as version tags and be included in the changelog. +Each entry use the the syntax as [`tag_format`](#tag_format). [Read more][tag_format] + +### `ignored_tag_formats` + +Type: `list` + +Default: `[ ]` + +Tags matching those formats will be totally ignored and won't raise a warning. +Each entry use the the syntax as [`tag_format`](#tag_format) with the addition of `*` +that will match everything (non-greedy). [Read more][tag_format] + +### `update_changelog_on_bump` + +Type: `bool` + +Default: `false` + +Create changelog when running `cz bump` + +### `gpg_sign` + +Type: `bool` + +Default: `false` + +Use gpg signed tags instead of lightweight tags. + +### `annotated_tag` + +Type: `bool` + +Default: `false` + +Use annotated tags instead of lightweight tags. [See difference][annotated-tags-vs-lightweight] + +### `bump_message` + +Type: `str` + +Default: `None` + +Create custom commit message, useful to skip ci. [Read more][bump_message] + +### `retry_after_failure` + +Type: `bool` + +Default: `false` + +Automatically retry failed commit when running `cz commit`. [Read more][retry_after_failure] + +### `allow_abort` + +Type: `bool` + +Default: `false` + +Disallow empty commit messages, useful in ci. [Read more][allow_abort] + +### `allowed_prefixes` + +Type: `list` +Default: `[ "Merge", "Revert", "Pull request", "fixup!", "squash!"]` +Allow some prefixes and do not try to match the regex when checking the message [Read more][allowed_prefixes] + +### `changelog_file` + +Type: `str` + +Default: `CHANGELOG.md` + +Filename of exported changelog + +### `changelog_format` + +Type: `str` + +Default: None + +Format used to parse and generate the changelog, If not specified, guessed from [`changelog_file`](#changelog_file). + +### `changelog_incremental` + +Type: `bool` + +Default: `false` + +Update changelog with the missing versions. This is good if you don't want to replace previous versions in the file. Note: when doing `cz bump --changelog` this is automatically set to `true` + +### `changelog_start_rev` + +Type: `str` + +Default: `None` + +Start from a given git rev to generate the changelog + +### `changelog_merge_prerelease` + +Type: `bool` + +Default: `false` + +Collect all changes of prerelease versions into the next non-prerelease version when creating the changelog. + +### `style` + +Type: `list` + +see above + +Style for the prompts (It will merge this value with default style.) [See More (Styling your prompts with your favorite colors)][additional-features] + +### `customize` + +Type: `dict` + +Default: `None` + +**This is only supported when config through `toml`.** Custom rules for committing and bumping. [Read more][customization] + +### `use_shortcuts` + +Type: `bool` + +Default: `false` + +If enabled, commitizen will show keyboard shortcuts when selecting from a list. Define a `key` for each of your choices to set the key. [Read more][shortcuts] + +### `major_version_zero` + +Type: `bool` + +Default: `false` + +When true, breaking changes on a `0.x` will remain as a `0.x` version. On `false`, a breaking change will bump a `0.x` version to `1.0`. [major-version-zero] + +### `prerelease_offset` + +Type: `int` + +Default: `0` + +In some circumstances, a prerelease cannot start with a 0, e.g. in an embedded project individual characters are encoded as bytes. This can be done by specifying an offset from which to start counting. [prerelease-offset] + +### `pre_bump_hooks` + +Type: `list[str]` + +Default: `[]` + +Calls the hook scripts **before** bumping version. [Read more][pre_bump_hooks] + +### `post_bump_hooks` + +Type: `list[str]` + +Default: `[]` + +Calls the hook scripts **after** bumping the version. [Read more][post_bump_hooks] + +### `encoding` + +Type: `str` + +Default: `utf-8` + +Sets the character encoding to be used when parsing commit messages. [Read more][encoding] + +### `template` + +Type: `str` + +Default: `None` (provided by plugin) + +Provide custom changelog jinja template path relative to the current working directory. [Read more][template-customization] + +### `extras` + +Type: `dict[str, Any]` + +Default: `{}` + +Provide extra variables to the changelog template. [Read more][template-customization] + +## Configuration file + +### pyproject.toml, .cz.toml or cz.toml + +Default and recommended configuration format for a project. +For a **python** project, we recommend adding an entry to your `pyproject.toml`. +You can also create a `.cz.toml` or `cz.toml` file at the root of your project folder. + +Example configuration: + +```toml +[tool.commitizen] +name = "cz_conventional_commits" +version = "0.1.0" +version_files = [ + "src/__version__.py", + "pyproject.toml:version" +] +update_changelog_on_bump = true +style = [ + ["qmark", "fg:#ff9d00 bold"], + ["question", "bold"], + ["answer", "fg:#ff9d00 bold"], + ["pointer", "fg:#ff9d00 bold"], + ["highlighted", "fg:#ff9d00 bold"], + ["selected", "fg:#cc5454"], + ["separator", "fg:#cc5454"], + ["instruction", ""], + ["text", ""], + ["disabled", "fg:#858585 italic"] +] +``` + +### .cz.json or cz.json + +Commitizen has support for JSON configuration. Recommended for `NodeJS` projects. + +```json +{ + "commitizen": { + "name": "cz_conventional_commits", + "version": "0.1.0", + "version_files": ["src/__version__.py", "pyproject.toml:version"], + "style": [ + ["qmark", "fg:#ff9d00 bold"], + ["question", "bold"], + ["answer", "fg:#ff9d00 bold"], + ["pointer", "fg:#ff9d00 bold"], + ["highlighted", "fg:#ff9d00 bold"], + ["selected", "fg:#cc5454"], + ["separator", "fg:#cc5454"], + ["instruction", ""], + ["text", ""], + ["disabled", "fg:#858585 italic"] + ] + } +} +``` + +### .cz.yaml or cz.yaml + +YAML configuration is supported by Commitizen. Recommended for `Go`, `ansible`, or even `helm` charts projects. + +```yaml +commitizen: + name: cz_conventional_commits + version: 0.1.0 + version_files: + - src/__version__.py + - pyproject.toml:version + style: + - - qmark + - fg:#ff9d00 bold + - - question + - bold + - - answer + - fg:#ff9d00 bold + - - pointer + - fg:#ff9d00 bold + - - highlighted + - fg:#ff9d00 bold + - - selected + - fg:#cc5454 + - - separator + - fg:#cc5454 + - - instruction + - "" + - - text + - "" + - - disabled + - fg:#858585 italic +``` + +## Version providers + +Commitizen can read and write version from different sources. +By default, it use the `commitizen` one which is using the `version` field from the commitizen settings. +But you can use any `commitizen.provider` entrypoint as value for `version_provider`. + +Commitizen provides some version providers for some well known formats: + +| name | description | +| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `commitizen` | Default version provider: Fetch and set version in commitizen config. | +| `scm` | Fetch the version from git and does not need to set it back | +| `pep621` | Get and set version from `pyproject.toml` `project.version` field | +| `poetry` | Get and set version from `pyproject.toml` `tool.poetry.version` field | +| `uv` | Get and set version from `pyproject.toml` `project.version` field and `uv.lock` `package.version` field whose `package.name` field is the same as `pyproject.toml` `project.name` field | +| `cargo` | Get and set version from `Cargo.toml` `project.version` field | +| `npm` | Get and set version from `package.json` `version` field, `package-lock.json` `version,packages.''.version` fields if the file exists, and `npm-shrinkwrap.json` `version,packages.''.version` fields if the file exists | +| `composer` | Get and set version from `composer.json` `project.version` field | + +!!! note +The `scm` provider is meant to be used with `setuptools-scm` or any packager `*-scm` plugin. + +An example in your `.cz.toml` or `cz.toml` would look like this: + +```toml +[tool.commitizen] +version_provider = "pep621" +``` + +### Custom version provider + +You can add you own version provider by extending `VersionProvider` and exposing it on the `commitizen.provider` entrypoint. + +Here a quick example of a `my-provider` provider reading and writing version in a `VERSION` file. + +```python title="my_provider.py" +from pathlib import Path +from commitizen.providers import VersionProvider + + +class MyProvider(VersionProvider): + file = Path() / "VERSION" + + def get_version(self) -> str: + return self.file.read_text() + + def set_version(self, version: str): + self.file.write_text(version) +``` + +```python title="setup.py" +from setuptools import setup + +setup( + name="my-commitizen-provider", + version="0.1.0", + py_modules=["my_provider"], + install_requires=["commitizen"], + entry_points={ + "commitizen.provider": [ + "my-provider = my_provider:MyProvider", + ] + }, +) +``` + +[version_files]: commands/bump.md#version_files +[tag_format]: commands/bump.md#tag_format +[bump_message]: commands/bump.md#bump_message +[major-version-zero]: commands/bump.md#-major-version-zero +[prerelease-offset]: commands/bump.md#-prerelease_offset +[retry_after_failure]: commands/commit.md#retry +[allow_abort]: commands/check.md#allow-abort +[version-scheme]: commands/bump.md#version-scheme +[pre_bump_hooks]: commands/bump.md#pre_bump_hooks +[post_bump_hooks]: commands/bump.md#post_bump_hooks +[allowed_prefixes]: commands/check.md#allowed-prefixes +[additional-features]: https://github.com/tmbo/questionary#additional-features +[customization]: customization.md +[shortcuts]: customization.md#shortcut-keys +[template-customization]: customization.md#customizing-the-changelog-template +[annotated-tags-vs-lightweight]: https://stackoverflow.com/a/11514139/2047185 +[encoding]: tutorials/writing_commits.md#writing-commits diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..0da1707 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,108 @@ +## Contributing to commitizen + +First of all, thank you for taking the time to contribute! ๐ŸŽ‰ + +When contributing to [commitizen](https://github.com/commitizen-tools/commitizen), please first create an [issue](https://github.com/commitizen-tools/commitizen/issues) to discuss the change you wish to make before making a change. + +If you're a first-time contributor, you can check the issues with [good first issue](https://github.com/commitizen-tools/commitizen/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) tag. + +## Install before contributing + +1. Install [poetry](https://python-poetry.org/) `>=2.0.0`, installation [pages](https://python-poetry.org/docs/#installing-with-the-official-installer) +2. Install [gpg](https://gnupg.org), installation [pages](https://gnupg.org/documentation/manuals/gnupg/Installation.html#Installation). For Mac users, you could try [homebrew](https://brew.sh/). + +## Before making a pull request + +1. Fork [the repository](https://github.com/commitizen-tools/commitizen). +2. Clone the repository from your GitHub. +3. Setup development environment through [poetry](https://python-poetry.org/) (`poetry install`). +4. Setup [pre-commit](https://pre-commit.com/) hook (`poetry setup-pre-commit`) +5. Check out a new branch and add your modification. +6. Add test cases for all your changes. + (We use [CodeCov](https://codecov.io/) to ensure our test coverage does not drop.) +7. Use [commitizen](https://github.com/commitizen-tools/commitizen) to do git commit. We follow [conventional commits](https://www.conventionalcommits.org/). +8. Run `poetry all` to ensure you follow the coding style and the tests pass. +9. Optionally, update the `./docs/README.md` or `docs/images/cli_help` (through running `poetry doc:screenshots`). +9. **Do not** update the `CHANGELOG.md`, it will be automatically created after merging to `master`. +10. **Do not** update the versions in the project, they will be automatically updated. +10. If your changes are about documentation. Run `poetry doc` to serve documentation locally and check whether there is any warning or error. +11. Send a [pull request](https://github.com/commitizen-tools/commitizen/pulls) ๐Ÿ™ + +## Use of GitHub Labels + +* good-first-issue *(issue only)* +* help-wanted +* issue-status: needs-triage *(issue only)* **(default label for issues)** +* issue-status: wont-fix +* issue-status: wont-implement +* issue-status: duplicate +* issue-status: invalid +* issue-status: wait-for-response +* issue-status: wait-for-implementation +* issue-status: pr-created +* pr-status: wait-for-review **(default label for PRs)** +* pr-status: reviewing +* pr-status: wait-for-modification +* pr-status: wait-for-response +* pr-status: ready-to-merge +* needs: test-case *(pr only)* +* needs: documentation *(pr only)* +* type: feature +* type: bug +* type: documentation +* type: refactor +* type: question *(issue only)* +* os: Windows +* os: Linux +* os: macOS + + +### Issue life cycle + +```mermaid +graph TD + input[/issue created/] --> + needs-triage + needs-triage --triage--> close(wont-implement, wont-fix, duplicate, invalid) + + needs-triage --triage--> wait-for-implementation + needs-triage --triage--> wait-for-response + + wait-for-response --response--> needs-triage + + wait-for-implementation --PR-created--> pr-created --PR-merged--> output[/close/] + + close --> output[/close/] +``` + +### Pull request life cycle + +```mermaid +flowchart TD + input[/pull request created/] --> + wait-for-review + --start reviewing --> + reviewing + --finish review --> + reviewed{approved} + + reviewed --Y--> + wait-for-merge --> + output[/merge/] + + reviewed --n--> + require-more-information{require more information} + + require-more-information --y--> + wait-for-response + --response--> + require-more-information + + require-more-information --n--> + wait-for-modification + --modification-received--> + review +``` + + +[conventional-commits]: https://www.conventionalcommits.org/ diff --git a/docs/customization.md b/docs/customization.md new file mode 100644 index 0000000..5011330 --- /dev/null +++ b/docs/customization.md @@ -0,0 +1,527 @@ +Customizing commitizen is not hard at all. +We have two different ways to do so. + +## 1. Customize in configuration file + +The basic steps are: + +1. Define your custom committing or bumping rules in the configuration file. +2. Declare `name = "cz_customize"` in your configuration file, or add `-n cz_customize` when running commitizen. + +Example: + +```toml +[tool.commitizen] +name = "cz_customize" + +[tool.commitizen.customize] +message_template = "{{change_type}}:{% if show_message %} {{message}}{% endif %}" +example = "feature: this feature enable customize through config file" +schema = "<type>: <body>" +schema_pattern = "(feature|bug fix):(\\s.*)" +bump_pattern = "^(break|new|fix|hotfix)" +bump_map = {"break" = "MAJOR", "new" = "MINOR", "fix" = "PATCH", "hotfix" = "PATCH"} +change_type_order = ["BREAKING CHANGE", "feat", "fix", "refactor", "perf"] +info_path = "cz_customize_info.txt" +info = """ +This is customized info +""" +commit_parser = "^(?P<change_type>feature|bug fix):\\s(?P<message>.*)?" +changelog_pattern = "^(feature|bug fix)?(!)?" +change_type_map = {"feature" = "Feat", "bug fix" = "Fix"} + +[[tool.commitizen.customize.questions]] +type = "list" +name = "change_type" +choices = [{value = "feature", name = "feature: A new feature."}, {value = "bug fix", name = "bug fix: A bug fix."}] +# choices = ["feature", "fix"] # short version +message = "Select the type of change you are committing" + +[[tool.commitizen.customize.questions]] +type = "input" +name = "message" +message = "Body." + +[[tool.commitizen.customize.questions]] +type = "confirm" +name = "show_message" +message = "Do you want to add body message in commit?" +``` + +The equivalent example for a json config file: + +```json +{ + "commitizen": { + "name": "cz_customize", + "customize": { + "message_template": "{{change_type}}:{% if show_message %} {{message}}{% endif %}", + "example": "feature: this feature enable customize through config file", + "schema": "<type>: <body>", + "schema_pattern": "(feature|bug fix):(\\s.*)", + "bump_pattern": "^(break|new|fix|hotfix)", + "bump_map": { + "break": "MAJOR", + "new": "MINOR", + "fix": "PATCH", + "hotfix": "PATCH" + }, + "change_type_order": ["BREAKING CHANGE", "feat", "fix", "refactor", "perf"], + "info_path": "cz_customize_info.txt", + "info": "This is customized info", + "commit_parser": "^(?P<change_type>feature|bug fix):\\s(?P<message>.*)?", + "changelog_pattern": "^(feature|bug fix)?(!)?", + "change_type_map": {"feature": "Feat", "bug fix": "Fix"}, + "questions": [ + { + "type": "list", + "name": "change_type", + "choices": [ + { + "value": "feature", + "name": "feature: A new feature." + }, + { + "value": "bug fix", + "name": "bug fix: A bug fix." + } + ], + "message": "Select the type of change you are committing" + }, + { + "type": "input", + "name": "message", + "message": "Body." + }, + { + "type": "confirm", + "name": "show_message", + "message": "Do you want to add body message in commit?" + } + ] + } + } +} +``` + +And the correspondent example for a yaml file: + +```yaml +commitizen: + name: cz_customize + customize: + message_template: "{{change_type}}:{% if show_message %} {{message}}{% endif %}" + example: 'feature: this feature enable customize through config file' + schema: "<type>: <body>" + schema_pattern: "(feature|bug fix):(\\s.*)" + bump_pattern: "^(break|new|fix|hotfix)" + commit_parser: "^(?P<change_type>feature|bug fix):\\s(?P<message>.*)?" + changelog_pattern: "^(feature|bug fix)?(!)?" + change_type_map: + feature: Feat + bug fix: Fix + bump_map: + break: MAJOR + new: MINOR + fix: PATCH + hotfix: PATCH + change_type_order: ["BREAKING CHANGE", "feat", "fix", "refactor", "perf"] + info_path: cz_customize_info.txt + info: This is customized info + questions: + - type: list + name: change_type + choices: + - value: feature + name: 'feature: A new feature.' + - value: bug fix + name: 'bug fix: A bug fix.' + message: Select the type of change you are committing + - type: input + name: message + message: 'Body.' + - type: confirm + name: show_message + message: 'Do you want to add body message in commit?' +``` + +### Customize configuration + +| Parameter | Type | Default | Description | +| ------------------- | ------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `questions` | `Questions` | `None` | Questions regarding the commit message. Detailed below. The type `Questions` is an alias to `Iterable[MutableMapping[str, Any]]` which is defined in `commitizen.defaults`. It expects a list of dictionaries. | +| `message_template` | `str` | `None` | The template for generating message from the given answers. `message_template` should either follow [Jinja2][jinja2] formatting specification, and all the variables in this template should be defined in `name` in `questions` | +| `example` | `str` | `""` | (OPTIONAL) Provide an example to help understand the style. Used by `cz example`. | +| `schema` | `str` | `""` | (OPTIONAL) Show the schema used. Used by `cz schema`. | +| `schema_pattern` | `str` | `""` | (OPTIONAL) The regular expression used to do commit message validation. Used by `cz check`. | +| `info_path` | `str` | `""` | (OPTIONAL) The path to the file that contains explanation of the commit rules. Used by `cz info`. If not provided `cz info`, will load `info` instead. | +| `info` | `str` | `""` | (OPTIONAL) Explanation of the commit rules. Used by `cz info`. | +| `bump_map` | `dict` | `None` | (OPTIONAL) Dictionary mapping the extracted information to a `SemVer` increment type (`MAJOR`, `MINOR`, `PATCH`) | +| `bump_pattern` | `str` | `None` | (OPTIONAL) Regex to extract information from commit (subject and body) | +| `change_type_order`| `str` | `None` | (OPTIONAL) List of strings used to order the Changelog. All other types will be sorted alphabetically. Default is `["BREAKING CHANGE", "Feat", "Fix", "Refactor", "Perf"]` | +| `commit_parser` | `str` | `None` | (OPTIONAL) Regex to extract information used in creating changelog. [See more][changelog-spec] | +| `changelog_pattern` | `str` | `None` | (OPTIONAL) Regex to understand which commits to include in the changelog | +| `change_type_map` | `dict` | `None` | (OPTIONAL) Dictionary mapping the type of the commit to a changelog entry | + +[jinja2]: https://jinja.palletsprojects.com/en/2.10.x/ +[changelog-spec]: https://commitizen-tools.github.io/commitizen/changelog/ + +#### Detailed `questions` content + +| Parameter | Type | Default | Description | +| ----------- | ------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `type` | `str` | `None` | The type of questions. Valid type: `list`, `input` and etc. [See More][different-question-types] | +| `name` | `str` | `None` | The key for the value answered by user. It's used in `message_template` | +| `message` | `str` | `None` | Detail description for the question. | +| `choices` | `list` | `None` | (OPTIONAL) The choices when `type = list`. Either use a list of values or a list of dictionaries with `name` and `value` keys. Keyboard shortcuts can be defined via `key`. See examples above. | +| `default` | `Any` | `None` | (OPTIONAL) The default value for this question. | +| `filter` | `str` | `None` | (OPTIONAL) Validator for user's answer. **(Work in Progress)** | +| `multiline` | `bool` | `False` | (OPTIONAL) Enable multiline support when `type = input`. | +[different-question-types]: https://github.com/tmbo/questionary#different-question-types + +#### Shortcut keys + +When the [`use_shortcuts`](config.md#settings) config option is enabled, commitizen can show and use keyboard shortcuts to select items from lists directly. +For example, when using the `cz_conventional_commits` commitizen template, shortcut keys are shown when selecting the commit type. Unless otherwise defined, keyboard shortcuts will be numbered automatically. +To specify keyboard shortcuts for your custom choices, provide the shortcut using the `key` parameter in dictionary form for each choice you would like to customize. + +## 2. Customize through customizing a class + +The basic steps are: + +1. Inheriting from `BaseCommitizen` +2. Give a name to your rules. +3. Create a python package using `setup.py`, `poetry`, etc +4. Expose the class as a `commitizen.plugin` entrypoint + +Check an [example][convcomms] on how to configure `BaseCommitizen`. + +You can also automate the steps above through [cookiecutter](https://cookiecutter.readthedocs.io/en/1.7.0/). + +```sh +cookiecutter gh:commitizen-tools/commitizen_cz_template +``` + +See [commitizen_cz_template](https://github.com/commitizen-tools/commitizen_cz_template) for details. + +Once you publish your rules, you can send us a PR to the [Third-party section](./third-party-commitizen.md). + +### Custom commit rules + +Create a Python module, for example `cz_jira.py`. + +Inherit from `BaseCommitizen`, and you must define `questions` and `message`. The others are optional. + +```python +from commitizen.cz.base import BaseCommitizen +from commitizen.defaults import Questions + + +class JiraCz(BaseCommitizen): + # Questions = Iterable[MutableMapping[str, Any]] + # It expects a list with dictionaries. + def questions(self) -> Questions: + """Questions regarding the commit message.""" + questions = [ + {"type": "input", "name": "title", "message": "Commit title"}, + {"type": "input", "name": "issue", "message": "Jira Issue number:"}, + ] + return questions + + def message(self, answers: dict) -> str: + """Generate the message with the given answers.""" + return "{0} (#{1})".format(answers["title"], answers["issue"]) + + def example(self) -> str: + """Provide an example to help understand the style (OPTIONAL) + + Used by `cz example`. + """ + return "Problem with user (#321)" + + def schema(self) -> str: + """Show the schema used (OPTIONAL) + + Used by `cz schema`. + """ + return "<title> (<issue>)" + + def info(self) -> str: + """Explanation of the commit rules. (OPTIONAL) + + Used by `cz info`. + """ + return "We use this because is useful" +``` + +The next file required is `setup.py` modified from flask version. + +```python +from setuptools import setup + +setup( + name="JiraCommitizen", + version="0.1.0", + py_modules=["cz_jira"], + license="MIT", + long_description="this is a long description", + install_requires=["commitizen"], + entry_points={"commitizen.plugin": ["cz_jira = cz_jira:JiraCz"]}, +) +``` + +So in the end, we would have + + . + โ”œโ”€โ”€ cz_jira.py + โ””โ”€โ”€ setup.py + +And that's it. You can install it without uploading to pypi by simply +doing `pip install .` + +If you feel like it should be part of this repo, create a PR. + +### Custom bump rules + +You need to define 2 parameters inside your custom `BaseCommitizen`. + +| Parameter | Type | Default | Description | +| -------------- | ------ | ------- | ----------------------------------------------------------------------------------------------------- | +| `bump_pattern` | `str` | `None` | Regex to extract information from commit (subject and body) | +| `bump_map` | `dict` | `None` | Dictionary mapping the extracted information to a `SemVer` increment type (`MAJOR`, `MINOR`, `PATCH`) | + +Let's see an example. + +```python +from commitizen.cz.base import BaseCommitizen + + +class StrangeCommitizen(BaseCommitizen): + bump_pattern = r"^(break|new|fix|hotfix)" + bump_map = {"break": "MAJOR", "new": "MINOR", "fix": "PATCH", "hotfix": "PATCH"} +``` + +That's it, your commitizen now supports custom rules, and you can run. + +```bash +cz -n cz_strange bump +``` + +[convcomms]: https://github.com/commitizen-tools/commitizen/blob/master/commitizen/cz/conventional_commits/conventional_commits.py + +### Custom changelog generator + +The changelog generator should just work in a very basic manner without touching anything. +You can customize it of course, and this are the variables you need to add to your custom `BaseCommitizen`. + +| Parameter | Type | Required | Description | +| -------------------------------- | ------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `commit_parser` | `str` | NO | Regex which should provide the variables explained in the [changelog description][changelog-des] | +| `changelog_pattern` | `str` | NO | Regex to validate the commits, this is useful to skip commits that don't meet your ruling standards like a Merge. Usually the same as bump_pattern | +| `change_type_map` | `dict` | NO | Convert the title of the change type that will appear in the changelog, if a value is not found, the original will be provided | +| `changelog_message_builder_hook` | `method: (dict, git.GitCommit) -> dict | list | None` | NO | Customize with extra information your message output, like adding links, this function is executed per parsed commit. Each GitCommit contains the following attrs: `rev`, `title`, `body`, `author`, `author_email`. Returning a falsy value ignore the commit. | +| `changelog_hook` | `method: (full_changelog: str, partial_changelog: Optional[str]) -> str` | NO | Receives the whole and partial (if used incremental) changelog. Useful to send slack messages or notify a compliance department. Must return the full_changelog | +| `changelog_release_hook` | `method: (release: dict, tag: git.GitTag) -> dict` | NO | Receives each generated changelog release and its associated tag. Useful to enrich a releases before they are rendered. Must return the update release + +```python +from commitizen.cz.base import BaseCommitizen +import chat +import compliance + + +class StrangeCommitizen(BaseCommitizen): + changelog_pattern = r"^(break|new|fix|hotfix)" + commit_parser = r"^(?P<change_type>feat|fix|refactor|perf|BREAKING CHANGE)(?:\((?P<scope>[^()\r\n]*)\)|\()?(?P<breaking>!)?:\s(?P<message>.*)?" + change_type_map = { + "feat": "Features", + "fix": "Bug Fixes", + "refactor": "Code Refactor", + "perf": "Performance improvements", + } + + def changelog_message_builder_hook( + self, parsed_message: dict, commit: git.GitCommit + ) -> dict | list | None: + rev = commit.rev + m = parsed_message["message"] + parsed_message[ + "message" + ] = f"{m} {rev} [{commit.author}]({commit.author_email})" + return parsed_message + + def changelog_release_hook(self, release: dict, tag: git.GitTag) -> dict: + release["author"] = tag.author + return release + + def changelog_hook( + self, full_changelog: str, partial_changelog: Optional[str] + ) -> str: + """Executed at the end of the changelog generation + + full_changelog: it's the output about to being written into the file + partial_changelog: it's the new stuff, this is useful to send slack messages or + similar + + Return: + the new updated full_changelog + """ + if partial_changelog: + chat.room("#committers").notify(partial_changelog) + if full_changelog: + compliance.send(full_changelog) + full_changelog.replace(" fix ", " **fix** ") + return full_changelog +``` + +[changelog-des]: ./commands/changelog.md#description + +### Raise Customize Exception + +If you want `commitizen` to catch your exception and print the message, you'll have to inherit `CzException`. + +```python +from commitizen.cz.exception import CzException + + +class NoSubjectProvidedException(CzException): + ... +``` + +### Migrating from legacy plugin format + +Commitizen migrated to a new plugin format relying on `importlib.metadata.EntryPoint`. +Migration should be straight-forward for legacy plugins: + +- Remove the `discover_this` line from you plugin module +- Expose the plugin class under as a `commitizen.plugin` entrypoint. + +The name of the plugin is now determined by the name of the entrypoint. + +#### Example + +If you were having a `CzPlugin` class in a `cz_plugin.py` module like this: + +```python +from commitizen.cz.base import BaseCommitizen + + +class PluginCz(BaseCommitizen): + ... + + +discover_this = PluginCz +``` + +Then remove the `discover_this` line: + +```python +from commitizen.cz.base import BaseCommitizen + + +class PluginCz(BaseCommitizen): + ... +``` + +and expose the class as entrypoint in you setuptools: + +```python +from setuptools import setup + +setup( + name="MyPlugin", + version="0.1.0", + py_modules=["cz_plugin"], + entry_points={"commitizen.plugin": ["plugin = cz_plugin:PluginCz"]}, + ..., +) +``` + +Then your plugin will be available under the name `plugin`. + +## Customizing the changelog template + +Commitizen gives you the possibility to provide your own changelog template, by: + +- providing one with your customization class +- providing one from the current working directory and setting it: + - as [configuration][template-config] + - as `--template` parameter to both `bump` and `changelog` commands +- either by providing a template with the same name as the default template + +By default, the template used is the `CHANGELOG.md.j2` file from the commitizen repository. + +### Providing a template with your customization class + +There is 3 parameters available to change the template rendering from your custom `BaseCommitizen`. + +| Parameter | Type | Default | Description | +| ----------------- | ------ | ------- | ----------------------------------------------------------------------------------------------------- | +| `template` | `str` | `None` | Provide your own template name (default to `CHANGELOG.md.j2`) | +| `template_loader` | `str` | `None` | Override the default template loader (so you can provide template from you customization class) | +| `template_extras` | `dict` | `None` | Provide some extra template parameters | + +Let's see an example. + +```python +from commitizen.cz.base import BaseCommitizen +from jinja2 import PackageLoader + + +class MyPlugin(BaseCommitizen): + template = "CHANGELOG.md.jinja" + template_loader = PackageLoader("my_plugin", "templates") + template_extras = {"key": "value"} +``` + +This snippet will: + +- use `CHANGELOG.md.jinja` as template name +- search for it in the `templates` directory for `my_plugin` package +- add the `key=value` variable in the template + +### Providing a template from the current working directory + +Users can provides their own template from their current working directory (your project root) by: + +- providing a template with the same name (`CHANGELOG.md.j2` unless overridden by your custom class) +- setting your template path as `template` configuration +- giving your template path as `--template` parameter to `bump` and `changelog` commands + +!!! Note + The path is relative to the current working directory, aka. your project root most of the time. + +### Template variables + +The default template use a single `tree` variable which is a list of entries (a release) with the following format: + +| Name | Type | Description | +| ---- | ---- | ----------- | +| version | `str` | The release version | +| date | `datetime` | The release date | +| changes | `list[tuple[str, list[Change]]]` | The release sorted changes list in the form `(type, changes)` | + +Each `Change` has the following fields: + +| Name | Type | Description | +| ---- | ---- | ----------- | +| scope | `str | None` | An optional scope | +| message | `str` | The commit message body | +| sha1 | `str` | The commit `sha1` | +| parents | `list[str]` | The parent commit(s) `sha1`(s) | +| author | `str` | The commit author name | +| author_email | `str` | The commit author email | + +!!! Note + The field values depend on the customization class and/or the settings you provide + +The `parents` field can be used to identify merge commits and generate a changelog based on those. Another use case +is listing commits that belong to the same pull request. + +When using another template (either provided by a plugin or by yourself), you can also pass extra template variables +by: + +- defining them in your configuration with the [`extras` settings][extras-config] +- providing them on the commandline with the `--extra/-e` parameter to `bump` and `changelog` commands + +[template-config]: config.md#template +[extras-config]: config.md#extras diff --git a/docs/exit_codes.md b/docs/exit_codes.md new file mode 100644 index 0000000..af9cb83 --- /dev/null +++ b/docs/exit_codes.md @@ -0,0 +1,39 @@ +# Exit Codes + +Commitizen handles expected exceptions through `CommitizenException` and returns different exit codes for different situations. They could be useful if you want to ignore specific errors in your pipeline. + +These exit codes can be found in `commitizen/exceptions.py::ExitCode`. + +| Exception | Exit Code | Description | +| --------------------------- | --------- | ----------------------------------------------------------------------------------------------------------- | +| ExpectedExit | 0 | Expected exit | +| DryRunExit | 0 | Exit due to passing `--dry-run` option | +| NoCommitizenFoundException | 1 | Using a cz (e.g., `cz_jira`) that cannot be found in your system | +| NotAGitProjectError | 2 | Not in a git project | +| NoCommitsFoundError | 3 | No commit found | +| NoVersionSpecifiedError | 4 | Version can not be found in configuration file | +| NoPatternMapError | 5 | bump / changelog pattern or map can not be found in configuration file | +| BumpCommitFailedError | 6 | Commit error when bumping version | +| BumpTagFailedError | 7 | Tag error when bumping version | +| NoAnswersError | 8 | No user response given | +| CommitError | 9 | git commit error | +| NoCommitBackupError | 10 | Commit back up file cannot be found | +| NothingToCommitError | 11 | Nothing in staging to be committed | +| CustomError | 12 | `CzException` raised | +| NoCommandFoundError | 13 | No command found when running commitizen cli (e.g., `cz --debug`) | +| InvalidCommitMessageError | 14 | The commit message does not pass `cz check` | +| MissingConfigError | 15 | Configuration missed for `cz_customize` | +| NoRevisionError | 16 | No revision found | +| CurrentVersionNotFoundError | 17 | current version cannot be found in _version_files_ | +| InvalidCommandArgumentError | 18 | The argument provide to command is invalid (e.g. `cz check -commit-msg-file filename --rev-range master..`) | +| InvalidConfigurationError | 19 | An error was found in the Commitizen Configuration, such as duplicates in `change_type_order` | +| NotAllowed | 20 | `--incremental` cannot be combined with a `rev_range` | +| NoneIncrementExit | 21 | The commits found are not eligible to be bumped | +| CharacterSetDecodeError | 22 | The character encoding of the command output could not be determined | +| GitCommandError | 23 | Unexpected failure while calling a git command | +| InvalidManualVersion | 24 | Manually provided version is invalid | +| InitFailedError | 25 | Failed to initialize pre-commit | +| RunHookError | 26 | An error occurred during a hook execution | +| VersionProviderUnknown | 27 | `version_provider` setting is set to an unknown version provider identifier | +| VersionSchemeUnknown | 28 | `version_scheme` setting is set to an unknown version scheme identifier | +| ChangelogFormatUnknown | 29 | `changelog_format` setting is set to an unknown version scheme identifier or could not be guessed | diff --git a/docs/external_links.md b/docs/external_links.md new file mode 100644 index 0000000..388bcc8 --- /dev/null +++ b/docs/external_links.md @@ -0,0 +1,18 @@ +> If you have written over commitizen, make a PR and add the link here ๐Ÿ’ช + +## Talks + +| Name | Speaker | Occasion | Language | Extra | +| ------------------------------------------------------------------------- | --------------- | ---------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- | +| commitizen-tools: What can we gain from crafting a git message convention | Wei Lee | Taipey.py 2020 June Meetup, Remote Python Pizza 2020 | English | [slides](https://speakerdeck.com/leew/commitizen-tools-what-can-we-gain-from-crafting-a-git-message-convention-at-taipey-dot-py) | +| Automating release cycles | Santiago Fraire | PyAmsterdam June 24, 2020, Online | English | [slides](https://woile.github.io/commitizen-presentation/) | +| [Automatizando Releases con Commitizen y Github Actions][automatizando] | Santiago Fraire | PyConAr 2020, Remote | Espaรฑol | [slides](https://woile.github.io/automating-releases-github-actions-presentation/#/) | + +## Articles + +- [Python Table Manners - Commitizen: ่ฆๆ ผๅŒ– commit message](https://lee-w.github.io/posts/tech/2020/03/python-table-manners-commitizen/) (Written in Traditional Mandarin) +- [Automating semantic release with commitizen](https://woile.dev/posts/automating-semver-releases-with-commitizen/) (English) +- [How to Write Better Git Commit Messages โ€“ A Step-By-Step Guide](https://www.freecodecamp.org/news/how-to-write-better-git-commit-messages/?utm_source=tldrnewsletter) (English) +- [Continuous delivery made easy (in Python)](https://medium.com/dev-genius/continuous-delivery-made-easy-in-python-c085e9c82e69) + +[automatizando]: https://youtu.be/t3aE2M8UPBo diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..29d9f40 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,142 @@ +## Support for PEP621 + +PEP621 establishes a `[project]` definition inside `pyproject.toml` + +```toml +[project] +name = "spam" +version = "2.5.1" +``` + +Commitizen provides a [`pep621` version provider](config.md#version-providers) to get and set version from this field. +You just need to set the proper `version_provider` setting: + +```toml +[project] +name = "spam" +version = "2.5.1" + +[tool.commitizen] +version_provider = "pep621" +``` + +## Why are `revert` and `chore` valid types in the check pattern of cz conventional_commits but not types we can select? + +`revert` and `chore` are added to the "pattern" in `cz check` in order to prevent backward errors, but officially they are not part of conventional commits, we are using the latest [types from Angular](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#type) (they used to but were removed). +However, you can create a customized `cz` with those extra types. (See [Customization](customization.md)). + +See more discussion in issue [#142](https://github.com/commitizen-tools/commitizen/issues/142) and [#36](https://github.com/commitizen-tools/commitizen/issues/36) + +## How to revert a bump? + +If for any reason, the created tag and changelog were to be undone, this is the snippet: + +```sh +git tag --delete <created_tag> +git reset HEAD~ +git reset --hard HEAD +``` + +This will remove the last tag created, plus the commit containing the update to `.cz.toml` and the changelog generated for the version. + +In case the commit was pushed to the server you can remove it by running + +```sh +git push --delete origin <created_tag> +``` + +## Is this project affiliated with the Commitizen JS project? + +It is not affiliated. + +Both are used for similar purposes, parsing commits, generating changelog and version we presume. +This one is written in python to make integration easier for python projects and the other serves the JS packages. + +They differ a bit in design, not sure if cz-js does any of this, but these are some of the stuff you can do with this repo (python's commitizen): + +- create custom rules, version bumps and changelog generation, by default we use the popular conventional commits (I think cz-js allows this). +- single package, install one thing and it will work (cz-js is a monorepo, but you have to install different dependencies AFAIK) +- pre-commit integration +- works on any language project, as long as you create the `.cz.toml` or `cz.toml` file. + +Where do they cross paths? + +If you are using conventional commits in your git history, then you could swap one with the other in theory. + +Regarding the name, [cz-js][cz-js] came first, they used the word commitizen first. When this project was created originally, the creator read "be a good commitizen", and thought it was just a cool word that made sense, and this would be a package that helps you be a good "commit citizen". + +[cz-js]: https://github.com/commitizen/cz-cli + +## How to handle revert commits? + +```sh +git revert --no-commit <SHA> +git commit -m "revert: foo bar" +``` + +## I got `Exception [WinError 995] The I/O operation ...` error + +This error was caused by a Python bug on Windows. It's been fixed by [this PR](https://github.com/python/cpython/pull/22017), and according to Python's changelog, [3.8.6rc1](https://docs.python.org/3.8/whatsnew/changelog.html#python-3-8-6-release-candidate-1) and [3.9.0rc2](https://docs.python.org/3.9/whatsnew/changelog.html#python-3-9-0-release-candidate-2) should be the accurate versions first contain this fix. In conclusion, upgrade your Python version might solve this issue. + +More discussion can be found in issue [#318](https://github.com/commitizen-tools/commitizen/issues/318). + +## Why does commitizen not support CalVer? + +`commitizen` could support CalVer alongside SemVer, but in practice implementing CalVer +creates numerous edge cases that are difficult to maintain ([#385]) and more generally +mixing the two version schemes may not be a good idea. If CalVer or other custom +versioning scheme is needed, `commitizen` could still be used to standardize commits +and create changelogs, but a separate package should be used for version increments. + +Mixing CalVer and SemVer is generally not recommended because each versioning scheme +serves a different purposes. Diverging from either specification can be confusing to +users and cause errors with third party tools that don't expect the non-standard format. + +In the future, `commitizen` may support some implementation of CalVer, but at the time +of writing, there are no plans to implement the feature ([#173]). + +If you would like to learn more about both schemes, there are plenty of good resources: + +- [Announcing CalVer](https://sedimental.org/calver.html) +- [API Versioning from Stripe](https://stripe.com/blog/api-versioning) +- [Discussion about pip's use of CalVer](https://github.com/pypa/pip/issues/5645#issuecomment-407192448) +- [Git Version Numbering](https://code.erpenbeck.io/git/2021/12/16/git-version-numbering/) +- [SemVer vs. CalVer and Why I Use Both](https://mikestaszel.com/2021/04/03/semver-vs-calver-and-why-i-use-both/) (but not at the same time) +- [Semver Will Not Save You](https://hynek.me/articles/semver-will-not-save-you/) +- [Why I Don't Like SemVer](https://snarky.ca/why-i-dont-like-semver/) + +[#173]: https://github.com/commitizen-tools/commitizen/issues/173 +[#385]: https://github.com/commitizen-tools/commitizen/pull/385 + +## How to change the tag format ? + +You can use the [`legacy_tag_formats`](config.md#legacy_tag_formats) to list old tag formats. +New bumped tags will be in the new format but old ones will still work for: +- changelog generation (full, incremental and version range) +- bump new version computation (automatically guessed or increment given) + + +So given if you change from `myproject-$version` to `${version}` and then `v${version}`, +your commitizen configuration will look like this: + +```toml +tag_format = "v${version}" +legacy_tag_formats = [ + "${version}", + "myproject-$version", +] +``` + +## How to avoid warnings for expected non-version tags + +You can explicitly ignore them with [`ignored_tag_formats`](config.md#ignored_tag_formats). + +```toml +tag_format = "v${version}" +ignored_tag_formats = [ + "stable", + "component-*", + "env/*", + "v${major}.${minor}", +] +``` diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 0000000..3c6257c --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,119 @@ +## Initialize commitizen + +If it's your first time, you'll need to create a commitizen configuration file. + +The assistant utility will help you set up everything + +```sh +cz init +``` + +Alternatively, create a file `.cz.toml` or `cz.toml` in your project's directory. + +```toml +[tool.commitizen] +version = "0.1.0" +update_changelog_on_bump = true +``` + +## Usage + +### Bump version + +```sh +cz bump +``` + +This command will bump your project's version, and it will create a tag. + +Because of the setting `update_changelog_on_bump`, bump will also create the **changelog**. +You can also [update files](./commands/bump.md#version_files). +You can configure the [version scheme](./commands/bump.md#version_scheme) and [version provider](./config.md#version-providers). + +There are many more options available, please read the docs for the [bump command](./commands/bump.md). + +### Committing + +Run in your terminal + +```bash +cz commit +``` + +or the shortcut + +```bash +cz c +``` + +#### Sign off the commit + +Run in the terminal + +```bash +cz commit -- --signoff +``` + +or the shortcut + +```bash +cz commit -- -s +``` + +### Get project version + +Running `cz version` will return the version of commitizen, but if you want +your project's version you can run: + +```sh +cz version -p +``` + +This can be useful in many situations, where otherwise, you would require a way +to parse the version of your project. Maybe it's simple if you use a `VERSION` file, +but once you start working with many different projects, it becomes tricky. + +A common example is, when you need to send to slack, the changes for the version that you +just created: + +```sh +cz changelog --dry-run "$(cz version -p)" +``` + +### Integration with Pre-commit + +Commitizen can lint your commit message for you with `cz check`. + +You can integrate this in your [pre-commit](https://pre-commit.com/) config with: + +```yaml +--- +repos: + - repo: https://github.com/commitizen-tools/commitizen + rev: master + hooks: + - id: commitizen + - id: commitizen-branch + stages: [pre-push] +``` + +After the configuration is added, you'll need to run: + +```sh +pre-commit install --hook-type commit-msg --hook-type pre-push +``` + +If you aren't using both hooks, you needn't install both stages. + +| Hook | Recommended Stage | +| ----------------- | ----------------- | +| commitizen | commit-msg | +| commitizen-branch | pre-push | + +Note that pre-commit discourages using `master` as a revision, and the above command will print a warning. You should replace the `master` revision with the [latest tag](https://github.com/commitizen-tools/commitizen/tags). This can be done automatically with: + +```sh +pre-commit autoupdate +``` + +Read more about the `check` command [here](commands/check.md). diff --git a/docs/images/bump.yml b/docs/images/bump.yml new file mode 100644 index 0000000..ea77a2f --- /dev/null +++ b/docs/images/bump.yml @@ -0,0 +1,195 @@ +# The configurations that used for the recording, feel free to edit them +config: + + # Specify a command to be executed + # like `/bin/bash -l`, `ls`, or any other commands + # the default is bash for Linux + # or powershell.exe for Windows + command: bash -l + + # Specify the current working directory path + # the default is the current working directory path + cwd: ~/my-project + + # Export additional ENV variables + env: + recording: true + + # Explicitly set the number of columns + # or use `auto` to take the current + # number of columns of your shell + cols: 80 + + # Explicitly set the number of rows + # or use `auto` to take the current + # number of rows of your shell + rows: 20 + + # Amount of times to repeat GIF + # If value is -1, play once + # If value is 0, loop indefinitely + # If value is a positive number, loop n times + repeat: 0 + + # Quality + # 1 - 100 + quality: 85 + + # Delay between frames in ms + # If the value is `auto` use the actual recording delays + frameDelay: auto + + # Maximum delay between frames in ms + # Ignored if the `frameDelay` isn't set to `auto` + # Set to `auto` to prevent limiting the max idle time + maxIdleTime: 2000 + + # The surrounding frame box + # The `type` can be null, window, floating, or solid` + # To hide the title use the value null + # Don't forget to add a backgroundColor style with a null as type + frameBox: + type: floating + title: "Commitizen: bump" + style: + border: 0px black solid + # boxShadow: none + # margin: 0px + + # Add a watermark image to the rendered gif + # You need to specify an absolute path for + # the image on your machine or a URL, and you can also + # add your own CSS styles + watermark: + imagePath: null + style: + position: absolute + right: 15px + bottom: 15px + width: 100px + opacity: 0.9 + + # Cursor style can be one of + # `block`, `underline`, or `bar` + cursorStyle: block + + # Font family + # You can use any font that is installed on your machine + # in CSS-like syntax + fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace" + + # The size of the font + fontSize: 16 + + # The height of lines + lineHeight: 1 + + # The spacing between letters + letterSpacing: 0 + + # Theme + theme: + background: "transparent" + foreground: "#afafaf" + cursor: "#c7c7c7" + black: "#232628" + red: "#fc4384" + green: "#b3e33b" + yellow: "#ffa727" + blue: "#75dff2" + magenta: "#ae89fe" + cyan: "#708387" + white: "#d5d5d0" + brightBlack: "#626566" + brightRed: "#ff7fac" + brightGreen: "#c8ed71" + brightYellow: "#ebdf86" + brightBlue: "#75dff2" + brightMagenta: "#ae89fe" + brightCyan: "#b1c6ca" + brightWhite: "#f9f9f4" + +# Records, feel free to edit them +records: + - delay: 2295 + content: "\e[1;33m\e[0;32m\e[1;34m\e[1;32msantiago\e[1;34m@\e[1;31mhome\e[1;37m in \e[1;34m~/my-project\e[0;36m |master=|\e[1;32m $\r\r\n\e[1;32m$\e[00m " + - delay: 662 + content: c + - delay: 70 + content: z + - delay: 111 + content: ' ' + - delay: 253 + content: '-' + - delay: 112 + content: '-' + - delay: 112 + content: v + - delay: 122 + content: e + - delay: 280 + content: r + - delay: 202 + content: s + - delay: 106 + content: i + - delay: 55 + content: o + - delay: 298 + content: 'n' + - delay: 273 + content: "\r\n" + - delay: 1121 + content: "1.1.0\r\n\e[0m" + - delay: 161 + content: "\e[1;33m\e[0;32m\e[1;34m\e[1;32msantiago\e[1;34m@\e[1;31mhome\e[1;37m in \e[1;34m~/my-project\e[0;36m |master=|\e[1;32m $\r\r\n\e[1;32m$\e[00m " + - delay: 667 + content: c + - delay: 95 + content: z + - delay: 147 + content: ' ' + - delay: 150 + content: b + - delay: 114 + content: u + - delay: 180 + content: m + - delay: 132 + content: p + - delay: 239 + content: "\r\n" + - delay: 1176 + content: "bump: version 1.1.0 โ†’ 1.1.1\r\ntag to create: v1.1.1\r\nincrement detected: PATCH\r\n" + - delay: 75 + content: "\e[32mDone!\e[0m\r\n\e[0m" + - delay: 183 + content: "\e[1;33m\e[0;32m\e[1;34m\e[1;32msantiago\e[1;34m@\e[1;31mhome\e[1;37m in \e[1;34m~/my-project\e[0;36m |master>|\e[1;32m $\r\r\n\e[1;32m$\e[00m " + - delay: 1500 + content: c + - delay: 70 + content: z + - delay: 44 + content: ' ' + - delay: 161 + content: '-' + - delay: 119 + content: '-' + - delay: 87 + content: v + - delay: 133 + content: e + - delay: 92 + content: r + - delay: 89 + content: s + - delay: 87 + content: i + - delay: 45 + content: o + - delay: 200 + content: 'n' + - delay: 199 + content: "\r\n" + - delay: 1120 + content: "1.1.1\r\n\e[0m" diff --git a/docs/images/cli_help/cz___help.svg b/docs/images/cli_help/cz___help.svg new file mode 100644 index 0000000..098e7df --- /dev/null +++ b/docs/images/cli_help/cz___help.svg @@ -0,0 +1,202 @@ +<svg class="rich-terminal" viewBox="0 0 994 928.4" xmlns="http://www.w3.org/2000/svg"> + <!-- Generated with Rich https://www.textualize.io --> + <style> + + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Regular"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); + font-style: normal; + font-weight: 400; + } + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Bold"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); + font-style: bold; + font-weight: 700; + } + + .terminal-2205183093-matrix { + font-family: Fira Code, monospace; + font-size: 20px; + line-height: 24.4px; + font-variant-east-asian: full-width; + } + + .terminal-2205183093-title { + font-size: 18px; + font-weight: bold; + font-family: arial; + } + + .terminal-2205183093-r1 { fill: #c5c8c6 } +.terminal-2205183093-r2 { fill: #c5c8c6;font-weight: bold } +.terminal-2205183093-r3 { fill: #d0b344 } +.terminal-2205183093-r4 { fill: #1984e9;text-decoration: underline; } +.terminal-2205183093-r5 { fill: #68a0b3;font-weight: bold } + </style> + + <defs> + <clipPath id="terminal-2205183093-clip-terminal"> + <rect x="0" y="0" width="975.0" height="877.4" /> + </clipPath> + <clipPath id="terminal-2205183093-line-0"> + <rect x="0" y="1.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-1"> + <rect x="0" y="25.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-2"> + <rect x="0" y="50.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-3"> + <rect x="0" y="74.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-4"> + <rect x="0" y="99.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-5"> + <rect x="0" y="123.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-6"> + <rect x="0" y="147.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-7"> + <rect x="0" y="172.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-8"> + <rect x="0" y="196.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-9"> + <rect x="0" y="221.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-10"> + <rect x="0" y="245.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-11"> + <rect x="0" y="269.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-12"> + <rect x="0" y="294.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-13"> + <rect x="0" y="318.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-14"> + <rect x="0" y="343.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-15"> + <rect x="0" y="367.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-16"> + <rect x="0" y="391.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-17"> + <rect x="0" y="416.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-18"> + <rect x="0" y="440.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-19"> + <rect x="0" y="465.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-20"> + <rect x="0" y="489.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-21"> + <rect x="0" y="513.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-22"> + <rect x="0" y="538.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-23"> + <rect x="0" y="562.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-24"> + <rect x="0" y="587.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-25"> + <rect x="0" y="611.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-26"> + <rect x="0" y="635.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-27"> + <rect x="0" y="660.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-28"> + <rect x="0" y="684.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-29"> + <rect x="0" y="709.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-30"> + <rect x="0" y="733.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-31"> + <rect x="0" y="757.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-32"> + <rect x="0" y="782.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-33"> + <rect x="0" y="806.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2205183093-line-34"> + <rect x="0" y="831.1" width="976" height="24.65"/> + </clipPath> + </defs> + + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="926.4" rx="8"/> + <g transform="translate(26,22)"> + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> + <circle cx="22" cy="0" r="7" fill="#febc2e"/> + <circle cx="44" cy="0" r="7" fill="#28c840"/> + </g> + + <g transform="translate(9, 41)" clip-path="url(#terminal-2205183093-clip-terminal)"> + + <g class="terminal-2205183093-matrix"> + <text class="terminal-2205183093-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-2205183093-line-0)">$ cz --help</text><text class="terminal-2205183093-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-2205183093-line-0)"> +</text><text class="terminal-2205183093-r1" x="0" y="44.4" textLength="122" clip-path="url(#terminal-2205183093-line-1)">usage: cz </text><text class="terminal-2205183093-r2" x="122" y="44.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-1)">[</text><text class="terminal-2205183093-r1" x="134.2" y="44.4" textLength="24.4" clip-path="url(#terminal-2205183093-line-1)">-h</text><text class="terminal-2205183093-r2" x="158.6" y="44.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-1)">]</text><text class="terminal-2205183093-r2" x="183" y="44.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-1)">[</text><text class="terminal-2205183093-r1" x="195.2" y="44.4" textLength="183" clip-path="url(#terminal-2205183093-line-1)">--config CONFIG</text><text class="terminal-2205183093-r2" x="378.2" y="44.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-1)">]</text><text class="terminal-2205183093-r2" x="402.6" y="44.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-1)">[</text><text class="terminal-2205183093-r1" x="414.8" y="44.4" textLength="85.4" clip-path="url(#terminal-2205183093-line-1)">--debug</text><text class="terminal-2205183093-r2" x="500.2" y="44.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-1)">]</text><text class="terminal-2205183093-r2" x="524.6" y="44.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-1)">[</text><text class="terminal-2205183093-r1" x="536.8" y="44.4" textLength="85.4" clip-path="url(#terminal-2205183093-line-1)">-n NAME</text><text class="terminal-2205183093-r2" x="622.2" y="44.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-1)">]</text><text class="terminal-2205183093-r2" x="646.6" y="44.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-1)">[</text><text class="terminal-2205183093-r1" x="658.8" y="44.4" textLength="146.4" clip-path="url(#terminal-2205183093-line-1)">-nr NO_RAISE</text><text class="terminal-2205183093-r2" x="805.2" y="44.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-1)">]</text><text class="terminal-2205183093-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-1)"> +</text><text class="terminal-2205183093-r2" x="122" y="68.8" textLength="12.2" clip-path="url(#terminal-2205183093-line-2)">{</text><text class="terminal-2205183093-r1" x="134.2" y="68.8" textLength="829.6" clip-path="url(#terminal-2205183093-line-2)">init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version</text><text class="terminal-2205183093-r2" x="963.8" y="68.8" textLength="12.2" clip-path="url(#terminal-2205183093-line-2)">}</text><text class="terminal-2205183093-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-2205183093-line-2)"> +</text><text class="terminal-2205183093-r3" x="0" y="93.2" textLength="36.6" clip-path="url(#terminal-2205183093-line-3)">...</text><text class="terminal-2205183093-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-2205183093-line-3)"> +</text><text class="terminal-2205183093-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-2205183093-line-4)"> +</text><text class="terminal-2205183093-r1" x="0" y="142" textLength="707.6" clip-path="url(#terminal-2205183093-line-5)">Commitizen is a cli tool to generate conventional commits.</text><text class="terminal-2205183093-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-2205183093-line-5)"> +</text><text class="terminal-2205183093-r1" x="0" y="166.4" textLength="524.6" clip-path="url(#terminal-2205183093-line-6)">For more information about the topic go to </text><text class="terminal-2205183093-r4" x="524.6" y="166.4" textLength="390.4" clip-path="url(#terminal-2205183093-line-6)">https://conventionalcommits.org/</text><text class="terminal-2205183093-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-6)"> +</text><text class="terminal-2205183093-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-2205183093-line-7)"> +</text><text class="terminal-2205183093-r1" x="0" y="215.2" textLength="97.6" clip-path="url(#terminal-2205183093-line-8)">options:</text><text class="terminal-2205183093-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-2205183093-line-8)"> +</text><text class="terminal-2205183093-r1" x="0" y="239.6" textLength="671" clip-path="url(#terminal-2205183093-line-9)">  -h, --help            show this help message and exit</text><text class="terminal-2205183093-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-2205183093-line-9)"> +</text><text class="terminal-2205183093-r1" x="0" y="264" textLength="658.8" clip-path="url(#terminal-2205183093-line-10)">  --config CONFIG       the path of configuration file</text><text class="terminal-2205183093-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-2205183093-line-10)"> +</text><text class="terminal-2205183093-r1" x="0" y="288.4" textLength="463.6" clip-path="url(#terminal-2205183093-line-11)">  --debug               use debug mode</text><text class="terminal-2205183093-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-11)"> +</text><text class="terminal-2205183093-r1" x="0" y="312.8" textLength="597.8" clip-path="url(#terminal-2205183093-line-12)">  -n, --name NAME       use the given commitizen </text><text class="terminal-2205183093-r2" x="597.8" y="312.8" textLength="12.2" clip-path="url(#terminal-2205183093-line-12)">(</text><text class="terminal-2205183093-r1" x="610" y="312.8" textLength="97.6" clip-path="url(#terminal-2205183093-line-12)">default:</text><text class="terminal-2205183093-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-2205183093-line-12)"> +</text><text class="terminal-2205183093-r1" x="0" y="337.2" textLength="573.4" clip-path="url(#terminal-2205183093-line-13)">                        cz_conventional_commits</text><text class="terminal-2205183093-r2" x="573.4" y="337.2" textLength="12.2" clip-path="url(#terminal-2205183093-line-13)">)</text><text class="terminal-2205183093-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-2205183093-line-13)"> +</text><text class="terminal-2205183093-r1" x="0" y="361.6" textLength="317.2" clip-path="url(#terminal-2205183093-line-14)">  -nr, --no-raise NO_RAISE</text><text class="terminal-2205183093-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-2205183093-line-14)"> +</text><text class="terminal-2205183093-r1" x="0" y="386" textLength="902.8" clip-path="url(#terminal-2205183093-line-15)">                        comma separated error codes that won't rise error,</text><text class="terminal-2205183093-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-2205183093-line-15)"> +</text><text class="terminal-2205183093-r1" x="0" y="410.4" textLength="439.2" clip-path="url(#terminal-2205183093-line-16)">                        e.g: cz -nr </text><text class="terminal-2205183093-r5" x="439.2" y="410.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-16)">1</text><text class="terminal-2205183093-r1" x="451.4" y="410.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-16)">,</text><text class="terminal-2205183093-r5" x="463.6" y="410.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-16)">2</text><text class="terminal-2205183093-r1" x="475.8" y="410.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-16)">,</text><text class="terminal-2205183093-r5" x="488" y="410.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-16)">3</text><text class="terminal-2205183093-r1" x="500.2" y="410.4" textLength="231.8" clip-path="url(#terminal-2205183093-line-16)"> bump. See codes at</text><text class="terminal-2205183093-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-16)"> +</text><text class="terminal-2205183093-r4" x="292.8" y="434.8" textLength="231.8" clip-path="url(#terminal-2205183093-line-17)">https://commitizen-</text><text class="terminal-2205183093-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-2205183093-line-17)"> +</text><text class="terminal-2205183093-r1" x="0" y="459.2" textLength="756.4" clip-path="url(#terminal-2205183093-line-18)">                        tools.github.io/commitizen/exit_codes/</text><text class="terminal-2205183093-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-2205183093-line-18)"> +</text><text class="terminal-2205183093-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-2205183093-line-19)"> +</text><text class="terminal-2205183093-r1" x="0" y="508" textLength="109.8" clip-path="url(#terminal-2205183093-line-20)">commands:</text><text class="terminal-2205183093-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-2205183093-line-20)"> +</text><text class="terminal-2205183093-r2" x="24.4" y="532.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-21)">{</text><text class="terminal-2205183093-r1" x="36.6" y="532.4" textLength="829.6" clip-path="url(#terminal-2205183093-line-21)">init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version</text><text class="terminal-2205183093-r2" x="866.2" y="532.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-21)">}</text><text class="terminal-2205183093-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-21)"> +</text><text class="terminal-2205183093-r1" x="0" y="556.8" textLength="646.6" clip-path="url(#terminal-2205183093-line-22)">    init                init commitizen configuration</text><text class="terminal-2205183093-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-2205183093-line-22)"> +</text><text class="terminal-2205183093-r1" x="0" y="581.2" textLength="134.2" clip-path="url(#terminal-2205183093-line-23)">    commit </text><text class="terminal-2205183093-r2" x="134.2" y="581.2" textLength="12.2" clip-path="url(#terminal-2205183093-line-23)">(</text><text class="terminal-2205183093-r1" x="146.4" y="581.2" textLength="12.2" clip-path="url(#terminal-2205183093-line-23)">c</text><text class="terminal-2205183093-r2" x="158.6" y="581.2" textLength="12.2" clip-path="url(#terminal-2205183093-line-23)">)</text><text class="terminal-2205183093-r1" x="170.8" y="581.2" textLength="329.4" clip-path="url(#terminal-2205183093-line-23)">          create new commit</text><text class="terminal-2205183093-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-2205183093-line-23)"> +</text><text class="terminal-2205183093-r1" x="0" y="605.6" textLength="610" clip-path="url(#terminal-2205183093-line-24)">    ls                  show available commitizens</text><text class="terminal-2205183093-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-2205183093-line-24)"> +</text><text class="terminal-2205183093-r1" x="0" y="630" textLength="524.6" clip-path="url(#terminal-2205183093-line-25)">    example             show commit example</text><text class="terminal-2205183093-r1" x="976" y="630" textLength="12.2" clip-path="url(#terminal-2205183093-line-25)"> +</text><text class="terminal-2205183093-r1" x="0" y="654.4" textLength="646.6" clip-path="url(#terminal-2205183093-line-26)">    info                show information about the cz</text><text class="terminal-2205183093-r1" x="976" y="654.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-26)"> +</text><text class="terminal-2205183093-r1" x="0" y="678.8" textLength="512.4" clip-path="url(#terminal-2205183093-line-27)">    schema              show commit schema</text><text class="terminal-2205183093-r1" x="976" y="678.8" textLength="12.2" clip-path="url(#terminal-2205183093-line-27)"> +</text><text class="terminal-2205183093-r1" x="0" y="703.2" textLength="805.2" clip-path="url(#terminal-2205183093-line-28)">    bump                bump semantic version based on the git log</text><text class="terminal-2205183093-r1" x="976" y="703.2" textLength="12.2" clip-path="url(#terminal-2205183093-line-28)"> +</text><text class="terminal-2205183093-r1" x="0" y="727.6" textLength="170.8" clip-path="url(#terminal-2205183093-line-29)">    changelog </text><text class="terminal-2205183093-r2" x="170.8" y="727.6" textLength="12.2" clip-path="url(#terminal-2205183093-line-29)">(</text><text class="terminal-2205183093-r1" x="183" y="727.6" textLength="24.4" clip-path="url(#terminal-2205183093-line-29)">ch</text><text class="terminal-2205183093-r2" x="207.4" y="727.6" textLength="12.2" clip-path="url(#terminal-2205183093-line-29)">)</text><text class="terminal-2205183093-r1" x="219.6" y="727.6" textLength="305" clip-path="url(#terminal-2205183093-line-29)">      generate changelog </text><text class="terminal-2205183093-r2" x="524.6" y="727.6" textLength="12.2" clip-path="url(#terminal-2205183093-line-29)">(</text><text class="terminal-2205183093-r1" x="536.8" y="727.6" textLength="329.4" clip-path="url(#terminal-2205183093-line-29)">note that it will overwrite</text><text class="terminal-2205183093-r1" x="976" y="727.6" textLength="12.2" clip-path="url(#terminal-2205183093-line-29)"> +</text><text class="terminal-2205183093-r1" x="0" y="752" textLength="451.4" clip-path="url(#terminal-2205183093-line-30)">                        existing file</text><text class="terminal-2205183093-r2" x="451.4" y="752" textLength="12.2" clip-path="url(#terminal-2205183093-line-30)">)</text><text class="terminal-2205183093-r1" x="976" y="752" textLength="12.2" clip-path="url(#terminal-2205183093-line-30)"> +</text><text class="terminal-2205183093-r1" x="0" y="776.4" textLength="951.6" clip-path="url(#terminal-2205183093-line-31)">    check               validates that a commit message matches the commitizen</text><text class="terminal-2205183093-r1" x="976" y="776.4" textLength="12.2" clip-path="url(#terminal-2205183093-line-31)"> +</text><text class="terminal-2205183093-r1" x="0" y="800.8" textLength="366" clip-path="url(#terminal-2205183093-line-32)">                        schema</text><text class="terminal-2205183093-r1" x="976" y="800.8" textLength="12.2" clip-path="url(#terminal-2205183093-line-32)"> +</text><text class="terminal-2205183093-r1" x="0" y="825.2" textLength="902.8" clip-path="url(#terminal-2205183093-line-33)">    version             get the version of the installed commitizen or the</text><text class="terminal-2205183093-r1" x="976" y="825.2" textLength="12.2" clip-path="url(#terminal-2205183093-line-33)"> +</text><text class="terminal-2205183093-r1" x="0" y="849.6" textLength="488" clip-path="url(#terminal-2205183093-line-34)">                        current project </text><text class="terminal-2205183093-r2" x="488" y="849.6" textLength="12.2" clip-path="url(#terminal-2205183093-line-34)">(</text><text class="terminal-2205183093-r1" x="500.2" y="849.6" textLength="353.8" clip-path="url(#terminal-2205183093-line-34)">default: installed commitizen</text><text class="terminal-2205183093-r2" x="854" y="849.6" textLength="12.2" clip-path="url(#terminal-2205183093-line-34)">)</text><text class="terminal-2205183093-r1" x="976" y="849.6" textLength="12.2" clip-path="url(#terminal-2205183093-line-34)"> +</text><text class="terminal-2205183093-r1" x="976" y="874" textLength="12.2" clip-path="url(#terminal-2205183093-line-35)"> +</text> + </g> + </g> +</svg> diff --git a/docs/images/cli_help/cz_bump___help.svg b/docs/images/cli_help/cz_bump___help.svg new file mode 100644 index 0000000..7f27636 --- /dev/null +++ b/docs/images/cli_help/cz_bump___help.svg @@ -0,0 +1,389 @@ +<svg class="rich-terminal" viewBox="0 0 994 2075.2" xmlns="http://www.w3.org/2000/svg"> + <!-- Generated with Rich https://www.textualize.io --> + <style> + + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Regular"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); + font-style: normal; + font-weight: 400; + } + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Bold"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); + font-style: bold; + font-weight: 700; + } + + .terminal-243650528-matrix { + font-family: Fira Code, monospace; + font-size: 20px; + line-height: 24.4px; + font-variant-east-asian: full-width; + } + + .terminal-243650528-title { + font-size: 18px; + font-weight: bold; + font-family: arial; + } + + .terminal-243650528-r1 { fill: #c5c8c6 } +.terminal-243650528-r2 { fill: #c5c8c6;font-weight: bold } +.terminal-243650528-r3 { fill: #68a0b3;font-weight: bold } +.terminal-243650528-r4 { fill: #98a84b } + </style> + + <defs> + <clipPath id="terminal-243650528-clip-terminal"> + <rect x="0" y="0" width="975.0" height="2024.1999999999998" /> + </clipPath> + <clipPath id="terminal-243650528-line-0"> + <rect x="0" y="1.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-1"> + <rect x="0" y="25.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-2"> + <rect x="0" y="50.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-3"> + <rect x="0" y="74.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-4"> + <rect x="0" y="99.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-5"> + <rect x="0" y="123.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-6"> + <rect x="0" y="147.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-7"> + <rect x="0" y="172.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-8"> + <rect x="0" y="196.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-9"> + <rect x="0" y="221.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-10"> + <rect x="0" y="245.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-11"> + <rect x="0" y="269.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-12"> + <rect x="0" y="294.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-13"> + <rect x="0" y="318.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-14"> + <rect x="0" y="343.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-15"> + <rect x="0" y="367.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-16"> + <rect x="0" y="391.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-17"> + <rect x="0" y="416.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-18"> + <rect x="0" y="440.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-19"> + <rect x="0" y="465.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-20"> + <rect x="0" y="489.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-21"> + <rect x="0" y="513.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-22"> + <rect x="0" y="538.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-23"> + <rect x="0" y="562.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-24"> + <rect x="0" y="587.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-25"> + <rect x="0" y="611.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-26"> + <rect x="0" y="635.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-27"> + <rect x="0" y="660.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-28"> + <rect x="0" y="684.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-29"> + <rect x="0" y="709.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-30"> + <rect x="0" y="733.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-31"> + <rect x="0" y="757.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-32"> + <rect x="0" y="782.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-33"> + <rect x="0" y="806.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-34"> + <rect x="0" y="831.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-35"> + <rect x="0" y="855.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-36"> + <rect x="0" y="879.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-37"> + <rect x="0" y="904.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-38"> + <rect x="0" y="928.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-39"> + <rect x="0" y="953.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-40"> + <rect x="0" y="977.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-41"> + <rect x="0" y="1001.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-42"> + <rect x="0" y="1026.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-43"> + <rect x="0" y="1050.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-44"> + <rect x="0" y="1075.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-45"> + <rect x="0" y="1099.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-46"> + <rect x="0" y="1123.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-47"> + <rect x="0" y="1148.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-48"> + <rect x="0" y="1172.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-49"> + <rect x="0" y="1197.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-50"> + <rect x="0" y="1221.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-51"> + <rect x="0" y="1245.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-52"> + <rect x="0" y="1270.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-53"> + <rect x="0" y="1294.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-54"> + <rect x="0" y="1319.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-55"> + <rect x="0" y="1343.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-56"> + <rect x="0" y="1367.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-57"> + <rect x="0" y="1392.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-58"> + <rect x="0" y="1416.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-59"> + <rect x="0" y="1441.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-60"> + <rect x="0" y="1465.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-61"> + <rect x="0" y="1489.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-62"> + <rect x="0" y="1514.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-63"> + <rect x="0" y="1538.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-64"> + <rect x="0" y="1563.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-65"> + <rect x="0" y="1587.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-66"> + <rect x="0" y="1611.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-67"> + <rect x="0" y="1636.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-68"> + <rect x="0" y="1660.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-69"> + <rect x="0" y="1685.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-70"> + <rect x="0" y="1709.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-71"> + <rect x="0" y="1733.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-72"> + <rect x="0" y="1758.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-73"> + <rect x="0" y="1782.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-74"> + <rect x="0" y="1807.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-75"> + <rect x="0" y="1831.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-76"> + <rect x="0" y="1855.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-77"> + <rect x="0" y="1880.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-78"> + <rect x="0" y="1904.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-79"> + <rect x="0" y="1929.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-80"> + <rect x="0" y="1953.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-243650528-line-81"> + <rect x="0" y="1977.9" width="976" height="24.65"/> + </clipPath> + </defs> + + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="2073.2" rx="8"/> + <g transform="translate(26,22)"> + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> + <circle cx="22" cy="0" r="7" fill="#febc2e"/> + <circle cx="44" cy="0" r="7" fill="#28c840"/> + </g> + + <g transform="translate(9, 41)" clip-path="url(#terminal-243650528-clip-terminal)"> + + <g class="terminal-243650528-matrix"> + <text class="terminal-243650528-r1" x="0" y="20" textLength="195.2" clip-path="url(#terminal-243650528-line-0)">$ cz bump --help</text><text class="terminal-243650528-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-243650528-line-0)"> +</text><text class="terminal-243650528-r1" x="0" y="44.4" textLength="183" clip-path="url(#terminal-243650528-line-1)">usage: cz bump </text><text class="terminal-243650528-r2" x="183" y="44.4" textLength="12.2" clip-path="url(#terminal-243650528-line-1)">[</text><text class="terminal-243650528-r1" x="195.2" y="44.4" textLength="24.4" clip-path="url(#terminal-243650528-line-1)">-h</text><text class="terminal-243650528-r2" x="219.6" y="44.4" textLength="12.2" clip-path="url(#terminal-243650528-line-1)">]</text><text class="terminal-243650528-r2" x="244" y="44.4" textLength="12.2" clip-path="url(#terminal-243650528-line-1)">[</text><text class="terminal-243650528-r1" x="256.2" y="44.4" textLength="109.8" clip-path="url(#terminal-243650528-line-1)">--dry-run</text><text class="terminal-243650528-r2" x="366" y="44.4" textLength="12.2" clip-path="url(#terminal-243650528-line-1)">]</text><text class="terminal-243650528-r2" x="390.4" y="44.4" textLength="12.2" clip-path="url(#terminal-243650528-line-1)">[</text><text class="terminal-243650528-r1" x="402.6" y="44.4" textLength="146.4" clip-path="url(#terminal-243650528-line-1)">--files-only</text><text class="terminal-243650528-r2" x="549" y="44.4" textLength="12.2" clip-path="url(#terminal-243650528-line-1)">]</text><text class="terminal-243650528-r2" x="573.4" y="44.4" textLength="12.2" clip-path="url(#terminal-243650528-line-1)">[</text><text class="terminal-243650528-r1" x="585.6" y="44.4" textLength="183" clip-path="url(#terminal-243650528-line-1)">--local-version</text><text class="terminal-243650528-r2" x="768.6" y="44.4" textLength="12.2" clip-path="url(#terminal-243650528-line-1)">]</text><text class="terminal-243650528-r2" x="793" y="44.4" textLength="12.2" clip-path="url(#terminal-243650528-line-1)">[</text><text class="terminal-243650528-r1" x="805.2" y="44.4" textLength="134.2" clip-path="url(#terminal-243650528-line-1)">--changelog</text><text class="terminal-243650528-r2" x="939.4" y="44.4" textLength="12.2" clip-path="url(#terminal-243650528-line-1)">]</text><text class="terminal-243650528-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-243650528-line-1)"> +</text><text class="terminal-243650528-r2" x="183" y="68.8" textLength="12.2" clip-path="url(#terminal-243650528-line-2)">[</text><text class="terminal-243650528-r1" x="195.2" y="68.8" textLength="134.2" clip-path="url(#terminal-243650528-line-2)">--no-verify</text><text class="terminal-243650528-r2" x="329.4" y="68.8" textLength="12.2" clip-path="url(#terminal-243650528-line-2)">]</text><text class="terminal-243650528-r2" x="353.8" y="68.8" textLength="12.2" clip-path="url(#terminal-243650528-line-2)">[</text><text class="terminal-243650528-r1" x="366" y="68.8" textLength="61" clip-path="url(#terminal-243650528-line-2)">--yes</text><text class="terminal-243650528-r2" x="427" y="68.8" textLength="12.2" clip-path="url(#terminal-243650528-line-2)">]</text><text class="terminal-243650528-r2" x="451.4" y="68.8" textLength="12.2" clip-path="url(#terminal-243650528-line-2)">[</text><text class="terminal-243650528-r1" x="463.6" y="68.8" textLength="280.6" clip-path="url(#terminal-243650528-line-2)">--tag-format TAG_FORMAT</text><text class="terminal-243650528-r2" x="744.2" y="68.8" textLength="12.2" clip-path="url(#terminal-243650528-line-2)">]</text><text class="terminal-243650528-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-243650528-line-2)"> +</text><text class="terminal-243650528-r2" x="183" y="93.2" textLength="12.2" clip-path="url(#terminal-243650528-line-3)">[</text><text class="terminal-243650528-r1" x="195.2" y="93.2" textLength="329.4" clip-path="url(#terminal-243650528-line-3)">--bump-message BUMP_MESSAGE</text><text class="terminal-243650528-r2" x="524.6" y="93.2" textLength="12.2" clip-path="url(#terminal-243650528-line-3)">]</text><text class="terminal-243650528-r2" x="549" y="93.2" textLength="12.2" clip-path="url(#terminal-243650528-line-3)">[</text><text class="terminal-243650528-r1" x="561.2" y="93.2" textLength="158.6" clip-path="url(#terminal-243650528-line-3)">--prerelease </text><text class="terminal-243650528-r2" x="719.8" y="93.2" textLength="12.2" clip-path="url(#terminal-243650528-line-3)">{</text><text class="terminal-243650528-r1" x="732" y="93.2" textLength="158.6" clip-path="url(#terminal-243650528-line-3)">alpha,beta,rc</text><text class="terminal-243650528-r2" x="890.6" y="93.2" textLength="12.2" clip-path="url(#terminal-243650528-line-3)">}</text><text class="terminal-243650528-r2" x="902.8" y="93.2" textLength="12.2" clip-path="url(#terminal-243650528-line-3)">]</text><text class="terminal-243650528-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-243650528-line-3)"> +</text><text class="terminal-243650528-r2" x="183" y="117.6" textLength="12.2" clip-path="url(#terminal-243650528-line-4)">[</text><text class="terminal-243650528-r1" x="195.2" y="117.6" textLength="280.6" clip-path="url(#terminal-243650528-line-4)">--devrelease DEVRELEASE</text><text class="terminal-243650528-r2" x="475.8" y="117.6" textLength="12.2" clip-path="url(#terminal-243650528-line-4)">]</text><text class="terminal-243650528-r2" x="500.2" y="117.6" textLength="12.2" clip-path="url(#terminal-243650528-line-4)">[</text><text class="terminal-243650528-r1" x="512.4" y="117.6" textLength="146.4" clip-path="url(#terminal-243650528-line-4)">--increment </text><text class="terminal-243650528-r2" x="658.8" y="117.6" textLength="12.2" clip-path="url(#terminal-243650528-line-4)">{</text><text class="terminal-243650528-r1" x="671" y="117.6" textLength="207.4" clip-path="url(#terminal-243650528-line-4)">MAJOR,MINOR,PATCH</text><text class="terminal-243650528-r2" x="878.4" y="117.6" textLength="12.2" clip-path="url(#terminal-243650528-line-4)">}</text><text class="terminal-243650528-r2" x="890.6" y="117.6" textLength="12.2" clip-path="url(#terminal-243650528-line-4)">]</text><text class="terminal-243650528-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-243650528-line-4)"> +</text><text class="terminal-243650528-r2" x="183" y="142" textLength="12.2" clip-path="url(#terminal-243650528-line-5)">[</text><text class="terminal-243650528-r1" x="195.2" y="142" textLength="207.4" clip-path="url(#terminal-243650528-line-5)">--increment-mode </text><text class="terminal-243650528-r2" x="402.6" y="142" textLength="12.2" clip-path="url(#terminal-243650528-line-5)">{</text><text class="terminal-243650528-r1" x="414.8" y="142" textLength="146.4" clip-path="url(#terminal-243650528-line-5)">linear,exact</text><text class="terminal-243650528-r2" x="561.2" y="142" textLength="12.2" clip-path="url(#terminal-243650528-line-5)">}</text><text class="terminal-243650528-r2" x="573.4" y="142" textLength="12.2" clip-path="url(#terminal-243650528-line-5)">]</text><text class="terminal-243650528-r2" x="597.8" y="142" textLength="12.2" clip-path="url(#terminal-243650528-line-5)">[</text><text class="terminal-243650528-r1" x="610" y="142" textLength="231.8" clip-path="url(#terminal-243650528-line-5)">--check-consistency</text><text class="terminal-243650528-r2" x="841.8" y="142" textLength="12.2" clip-path="url(#terminal-243650528-line-5)">]</text><text class="terminal-243650528-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-243650528-line-5)"> +</text><text class="terminal-243650528-r2" x="183" y="166.4" textLength="12.2" clip-path="url(#terminal-243650528-line-6)">[</text><text class="terminal-243650528-r1" x="195.2" y="166.4" textLength="183" clip-path="url(#terminal-243650528-line-6)">--annotated-tag</text><text class="terminal-243650528-r2" x="378.2" y="166.4" textLength="12.2" clip-path="url(#terminal-243650528-line-6)">]</text><text class="terminal-243650528-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-243650528-line-6)"> +</text><text class="terminal-243650528-r2" x="183" y="190.8" textLength="12.2" clip-path="url(#terminal-243650528-line-7)">[</text><text class="terminal-243650528-r1" x="195.2" y="190.8" textLength="549" clip-path="url(#terminal-243650528-line-7)">--annotated-tag-message ANNOTATED_TAG_MESSAGE</text><text class="terminal-243650528-r2" x="744.2" y="190.8" textLength="12.2" clip-path="url(#terminal-243650528-line-7)">]</text><text class="terminal-243650528-r2" x="768.6" y="190.8" textLength="12.2" clip-path="url(#terminal-243650528-line-7)">[</text><text class="terminal-243650528-r1" x="780.8" y="190.8" textLength="122" clip-path="url(#terminal-243650528-line-7)">--gpg-sign</text><text class="terminal-243650528-r2" x="902.8" y="190.8" textLength="12.2" clip-path="url(#terminal-243650528-line-7)">]</text><text class="terminal-243650528-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-243650528-line-7)"> +</text><text class="terminal-243650528-r2" x="183" y="215.2" textLength="12.2" clip-path="url(#terminal-243650528-line-8)">[</text><text class="terminal-243650528-r1" x="195.2" y="215.2" textLength="256.2" clip-path="url(#terminal-243650528-line-8)">--changelog-to-stdout</text><text class="terminal-243650528-r2" x="451.4" y="215.2" textLength="12.2" clip-path="url(#terminal-243650528-line-8)">]</text><text class="terminal-243650528-r2" x="475.8" y="215.2" textLength="12.2" clip-path="url(#terminal-243650528-line-8)">[</text><text class="terminal-243650528-r1" x="488" y="215.2" textLength="268.4" clip-path="url(#terminal-243650528-line-8)">--git-output-to-stderr</text><text class="terminal-243650528-r2" x="756.4" y="215.2" textLength="12.2" clip-path="url(#terminal-243650528-line-8)">]</text><text class="terminal-243650528-r2" x="780.8" y="215.2" textLength="12.2" clip-path="url(#terminal-243650528-line-8)">[</text><text class="terminal-243650528-r1" x="793" y="215.2" textLength="85.4" clip-path="url(#terminal-243650528-line-8)">--retry</text><text class="terminal-243650528-r2" x="878.4" y="215.2" textLength="12.2" clip-path="url(#terminal-243650528-line-8)">]</text><text class="terminal-243650528-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-243650528-line-8)"> +</text><text class="terminal-243650528-r2" x="183" y="239.6" textLength="12.2" clip-path="url(#terminal-243650528-line-9)">[</text><text class="terminal-243650528-r1" x="195.2" y="239.6" textLength="244" clip-path="url(#terminal-243650528-line-9)">--major-version-zero</text><text class="terminal-243650528-r2" x="439.2" y="239.6" textLength="12.2" clip-path="url(#terminal-243650528-line-9)">]</text><text class="terminal-243650528-r2" x="463.6" y="239.6" textLength="12.2" clip-path="url(#terminal-243650528-line-9)">[</text><text class="terminal-243650528-r1" x="475.8" y="239.6" textLength="231.8" clip-path="url(#terminal-243650528-line-9)">--template TEMPLATE</text><text class="terminal-243650528-r2" x="707.6" y="239.6" textLength="12.2" clip-path="url(#terminal-243650528-line-9)">]</text><text class="terminal-243650528-r2" x="732" y="239.6" textLength="12.2" clip-path="url(#terminal-243650528-line-9)">[</text><text class="terminal-243650528-r1" x="744.2" y="239.6" textLength="158.6" clip-path="url(#terminal-243650528-line-9)">--extra EXTRA</text><text class="terminal-243650528-r2" x="902.8" y="239.6" textLength="12.2" clip-path="url(#terminal-243650528-line-9)">]</text><text class="terminal-243650528-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-243650528-line-9)"> +</text><text class="terminal-243650528-r2" x="183" y="264" textLength="12.2" clip-path="url(#terminal-243650528-line-10)">[</text><text class="terminal-243650528-r1" x="195.2" y="264" textLength="256.2" clip-path="url(#terminal-243650528-line-10)">--file-name FILE_NAME</text><text class="terminal-243650528-r2" x="451.4" y="264" textLength="12.2" clip-path="url(#terminal-243650528-line-10)">]</text><text class="terminal-243650528-r2" x="475.8" y="264" textLength="12.2" clip-path="url(#terminal-243650528-line-10)">[</text><text class="terminal-243650528-r1" x="488" y="264" textLength="451.4" clip-path="url(#terminal-243650528-line-10)">--prerelease-offset PRERELEASE_OFFSET</text><text class="terminal-243650528-r2" x="939.4" y="264" textLength="12.2" clip-path="url(#terminal-243650528-line-10)">]</text><text class="terminal-243650528-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-243650528-line-10)"> +</text><text class="terminal-243650528-r2" x="183" y="288.4" textLength="12.2" clip-path="url(#terminal-243650528-line-11)">[</text><text class="terminal-243650528-r1" x="195.2" y="288.4" textLength="207.4" clip-path="url(#terminal-243650528-line-11)">--version-scheme </text><text class="terminal-243650528-r2" x="402.6" y="288.4" textLength="12.2" clip-path="url(#terminal-243650528-line-11)">{</text><text class="terminal-243650528-r1" x="414.8" y="288.4" textLength="256.2" clip-path="url(#terminal-243650528-line-11)">pep440,semver,semver2</text><text class="terminal-243650528-r2" x="671" y="288.4" textLength="12.2" clip-path="url(#terminal-243650528-line-11)">}</text><text class="terminal-243650528-r2" x="683.2" y="288.4" textLength="12.2" clip-path="url(#terminal-243650528-line-11)">]</text><text class="terminal-243650528-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-243650528-line-11)"> +</text><text class="terminal-243650528-r2" x="183" y="312.8" textLength="12.2" clip-path="url(#terminal-243650528-line-12)">[</text><text class="terminal-243650528-r1" x="195.2" y="312.8" textLength="183" clip-path="url(#terminal-243650528-line-12)">--version-type </text><text class="terminal-243650528-r2" x="378.2" y="312.8" textLength="12.2" clip-path="url(#terminal-243650528-line-12)">{</text><text class="terminal-243650528-r1" x="390.4" y="312.8" textLength="256.2" clip-path="url(#terminal-243650528-line-12)">pep440,semver,semver2</text><text class="terminal-243650528-r2" x="646.6" y="312.8" textLength="12.2" clip-path="url(#terminal-243650528-line-12)">}</text><text class="terminal-243650528-r2" x="658.8" y="312.8" textLength="12.2" clip-path="url(#terminal-243650528-line-12)">]</text><text class="terminal-243650528-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-243650528-line-12)"> +</text><text class="terminal-243650528-r2" x="183" y="337.2" textLength="12.2" clip-path="url(#terminal-243650528-line-13)">[</text><text class="terminal-243650528-r1" x="195.2" y="337.2" textLength="378.2" clip-path="url(#terminal-243650528-line-13)">--build-metadata BUILD_METADATA</text><text class="terminal-243650528-r2" x="573.4" y="337.2" textLength="12.2" clip-path="url(#terminal-243650528-line-13)">]</text><text class="terminal-243650528-r2" x="597.8" y="337.2" textLength="12.2" clip-path="url(#terminal-243650528-line-13)">[</text><text class="terminal-243650528-r1" x="610" y="337.2" textLength="122" clip-path="url(#terminal-243650528-line-13)">--get-next</text><text class="terminal-243650528-r2" x="732" y="337.2" textLength="12.2" clip-path="url(#terminal-243650528-line-13)">]</text><text class="terminal-243650528-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-243650528-line-13)"> +</text><text class="terminal-243650528-r2" x="183" y="361.6" textLength="12.2" clip-path="url(#terminal-243650528-line-14)">[</text><text class="terminal-243650528-r1" x="195.2" y="361.6" textLength="207.4" clip-path="url(#terminal-243650528-line-14)">--allow-no-commit</text><text class="terminal-243650528-r2" x="402.6" y="361.6" textLength="12.2" clip-path="url(#terminal-243650528-line-14)">]</text><text class="terminal-243650528-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-243650528-line-14)"> +</text><text class="terminal-243650528-r2" x="183" y="386" textLength="12.2" clip-path="url(#terminal-243650528-line-15)">[</text><text class="terminal-243650528-r1" x="195.2" y="386" textLength="170.8" clip-path="url(#terminal-243650528-line-15)">MANUAL_VERSION</text><text class="terminal-243650528-r2" x="366" y="386" textLength="12.2" clip-path="url(#terminal-243650528-line-15)">]</text><text class="terminal-243650528-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-243650528-line-15)"> +</text><text class="terminal-243650528-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-243650528-line-16)"> +</text><text class="terminal-243650528-r1" x="0" y="434.8" textLength="512.4" clip-path="url(#terminal-243650528-line-17)">bump semantic version based on the git log</text><text class="terminal-243650528-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-243650528-line-17)"> +</text><text class="terminal-243650528-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-243650528-line-18)"> +</text><text class="terminal-243650528-r1" x="0" y="483.6" textLength="256.2" clip-path="url(#terminal-243650528-line-19)">positional arguments:</text><text class="terminal-243650528-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-243650528-line-19)"> +</text><text class="terminal-243650528-r1" x="0" y="508" textLength="610" clip-path="url(#terminal-243650528-line-20)">  MANUAL_VERSION        bump to the given version </text><text class="terminal-243650528-r2" x="610" y="508" textLength="12.2" clip-path="url(#terminal-243650528-line-20)">(</text><text class="terminal-243650528-r1" x="622.2" y="508" textLength="61" clip-path="url(#terminal-243650528-line-20)">e.g: </text><text class="terminal-243650528-r3" x="683.2" y="508" textLength="36.6" clip-path="url(#terminal-243650528-line-20)">1.5</text><text class="terminal-243650528-r1" x="719.8" y="508" textLength="12.2" clip-path="url(#terminal-243650528-line-20)">.</text><text class="terminal-243650528-r3" x="732" y="508" textLength="12.2" clip-path="url(#terminal-243650528-line-20)">3</text><text class="terminal-243650528-r2" x="744.2" y="508" textLength="12.2" clip-path="url(#terminal-243650528-line-20)">)</text><text class="terminal-243650528-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-243650528-line-20)"> +</text><text class="terminal-243650528-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-243650528-line-21)"> +</text><text class="terminal-243650528-r1" x="0" y="556.8" textLength="97.6" clip-path="url(#terminal-243650528-line-22)">options:</text><text class="terminal-243650528-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-243650528-line-22)"> +</text><text class="terminal-243650528-r1" x="0" y="581.2" textLength="671" clip-path="url(#terminal-243650528-line-23)">  -h, --help            show this help message and exit</text><text class="terminal-243650528-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-243650528-line-23)"> +</text><text class="terminal-243650528-r1" x="0" y="605.6" textLength="915" clip-path="url(#terminal-243650528-line-24)">  --dry-run             show output to stdout, no commit, no modified files</text><text class="terminal-243650528-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-243650528-line-24)"> +</text><text class="terminal-243650528-r1" x="0" y="630" textLength="793" clip-path="url(#terminal-243650528-line-25)">  --files-only          bump version in the files from the config</text><text class="terminal-243650528-r1" x="976" y="630" textLength="12.2" clip-path="url(#terminal-243650528-line-25)"> +</text><text class="terminal-243650528-r1" x="0" y="654.4" textLength="719.8" clip-path="url(#terminal-243650528-line-26)">  --local-version       bump only the local version portion</text><text class="terminal-243650528-r1" x="976" y="654.4" textLength="12.2" clip-path="url(#terminal-243650528-line-26)"> +</text><text class="terminal-243650528-r1" x="0" y="678.8" textLength="841.8" clip-path="url(#terminal-243650528-line-27)">  --changelog, -ch      generate the changelog for the newest version</text><text class="terminal-243650528-r1" x="976" y="678.8" textLength="12.2" clip-path="url(#terminal-243650528-line-27)"> +</text><text class="terminal-243650528-r1" x="0" y="703.2" textLength="902.8" clip-path="url(#terminal-243650528-line-28)">  --no-verify           this option bypasses the pre-commit and commit-msg</text><text class="terminal-243650528-r1" x="976" y="703.2" textLength="12.2" clip-path="url(#terminal-243650528-line-28)"> +</text><text class="terminal-243650528-r1" x="0" y="727.6" textLength="353.8" clip-path="url(#terminal-243650528-line-29)">                        hooks</text><text class="terminal-243650528-r1" x="976" y="727.6" textLength="12.2" clip-path="url(#terminal-243650528-line-29)"> +</text><text class="terminal-243650528-r1" x="0" y="752" textLength="719.8" clip-path="url(#terminal-243650528-line-30)">  --yes                 accept automatically questions done</text><text class="terminal-243650528-r1" x="976" y="752" textLength="12.2" clip-path="url(#terminal-243650528-line-30)"> +</text><text class="terminal-243650528-r1" x="0" y="776.4" textLength="305" clip-path="url(#terminal-243650528-line-31)">  --tag-format TAG_FORMAT</text><text class="terminal-243650528-r1" x="976" y="776.4" textLength="12.2" clip-path="url(#terminal-243650528-line-31)"> +</text><text class="terminal-243650528-r1" x="0" y="800.8" textLength="939.4" clip-path="url(#terminal-243650528-line-32)">                        the format used to tag the commit and read it, use it</text><text class="terminal-243650528-r1" x="976" y="800.8" textLength="12.2" clip-path="url(#terminal-243650528-line-32)"> +</text><text class="terminal-243650528-r1" x="0" y="825.2" textLength="866.2" clip-path="url(#terminal-243650528-line-33)">                        in existing projects, wrap around simple quotes</text><text class="terminal-243650528-r1" x="976" y="825.2" textLength="12.2" clip-path="url(#terminal-243650528-line-33)"> +</text><text class="terminal-243650528-r1" x="0" y="849.6" textLength="353.8" clip-path="url(#terminal-243650528-line-34)">  --bump-message BUMP_MESSAGE</text><text class="terminal-243650528-r1" x="976" y="849.6" textLength="12.2" clip-path="url(#terminal-243650528-line-34)"> +</text><text class="terminal-243650528-r1" x="0" y="874" textLength="902.8" clip-path="url(#terminal-243650528-line-35)">                        template used to create the release commit, useful</text><text class="terminal-243650528-r1" x="976" y="874" textLength="12.2" clip-path="url(#terminal-243650528-line-35)"> +</text><text class="terminal-243650528-r1" x="0" y="898.4" textLength="536.8" clip-path="url(#terminal-243650528-line-36)">                        when working with CI</text><text class="terminal-243650528-r1" x="976" y="898.4" textLength="12.2" clip-path="url(#terminal-243650528-line-36)"> +</text><text class="terminal-243650528-r1" x="0" y="922.8" textLength="244" clip-path="url(#terminal-243650528-line-37)">  --prerelease, -pr </text><text class="terminal-243650528-r2" x="244" y="922.8" textLength="12.2" clip-path="url(#terminal-243650528-line-37)">{</text><text class="terminal-243650528-r1" x="256.2" y="922.8" textLength="158.6" clip-path="url(#terminal-243650528-line-37)">alpha,beta,rc</text><text class="terminal-243650528-r2" x="414.8" y="922.8" textLength="12.2" clip-path="url(#terminal-243650528-line-37)">}</text><text class="terminal-243650528-r1" x="976" y="922.8" textLength="12.2" clip-path="url(#terminal-243650528-line-37)"> +</text><text class="terminal-243650528-r1" x="0" y="947.2" textLength="597.8" clip-path="url(#terminal-243650528-line-38)">                        choose type of prerelease</text><text class="terminal-243650528-r1" x="976" y="947.2" textLength="12.2" clip-path="url(#terminal-243650528-line-38)"> +</text><text class="terminal-243650528-r1" x="0" y="971.6" textLength="353.8" clip-path="url(#terminal-243650528-line-39)">  --devrelease, -d DEVRELEASE</text><text class="terminal-243650528-r1" x="976" y="971.6" textLength="12.2" clip-path="url(#terminal-243650528-line-39)"> +</text><text class="terminal-243650528-r1" x="0" y="996" textLength="841.8" clip-path="url(#terminal-243650528-line-40)">                        specify non-negative integer for dev. release</text><text class="terminal-243650528-r1" x="976" y="996" textLength="12.2" clip-path="url(#terminal-243650528-line-40)"> +</text><text class="terminal-243650528-r1" x="0" y="1020.4" textLength="170.8" clip-path="url(#terminal-243650528-line-41)">  --increment </text><text class="terminal-243650528-r2" x="170.8" y="1020.4" textLength="12.2" clip-path="url(#terminal-243650528-line-41)">{</text><text class="terminal-243650528-r1" x="183" y="1020.4" textLength="207.4" clip-path="url(#terminal-243650528-line-41)">MAJOR,MINOR,PATCH</text><text class="terminal-243650528-r2" x="390.4" y="1020.4" textLength="12.2" clip-path="url(#terminal-243650528-line-41)">}</text><text class="terminal-243650528-r1" x="976" y="1020.4" textLength="12.2" clip-path="url(#terminal-243650528-line-41)"> +</text><text class="terminal-243650528-r1" x="0" y="1044.8" textLength="756.4" clip-path="url(#terminal-243650528-line-42)">                        manually specify the desired increment</text><text class="terminal-243650528-r1" x="976" y="1044.8" textLength="12.2" clip-path="url(#terminal-243650528-line-42)"> +</text><text class="terminal-243650528-r1" x="0" y="1069.2" textLength="231.8" clip-path="url(#terminal-243650528-line-43)">  --increment-mode </text><text class="terminal-243650528-r2" x="231.8" y="1069.2" textLength="12.2" clip-path="url(#terminal-243650528-line-43)">{</text><text class="terminal-243650528-r1" x="244" y="1069.2" textLength="146.4" clip-path="url(#terminal-243650528-line-43)">linear,exact</text><text class="terminal-243650528-r2" x="390.4" y="1069.2" textLength="12.2" clip-path="url(#terminal-243650528-line-43)">}</text><text class="terminal-243650528-r1" x="976" y="1069.2" textLength="12.2" clip-path="url(#terminal-243650528-line-43)"> +</text><text class="terminal-243650528-r1" x="0" y="1093.6" textLength="902.8" clip-path="url(#terminal-243650528-line-44)">                        set the method by which the new version is chosen.</text><text class="terminal-243650528-r1" x="976" y="1093.6" textLength="12.2" clip-path="url(#terminal-243650528-line-44)"> +</text><text class="terminal-243650528-r4" x="292.8" y="1118" textLength="97.6" clip-path="url(#terminal-243650528-line-45)">'linear'</text><text class="terminal-243650528-r2" x="402.6" y="1118" textLength="12.2" clip-path="url(#terminal-243650528-line-45)">(</text><text class="terminal-243650528-r1" x="414.8" y="1118" textLength="85.4" clip-path="url(#terminal-243650528-line-45)">default</text><text class="terminal-243650528-r2" x="500.2" y="1118" textLength="12.2" clip-path="url(#terminal-243650528-line-45)">)</text><text class="terminal-243650528-r1" x="512.4" y="1118" textLength="414.8" clip-path="url(#terminal-243650528-line-45)"> guesses the next version based on</text><text class="terminal-243650528-r1" x="976" y="1118" textLength="12.2" clip-path="url(#terminal-243650528-line-45)"> +</text><text class="terminal-243650528-r1" x="0" y="1142.4" textLength="939.4" clip-path="url(#terminal-243650528-line-46)">                        typical linear version progression, such that bumping</text><text class="terminal-243650528-r1" x="976" y="1142.4" textLength="12.2" clip-path="url(#terminal-243650528-line-46)"> +</text><text class="terminal-243650528-r1" x="0" y="1166.8" textLength="866.2" clip-path="url(#terminal-243650528-line-47)">                        of a pre-release with lower precedence than the</text><text class="terminal-243650528-r1" x="976" y="1166.8" textLength="12.2" clip-path="url(#terminal-243650528-line-47)"> +</text><text class="terminal-243650528-r1" x="0" y="1191.2" textLength="939.4" clip-path="url(#terminal-243650528-line-48)">                        current pre-release phase maintains the current phase</text><text class="terminal-243650528-r1" x="976" y="1191.2" textLength="12.2" clip-path="url(#terminal-243650528-line-48)"> +</text><text class="terminal-243650528-r1" x="0" y="1215.6" textLength="561.2" clip-path="url(#terminal-243650528-line-49)">                        of higher precedence. </text><text class="terminal-243650528-r4" x="561.2" y="1215.6" textLength="85.4" clip-path="url(#terminal-243650528-line-49)">'exact'</text><text class="terminal-243650528-r1" x="646.6" y="1215.6" textLength="305" clip-path="url(#terminal-243650528-line-49)"> applies the changes that</text><text class="terminal-243650528-r1" x="976" y="1215.6" textLength="12.2" clip-path="url(#terminal-243650528-line-49)"> +</text><text class="terminal-243650528-r1" x="0" y="1240" textLength="536.8" clip-path="url(#terminal-243650528-line-50)">                        have been specified </text><text class="terminal-243650528-r2" x="536.8" y="1240" textLength="12.2" clip-path="url(#terminal-243650528-line-50)">(</text><text class="terminal-243650528-r1" x="549" y="1240" textLength="353.8" clip-path="url(#terminal-243650528-line-50)">or determined from the commit</text><text class="terminal-243650528-r1" x="976" y="1240" textLength="12.2" clip-path="url(#terminal-243650528-line-50)"> +</text><text class="terminal-243650528-r1" x="0" y="1264.4" textLength="329.4" clip-path="url(#terminal-243650528-line-51)">                        log</text><text class="terminal-243650528-r2" x="329.4" y="1264.4" textLength="12.2" clip-path="url(#terminal-243650528-line-51)">)</text><text class="terminal-243650528-r1" x="341.6" y="1264.4" textLength="585.6" clip-path="url(#terminal-243650528-line-51)"> without interpretation, such that the increment</text><text class="terminal-243650528-r1" x="976" y="1264.4" textLength="12.2" clip-path="url(#terminal-243650528-line-51)"> +</text><text class="terminal-243650528-r1" x="0" y="1288.8" textLength="707.6" clip-path="url(#terminal-243650528-line-52)">                        and pre-release are always honored</text><text class="terminal-243650528-r1" x="976" y="1288.8" textLength="12.2" clip-path="url(#terminal-243650528-line-52)"> +</text><text class="terminal-243650528-r1" x="0" y="1313.2" textLength="317.2" clip-path="url(#terminal-243650528-line-53)">  --check-consistency, -cc</text><text class="terminal-243650528-r1" x="976" y="1313.2" textLength="12.2" clip-path="url(#terminal-243650528-line-53)"> +</text><text class="terminal-243650528-r1" x="0" y="1337.6" textLength="951.6" clip-path="url(#terminal-243650528-line-54)">                        check consistency among versions defined in commitizen</text><text class="terminal-243650528-r1" x="976" y="1337.6" textLength="12.2" clip-path="url(#terminal-243650528-line-54)"> +</text><text class="terminal-243650528-r1" x="0" y="1362" textLength="671" clip-path="url(#terminal-243650528-line-55)">                        configuration and version_files</text><text class="terminal-243650528-r1" x="976" y="1362" textLength="12.2" clip-path="url(#terminal-243650528-line-55)"> +</text><text class="terminal-243650528-r1" x="0" y="1386.4" textLength="866.2" clip-path="url(#terminal-243650528-line-56)">  --annotated-tag, -at  create annotated tag instead of lightweight one</text><text class="terminal-243650528-r1" x="976" y="1386.4" textLength="12.2" clip-path="url(#terminal-243650528-line-56)"> +</text><text class="terminal-243650528-r1" x="0" y="1410.8" textLength="646.6" clip-path="url(#terminal-243650528-line-57)">  --annotated-tag-message, -atm ANNOTATED_TAG_MESSAGE</text><text class="terminal-243650528-r1" x="976" y="1410.8" textLength="12.2" clip-path="url(#terminal-243650528-line-57)"> +</text><text class="terminal-243650528-r1" x="0" y="1435.2" textLength="634.4" clip-path="url(#terminal-243650528-line-58)">                        create annotated tag message</text><text class="terminal-243650528-r1" x="976" y="1435.2" textLength="12.2" clip-path="url(#terminal-243650528-line-58)"> +</text><text class="terminal-243650528-r1" x="0" y="1459.6" textLength="719.8" clip-path="url(#terminal-243650528-line-59)">  --gpg-sign, -s        sign tag instead of lightweight one</text><text class="terminal-243650528-r1" x="976" y="1459.6" textLength="12.2" clip-path="url(#terminal-243650528-line-59)"> +</text><text class="terminal-243650528-r1" x="0" y="1484" textLength="280.6" clip-path="url(#terminal-243650528-line-60)">  --changelog-to-stdout</text><text class="terminal-243650528-r1" x="976" y="1484" textLength="12.2" clip-path="url(#terminal-243650528-line-60)"> +</text><text class="terminal-243650528-r1" x="0" y="1508.4" textLength="658.8" clip-path="url(#terminal-243650528-line-61)">                        Output changelog to the stdout</text><text class="terminal-243650528-r1" x="976" y="1508.4" textLength="12.2" clip-path="url(#terminal-243650528-line-61)"> +</text><text class="terminal-243650528-r1" x="0" y="1532.8" textLength="292.8" clip-path="url(#terminal-243650528-line-62)">  --git-output-to-stderr</text><text class="terminal-243650528-r1" x="976" y="1532.8" textLength="12.2" clip-path="url(#terminal-243650528-line-62)"> +</text><text class="terminal-243650528-r1" x="0" y="1557.2" textLength="646.6" clip-path="url(#terminal-243650528-line-63)">                        Redirect git output to stderr</text><text class="terminal-243650528-r1" x="976" y="1557.2" textLength="12.2" clip-path="url(#terminal-243650528-line-63)"> +</text><text class="terminal-243650528-r1" x="0" y="1581.6" textLength="744.2" clip-path="url(#terminal-243650528-line-64)">  --retry               retry commit if it fails the 1st time</text><text class="terminal-243650528-r1" x="976" y="1581.6" textLength="12.2" clip-path="url(#terminal-243650528-line-64)"> +</text><text class="terminal-243650528-r1" x="0" y="1606" textLength="939.4" clip-path="url(#terminal-243650528-line-65)">  --major-version-zero  keep major version at zero, even for breaking changes</text><text class="terminal-243650528-r1" x="976" y="1606" textLength="12.2" clip-path="url(#terminal-243650528-line-65)"> +</text><text class="terminal-243650528-r1" x="0" y="1630.4" textLength="305" clip-path="url(#terminal-243650528-line-66)">  --template, -t TEMPLATE</text><text class="terminal-243650528-r1" x="976" y="1630.4" textLength="12.2" clip-path="url(#terminal-243650528-line-66)"> +</text><text class="terminal-243650528-r1" x="0" y="1654.8" textLength="646.6" clip-path="url(#terminal-243650528-line-67)">                        changelog template file name </text><text class="terminal-243650528-r2" x="646.6" y="1654.8" textLength="12.2" clip-path="url(#terminal-243650528-line-67)">(</text><text class="terminal-243650528-r1" x="658.8" y="1654.8" textLength="280.6" clip-path="url(#terminal-243650528-line-67)">relative to the current</text><text class="terminal-243650528-r1" x="976" y="1654.8" textLength="12.2" clip-path="url(#terminal-243650528-line-67)"> +</text><text class="terminal-243650528-r1" x="0" y="1679.2" textLength="500.2" clip-path="url(#terminal-243650528-line-68)">                        working directory</text><text class="terminal-243650528-r2" x="500.2" y="1679.2" textLength="12.2" clip-path="url(#terminal-243650528-line-68)">)</text><text class="terminal-243650528-r1" x="976" y="1679.2" textLength="12.2" clip-path="url(#terminal-243650528-line-68)"> +</text><text class="terminal-243650528-r1" x="0" y="1703.6" textLength="622.2" clip-path="url(#terminal-243650528-line-69)">  --extra, -e EXTRA     a changelog extra variable </text><text class="terminal-243650528-r2" x="622.2" y="1703.6" textLength="12.2" clip-path="url(#terminal-243650528-line-69)">(</text><text class="terminal-243650528-r1" x="634.4" y="1703.6" textLength="146.4" clip-path="url(#terminal-243650528-line-69)">in the form </text><text class="terminal-243650528-r4" x="780.8" y="1703.6" textLength="12.2" clip-path="url(#terminal-243650528-line-69)">'</text><text class="terminal-243650528-r4" x="793" y="1703.6" textLength="36.6" clip-path="url(#terminal-243650528-line-69)">key</text><text class="terminal-243650528-r4" x="829.6" y="1703.6" textLength="12.2" clip-path="url(#terminal-243650528-line-69)">=</text><text class="terminal-243650528-r4" x="841.8" y="1703.6" textLength="61" clip-path="url(#terminal-243650528-line-69)">value</text><text class="terminal-243650528-r4" x="902.8" y="1703.6" textLength="12.2" clip-path="url(#terminal-243650528-line-69)">'</text><text class="terminal-243650528-r2" x="915" y="1703.6" textLength="12.2" clip-path="url(#terminal-243650528-line-69)">)</text><text class="terminal-243650528-r1" x="976" y="1703.6" textLength="12.2" clip-path="url(#terminal-243650528-line-69)"> +</text><text class="terminal-243650528-r1" x="0" y="1728" textLength="280.6" clip-path="url(#terminal-243650528-line-70)">  --file-name FILE_NAME</text><text class="terminal-243650528-r1" x="976" y="1728" textLength="12.2" clip-path="url(#terminal-243650528-line-70)"> +</text><text class="terminal-243650528-r1" x="0" y="1752.4" textLength="573.4" clip-path="url(#terminal-243650528-line-71)">                        file name of changelog </text><text class="terminal-243650528-r2" x="573.4" y="1752.4" textLength="12.2" clip-path="url(#terminal-243650528-line-71)">(</text><text class="terminal-243650528-r1" x="585.6" y="1752.4" textLength="109.8" clip-path="url(#terminal-243650528-line-71)">default: </text><text class="terminal-243650528-r4" x="695.4" y="1752.4" textLength="170.8" clip-path="url(#terminal-243650528-line-71)">'CHANGELOG.md'</text><text class="terminal-243650528-r2" x="866.2" y="1752.4" textLength="12.2" clip-path="url(#terminal-243650528-line-71)">)</text><text class="terminal-243650528-r1" x="976" y="1752.4" textLength="12.2" clip-path="url(#terminal-243650528-line-71)"> +</text><text class="terminal-243650528-r1" x="0" y="1776.8" textLength="475.8" clip-path="url(#terminal-243650528-line-72)">  --prerelease-offset PRERELEASE_OFFSET</text><text class="terminal-243650528-r1" x="976" y="1776.8" textLength="12.2" clip-path="url(#terminal-243650528-line-72)"> +</text><text class="terminal-243650528-r1" x="0" y="1801.2" textLength="719.8" clip-path="url(#terminal-243650528-line-73)">                        start pre-releases with this offset</text><text class="terminal-243650528-r1" x="976" y="1801.2" textLength="12.2" clip-path="url(#terminal-243650528-line-73)"> +</text><text class="terminal-243650528-r1" x="0" y="1825.6" textLength="231.8" clip-path="url(#terminal-243650528-line-74)">  --version-scheme </text><text class="terminal-243650528-r2" x="231.8" y="1825.6" textLength="12.2" clip-path="url(#terminal-243650528-line-74)">{</text><text class="terminal-243650528-r1" x="244" y="1825.6" textLength="256.2" clip-path="url(#terminal-243650528-line-74)">pep440,semver,semver2</text><text class="terminal-243650528-r2" x="500.2" y="1825.6" textLength="12.2" clip-path="url(#terminal-243650528-line-74)">}</text><text class="terminal-243650528-r1" x="976" y="1825.6" textLength="12.2" clip-path="url(#terminal-243650528-line-74)"> +</text><text class="terminal-243650528-r1" x="0" y="1850" textLength="549" clip-path="url(#terminal-243650528-line-75)">                        choose version scheme</text><text class="terminal-243650528-r1" x="976" y="1850" textLength="12.2" clip-path="url(#terminal-243650528-line-75)"> +</text><text class="terminal-243650528-r1" x="0" y="1874.4" textLength="207.4" clip-path="url(#terminal-243650528-line-76)">  --version-type </text><text class="terminal-243650528-r2" x="207.4" y="1874.4" textLength="12.2" clip-path="url(#terminal-243650528-line-76)">{</text><text class="terminal-243650528-r1" x="219.6" y="1874.4" textLength="256.2" clip-path="url(#terminal-243650528-line-76)">pep440,semver,semver2</text><text class="terminal-243650528-r2" x="475.8" y="1874.4" textLength="12.2" clip-path="url(#terminal-243650528-line-76)">}</text><text class="terminal-243650528-r1" x="976" y="1874.4" textLength="12.2" clip-path="url(#terminal-243650528-line-76)"> +</text><text class="terminal-243650528-r1" x="0" y="1898.8" textLength="683.2" clip-path="url(#terminal-243650528-line-77)">                        Deprecated, use --version-scheme</text><text class="terminal-243650528-r1" x="976" y="1898.8" textLength="12.2" clip-path="url(#terminal-243650528-line-77)"> +</text><text class="terminal-243650528-r1" x="0" y="1923.2" textLength="402.6" clip-path="url(#terminal-243650528-line-78)">  --build-metadata BUILD_METADATA</text><text class="terminal-243650528-r1" x="976" y="1923.2" textLength="12.2" clip-path="url(#terminal-243650528-line-78)"> +</text><text class="terminal-243650528-r1" x="0" y="1947.6" textLength="915" clip-path="url(#terminal-243650528-line-79)">                        Add additional build-metadata to the version-number</text><text class="terminal-243650528-r1" x="976" y="1947.6" textLength="12.2" clip-path="url(#terminal-243650528-line-79)"> +</text><text class="terminal-243650528-r1" x="0" y="1972" textLength="854" clip-path="url(#terminal-243650528-line-80)">  --get-next            Determine the next version and write to stdout</text><text class="terminal-243650528-r1" x="976" y="1972" textLength="12.2" clip-path="url(#terminal-243650528-line-80)"> +</text><text class="terminal-243650528-r1" x="0" y="1996.4" textLength="744.2" clip-path="url(#terminal-243650528-line-81)">  --allow-no-commit     bump version without eligible commits</text><text class="terminal-243650528-r1" x="976" y="1996.4" textLength="12.2" clip-path="url(#terminal-243650528-line-81)"> +</text><text class="terminal-243650528-r1" x="976" y="2020.8" textLength="12.2" clip-path="url(#terminal-243650528-line-82)"> +</text> + </g> + </g> +</svg> diff --git a/docs/images/cli_help/cz_changelog___help.svg b/docs/images/cli_help/cz_changelog___help.svg new file mode 100644 index 0000000..1160ccf --- /dev/null +++ b/docs/images/cli_help/cz_changelog___help.svg @@ -0,0 +1,221 @@ +<svg class="rich-terminal" viewBox="0 0 994 1050.4" xmlns="http://www.w3.org/2000/svg"> + <!-- Generated with Rich https://www.textualize.io --> + <style> + + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Regular"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); + font-style: normal; + font-weight: 400; + } + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Bold"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); + font-style: bold; + font-weight: 700; + } + + .terminal-1106739011-matrix { + font-family: Fira Code, monospace; + font-size: 20px; + line-height: 24.4px; + font-variant-east-asian: full-width; + } + + .terminal-1106739011-title { + font-size: 18px; + font-weight: bold; + font-family: arial; + } + + .terminal-1106739011-r1 { fill: #c5c8c6 } +.terminal-1106739011-r2 { fill: #c5c8c6;font-weight: bold } +.terminal-1106739011-r3 { fill: #68a0b3;font-weight: bold } +.terminal-1106739011-r4 { fill: #98a84b } + </style> + + <defs> + <clipPath id="terminal-1106739011-clip-terminal"> + <rect x="0" y="0" width="975.0" height="999.4" /> + </clipPath> + <clipPath id="terminal-1106739011-line-0"> + <rect x="0" y="1.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-1"> + <rect x="0" y="25.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-2"> + <rect x="0" y="50.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-3"> + <rect x="0" y="74.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-4"> + <rect x="0" y="99.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-5"> + <rect x="0" y="123.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-6"> + <rect x="0" y="147.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-7"> + <rect x="0" y="172.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-8"> + <rect x="0" y="196.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-9"> + <rect x="0" y="221.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-10"> + <rect x="0" y="245.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-11"> + <rect x="0" y="269.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-12"> + <rect x="0" y="294.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-13"> + <rect x="0" y="318.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-14"> + <rect x="0" y="343.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-15"> + <rect x="0" y="367.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-16"> + <rect x="0" y="391.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-17"> + <rect x="0" y="416.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-18"> + <rect x="0" y="440.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-19"> + <rect x="0" y="465.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-20"> + <rect x="0" y="489.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-21"> + <rect x="0" y="513.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-22"> + <rect x="0" y="538.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-23"> + <rect x="0" y="562.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-24"> + <rect x="0" y="587.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-25"> + <rect x="0" y="611.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-26"> + <rect x="0" y="635.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-27"> + <rect x="0" y="660.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-28"> + <rect x="0" y="684.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-29"> + <rect x="0" y="709.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-30"> + <rect x="0" y="733.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-31"> + <rect x="0" y="757.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-32"> + <rect x="0" y="782.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-33"> + <rect x="0" y="806.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-34"> + <rect x="0" y="831.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-35"> + <rect x="0" y="855.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-36"> + <rect x="0" y="879.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-37"> + <rect x="0" y="904.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-38"> + <rect x="0" y="928.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1106739011-line-39"> + <rect x="0" y="953.1" width="976" height="24.65"/> + </clipPath> + </defs> + + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="1048.4" rx="8"/> + <g transform="translate(26,22)"> + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> + <circle cx="22" cy="0" r="7" fill="#febc2e"/> + <circle cx="44" cy="0" r="7" fill="#28c840"/> + </g> + + <g transform="translate(9, 41)" clip-path="url(#terminal-1106739011-clip-terminal)"> + + <g class="terminal-1106739011-matrix"> + <text class="terminal-1106739011-r1" x="0" y="20" textLength="256.2" clip-path="url(#terminal-1106739011-line-0)">$ cz changelog --help</text><text class="terminal-1106739011-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-1106739011-line-0)"> +</text><text class="terminal-1106739011-r1" x="0" y="44.4" textLength="244" clip-path="url(#terminal-1106739011-line-1)">usage: cz changelog </text><text class="terminal-1106739011-r2" x="244" y="44.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-1)">[</text><text class="terminal-1106739011-r1" x="256.2" y="44.4" textLength="24.4" clip-path="url(#terminal-1106739011-line-1)">-h</text><text class="terminal-1106739011-r2" x="280.6" y="44.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-1)">]</text><text class="terminal-1106739011-r2" x="305" y="44.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-1)">[</text><text class="terminal-1106739011-r1" x="317.2" y="44.4" textLength="109.8" clip-path="url(#terminal-1106739011-line-1)">--dry-run</text><text class="terminal-1106739011-r2" x="427" y="44.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-1)">]</text><text class="terminal-1106739011-r2" x="451.4" y="44.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-1)">[</text><text class="terminal-1106739011-r1" x="463.6" y="44.4" textLength="256.2" clip-path="url(#terminal-1106739011-line-1)">--file-name FILE_NAME</text><text class="terminal-1106739011-r2" x="719.8" y="44.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-1)">]</text><text class="terminal-1106739011-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-1)"> +</text><text class="terminal-1106739011-r2" x="244" y="68.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-2)">[</text><text class="terminal-1106739011-r1" x="256.2" y="68.8" textLength="475.8" clip-path="url(#terminal-1106739011-line-2)">--unreleased-version UNRELEASED_VERSION</text><text class="terminal-1106739011-r2" x="732" y="68.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-2)">]</text><text class="terminal-1106739011-r2" x="756.4" y="68.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-2)">[</text><text class="terminal-1106739011-r1" x="768.6" y="68.8" textLength="158.6" clip-path="url(#terminal-1106739011-line-2)">--incremental</text><text class="terminal-1106739011-r2" x="927.2" y="68.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-2)">]</text><text class="terminal-1106739011-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-2)"> +</text><text class="terminal-1106739011-r2" x="244" y="93.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-3)">[</text><text class="terminal-1106739011-r1" x="256.2" y="93.2" textLength="256.2" clip-path="url(#terminal-1106739011-line-3)">--start-rev START_REV</text><text class="terminal-1106739011-r2" x="512.4" y="93.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-3)">]</text><text class="terminal-1106739011-r2" x="536.8" y="93.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-3)">[</text><text class="terminal-1106739011-r1" x="549" y="93.2" textLength="219.6" clip-path="url(#terminal-1106739011-line-3)">--merge-prerelease</text><text class="terminal-1106739011-r2" x="768.6" y="93.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-3)">]</text><text class="terminal-1106739011-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-3)"> +</text><text class="terminal-1106739011-r2" x="244" y="117.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-4)">[</text><text class="terminal-1106739011-r1" x="256.2" y="117.6" textLength="207.4" clip-path="url(#terminal-1106739011-line-4)">--version-scheme </text><text class="terminal-1106739011-r2" x="463.6" y="117.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-4)">{</text><text class="terminal-1106739011-r1" x="475.8" y="117.6" textLength="256.2" clip-path="url(#terminal-1106739011-line-4)">pep440,semver,semver2</text><text class="terminal-1106739011-r2" x="732" y="117.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-4)">}</text><text class="terminal-1106739011-r2" x="744.2" y="117.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-4)">]</text><text class="terminal-1106739011-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-4)"> +</text><text class="terminal-1106739011-r2" x="244" y="142" textLength="12.2" clip-path="url(#terminal-1106739011-line-5)">[</text><text class="terminal-1106739011-r1" x="256.2" y="142" textLength="402.6" clip-path="url(#terminal-1106739011-line-5)">--export-template EXPORT_TEMPLATE</text><text class="terminal-1106739011-r2" x="658.8" y="142" textLength="12.2" clip-path="url(#terminal-1106739011-line-5)">]</text><text class="terminal-1106739011-r2" x="683.2" y="142" textLength="12.2" clip-path="url(#terminal-1106739011-line-5)">[</text><text class="terminal-1106739011-r1" x="695.4" y="142" textLength="231.8" clip-path="url(#terminal-1106739011-line-5)">--template TEMPLATE</text><text class="terminal-1106739011-r2" x="927.2" y="142" textLength="12.2" clip-path="url(#terminal-1106739011-line-5)">]</text><text class="terminal-1106739011-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-1106739011-line-5)"> +</text><text class="terminal-1106739011-r2" x="244" y="166.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-6)">[</text><text class="terminal-1106739011-r1" x="256.2" y="166.4" textLength="158.6" clip-path="url(#terminal-1106739011-line-6)">--extra EXTRA</text><text class="terminal-1106739011-r2" x="414.8" y="166.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-6)">]</text><text class="terminal-1106739011-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-6)"> +</text><text class="terminal-1106739011-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-7)"> +</text><text class="terminal-1106739011-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-8)"> +</text><text class="terminal-1106739011-r1" x="0" y="239.6" textLength="231.8" clip-path="url(#terminal-1106739011-line-9)">generate changelog </text><text class="terminal-1106739011-r2" x="231.8" y="239.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-9)">(</text><text class="terminal-1106739011-r1" x="244" y="239.6" textLength="500.2" clip-path="url(#terminal-1106739011-line-9)">note that it will overwrite existing file</text><text class="terminal-1106739011-r2" x="744.2" y="239.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-9)">)</text><text class="terminal-1106739011-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-9)"> +</text><text class="terminal-1106739011-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-1106739011-line-10)"> +</text><text class="terminal-1106739011-r1" x="0" y="288.4" textLength="256.2" clip-path="url(#terminal-1106739011-line-11)">positional arguments:</text><text class="terminal-1106739011-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-11)"> +</text><text class="terminal-1106739011-r1" x="0" y="312.8" textLength="805.2" clip-path="url(#terminal-1106739011-line-12)">  rev_range             generates changelog for the given version </text><text class="terminal-1106739011-r2" x="805.2" y="312.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-12)">(</text><text class="terminal-1106739011-r1" x="817.4" y="312.8" textLength="61" clip-path="url(#terminal-1106739011-line-12)">e.g: </text><text class="terminal-1106739011-r3" x="878.4" y="312.8" textLength="36.6" clip-path="url(#terminal-1106739011-line-12)">1.5</text><text class="terminal-1106739011-r1" x="915" y="312.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-12)">.</text><text class="terminal-1106739011-r3" x="927.2" y="312.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-12)">3</text><text class="terminal-1106739011-r2" x="939.4" y="312.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-12)">)</text><text class="terminal-1106739011-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-12)"> +</text><text class="terminal-1106739011-r1" x="0" y="337.2" textLength="500.2" clip-path="url(#terminal-1106739011-line-13)">                        or version range </text><text class="terminal-1106739011-r2" x="500.2" y="337.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-13)">(</text><text class="terminal-1106739011-r1" x="512.4" y="337.2" textLength="61" clip-path="url(#terminal-1106739011-line-13)">e.g: </text><text class="terminal-1106739011-r3" x="573.4" y="337.2" textLength="36.6" clip-path="url(#terminal-1106739011-line-13)">1.5</text><text class="terminal-1106739011-r1" x="610" y="337.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-13)">.</text><text class="terminal-1106739011-r3" x="622.2" y="337.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-13)">3</text><text class="terminal-1106739011-r1" x="634.4" y="337.2" textLength="24.4" clip-path="url(#terminal-1106739011-line-13)">..</text><text class="terminal-1106739011-r3" x="658.8" y="337.2" textLength="36.6" clip-path="url(#terminal-1106739011-line-13)">1.7</text><text class="terminal-1106739011-r1" x="695.4" y="337.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-13)">.</text><text class="terminal-1106739011-r3" x="707.6" y="337.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-13)">9</text><text class="terminal-1106739011-r2" x="719.8" y="337.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-13)">)</text><text class="terminal-1106739011-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-13)"> +</text><text class="terminal-1106739011-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-14)"> +</text><text class="terminal-1106739011-r1" x="0" y="386" textLength="97.6" clip-path="url(#terminal-1106739011-line-15)">options:</text><text class="terminal-1106739011-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-1106739011-line-15)"> +</text><text class="terminal-1106739011-r1" x="0" y="410.4" textLength="671" clip-path="url(#terminal-1106739011-line-16)">  -h, --help            show this help message and exit</text><text class="terminal-1106739011-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-16)"> +</text><text class="terminal-1106739011-r1" x="0" y="434.8" textLength="585.6" clip-path="url(#terminal-1106739011-line-17)">  --dry-run             show changelog to stdout</text><text class="terminal-1106739011-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-17)"> +</text><text class="terminal-1106739011-r1" x="0" y="459.2" textLength="280.6" clip-path="url(#terminal-1106739011-line-18)">  --file-name FILE_NAME</text><text class="terminal-1106739011-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-18)"> +</text><text class="terminal-1106739011-r1" x="0" y="483.6" textLength="573.4" clip-path="url(#terminal-1106739011-line-19)">                        file name of changelog </text><text class="terminal-1106739011-r2" x="573.4" y="483.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-19)">(</text><text class="terminal-1106739011-r1" x="585.6" y="483.6" textLength="109.8" clip-path="url(#terminal-1106739011-line-19)">default: </text><text class="terminal-1106739011-r4" x="695.4" y="483.6" textLength="170.8" clip-path="url(#terminal-1106739011-line-19)">'CHANGELOG.md'</text><text class="terminal-1106739011-r2" x="866.2" y="483.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-19)">)</text><text class="terminal-1106739011-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-19)"> +</text><text class="terminal-1106739011-r1" x="0" y="508" textLength="500.2" clip-path="url(#terminal-1106739011-line-20)">  --unreleased-version UNRELEASED_VERSION</text><text class="terminal-1106739011-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-1106739011-line-20)"> +</text><text class="terminal-1106739011-r1" x="0" y="532.4" textLength="707.6" clip-path="url(#terminal-1106739011-line-21)">                        set the value for the new version </text><text class="terminal-1106739011-r2" x="707.6" y="532.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-21)">(</text><text class="terminal-1106739011-r1" x="719.8" y="532.4" textLength="207.4" clip-path="url(#terminal-1106739011-line-21)">use the tag value</text><text class="terminal-1106739011-r2" x="927.2" y="532.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-21)">)</text><text class="terminal-1106739011-r1" x="939.4" y="532.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-21)">,</text><text class="terminal-1106739011-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-21)"> +</text><text class="terminal-1106739011-r1" x="0" y="556.8" textLength="622.2" clip-path="url(#terminal-1106739011-line-22)">                        instead of using unreleased</text><text class="terminal-1106739011-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-22)"> +</text><text class="terminal-1106739011-r1" x="0" y="581.2" textLength="939.4" clip-path="url(#terminal-1106739011-line-23)">  --incremental         generates changelog from last created version, useful</text><text class="terminal-1106739011-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-23)"> +</text><text class="terminal-1106739011-r1" x="0" y="605.6" textLength="817.4" clip-path="url(#terminal-1106739011-line-24)">                        if the changelog has been manually modified</text><text class="terminal-1106739011-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-24)"> +</text><text class="terminal-1106739011-r1" x="0" y="630" textLength="280.6" clip-path="url(#terminal-1106739011-line-25)">  --start-rev START_REV</text><text class="terminal-1106739011-r1" x="976" y="630" textLength="12.2" clip-path="url(#terminal-1106739011-line-25)"> +</text><text class="terminal-1106739011-r1" x="0" y="654.4" textLength="866.2" clip-path="url(#terminal-1106739011-line-26)">                        start rev of the changelog. If not set, it will</text><text class="terminal-1106739011-r1" x="976" y="654.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-26)"> +</text><text class="terminal-1106739011-r1" x="0" y="678.8" textLength="695.4" clip-path="url(#terminal-1106739011-line-27)">                        generate changelog from the start</text><text class="terminal-1106739011-r1" x="976" y="678.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-27)"> +</text><text class="terminal-1106739011-r1" x="0" y="703.2" textLength="915" clip-path="url(#terminal-1106739011-line-28)">  --merge-prerelease    collect all changes from prereleases into next non-</text><text class="terminal-1106739011-r1" x="976" y="703.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-28)"> +</text><text class="terminal-1106739011-r1" x="0" y="727.6" textLength="951.6" clip-path="url(#terminal-1106739011-line-29)">                        prerelease. If not set, it will include prereleases in</text><text class="terminal-1106739011-r1" x="976" y="727.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-29)"> +</text><text class="terminal-1106739011-r1" x="0" y="752" textLength="451.4" clip-path="url(#terminal-1106739011-line-30)">                        the changelog</text><text class="terminal-1106739011-r1" x="976" y="752" textLength="12.2" clip-path="url(#terminal-1106739011-line-30)"> +</text><text class="terminal-1106739011-r1" x="0" y="776.4" textLength="231.8" clip-path="url(#terminal-1106739011-line-31)">  --version-scheme </text><text class="terminal-1106739011-r2" x="231.8" y="776.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-31)">{</text><text class="terminal-1106739011-r1" x="244" y="776.4" textLength="256.2" clip-path="url(#terminal-1106739011-line-31)">pep440,semver,semver2</text><text class="terminal-1106739011-r2" x="500.2" y="776.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-31)">}</text><text class="terminal-1106739011-r1" x="976" y="776.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-31)"> +</text><text class="terminal-1106739011-r1" x="0" y="800.8" textLength="549" clip-path="url(#terminal-1106739011-line-32)">                        choose version scheme</text><text class="terminal-1106739011-r1" x="976" y="800.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-32)"> +</text><text class="terminal-1106739011-r1" x="0" y="825.2" textLength="427" clip-path="url(#terminal-1106739011-line-33)">  --export-template EXPORT_TEMPLATE</text><text class="terminal-1106739011-r1" x="976" y="825.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-33)"> +</text><text class="terminal-1106739011-r1" x="0" y="849.6" textLength="927.2" clip-path="url(#terminal-1106739011-line-34)">                        Export the changelog template into this file instead</text><text class="terminal-1106739011-r1" x="976" y="849.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-34)"> +</text><text class="terminal-1106739011-r1" x="0" y="874" textLength="475.8" clip-path="url(#terminal-1106739011-line-35)">                        of rendering it</text><text class="terminal-1106739011-r1" x="976" y="874" textLength="12.2" clip-path="url(#terminal-1106739011-line-35)"> +</text><text class="terminal-1106739011-r1" x="0" y="898.4" textLength="305" clip-path="url(#terminal-1106739011-line-36)">  --template, -t TEMPLATE</text><text class="terminal-1106739011-r1" x="976" y="898.4" textLength="12.2" clip-path="url(#terminal-1106739011-line-36)"> +</text><text class="terminal-1106739011-r1" x="0" y="922.8" textLength="646.6" clip-path="url(#terminal-1106739011-line-37)">                        changelog template file name </text><text class="terminal-1106739011-r2" x="646.6" y="922.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-37)">(</text><text class="terminal-1106739011-r1" x="658.8" y="922.8" textLength="280.6" clip-path="url(#terminal-1106739011-line-37)">relative to the current</text><text class="terminal-1106739011-r1" x="976" y="922.8" textLength="12.2" clip-path="url(#terminal-1106739011-line-37)"> +</text><text class="terminal-1106739011-r1" x="0" y="947.2" textLength="500.2" clip-path="url(#terminal-1106739011-line-38)">                        working directory</text><text class="terminal-1106739011-r2" x="500.2" y="947.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-38)">)</text><text class="terminal-1106739011-r1" x="976" y="947.2" textLength="12.2" clip-path="url(#terminal-1106739011-line-38)"> +</text><text class="terminal-1106739011-r1" x="0" y="971.6" textLength="622.2" clip-path="url(#terminal-1106739011-line-39)">  --extra, -e EXTRA     a changelog extra variable </text><text class="terminal-1106739011-r2" x="622.2" y="971.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-39)">(</text><text class="terminal-1106739011-r1" x="634.4" y="971.6" textLength="146.4" clip-path="url(#terminal-1106739011-line-39)">in the form </text><text class="terminal-1106739011-r4" x="780.8" y="971.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-39)">'</text><text class="terminal-1106739011-r4" x="793" y="971.6" textLength="36.6" clip-path="url(#terminal-1106739011-line-39)">key</text><text class="terminal-1106739011-r4" x="829.6" y="971.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-39)">=</text><text class="terminal-1106739011-r4" x="841.8" y="971.6" textLength="61" clip-path="url(#terminal-1106739011-line-39)">value</text><text class="terminal-1106739011-r4" x="902.8" y="971.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-39)">'</text><text class="terminal-1106739011-r2" x="915" y="971.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-39)">)</text><text class="terminal-1106739011-r1" x="976" y="971.6" textLength="12.2" clip-path="url(#terminal-1106739011-line-39)"> +</text><text class="terminal-1106739011-r1" x="976" y="996" textLength="12.2" clip-path="url(#terminal-1106739011-line-40)"> +</text> + </g> + </g> +</svg> diff --git a/docs/images/cli_help/cz_check___help.svg b/docs/images/cli_help/cz_check___help.svg new file mode 100644 index 0000000..690bfec --- /dev/null +++ b/docs/images/cli_help/cz_check___help.svg @@ -0,0 +1,165 @@ +<svg class="rich-terminal" viewBox="0 0 994 708.8" xmlns="http://www.w3.org/2000/svg"> + <!-- Generated with Rich https://www.textualize.io --> + <style> + + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Regular"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); + font-style: normal; + font-weight: 400; + } + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Bold"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); + font-style: bold; + font-weight: 700; + } + + .terminal-1360575461-matrix { + font-family: Fira Code, monospace; + font-size: 20px; + line-height: 24.4px; + font-variant-east-asian: full-width; + } + + .terminal-1360575461-title { + font-size: 18px; + font-weight: bold; + font-family: arial; + } + + .terminal-1360575461-r1 { fill: #c5c8c6 } +.terminal-1360575461-r2 { fill: #c5c8c6;font-weight: bold } +.terminal-1360575461-r3 { fill: #d0b344 } +.terminal-1360575461-r4 { fill: #68a0b3;font-weight: bold } + </style> + + <defs> + <clipPath id="terminal-1360575461-clip-terminal"> + <rect x="0" y="0" width="975.0" height="657.8" /> + </clipPath> + <clipPath id="terminal-1360575461-line-0"> + <rect x="0" y="1.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-1"> + <rect x="0" y="25.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-2"> + <rect x="0" y="50.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-3"> + <rect x="0" y="74.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-4"> + <rect x="0" y="99.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-5"> + <rect x="0" y="123.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-6"> + <rect x="0" y="147.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-7"> + <rect x="0" y="172.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-8"> + <rect x="0" y="196.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-9"> + <rect x="0" y="221.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-10"> + <rect x="0" y="245.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-11"> + <rect x="0" y="269.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-12"> + <rect x="0" y="294.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-13"> + <rect x="0" y="318.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-14"> + <rect x="0" y="343.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-15"> + <rect x="0" y="367.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-16"> + <rect x="0" y="391.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-17"> + <rect x="0" y="416.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-18"> + <rect x="0" y="440.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-19"> + <rect x="0" y="465.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-20"> + <rect x="0" y="489.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-21"> + <rect x="0" y="513.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-22"> + <rect x="0" y="538.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-23"> + <rect x="0" y="562.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-24"> + <rect x="0" y="587.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1360575461-line-25"> + <rect x="0" y="611.5" width="976" height="24.65"/> + </clipPath> + </defs> + + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="706.8" rx="8"/> + <g transform="translate(26,22)"> + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> + <circle cx="22" cy="0" r="7" fill="#febc2e"/> + <circle cx="44" cy="0" r="7" fill="#28c840"/> + </g> + + <g transform="translate(9, 41)" clip-path="url(#terminal-1360575461-clip-terminal)"> + + <g class="terminal-1360575461-matrix"> + <text class="terminal-1360575461-r1" x="0" y="20" textLength="207.4" clip-path="url(#terminal-1360575461-line-0)">$ cz check --help</text><text class="terminal-1360575461-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-1360575461-line-0)"> +</text><text class="terminal-1360575461-r1" x="0" y="44.4" textLength="195.2" clip-path="url(#terminal-1360575461-line-1)">usage: cz check </text><text class="terminal-1360575461-r2" x="195.2" y="44.4" textLength="12.2" clip-path="url(#terminal-1360575461-line-1)">[</text><text class="terminal-1360575461-r1" x="207.4" y="44.4" textLength="24.4" clip-path="url(#terminal-1360575461-line-1)">-h</text><text class="terminal-1360575461-r2" x="231.8" y="44.4" textLength="12.2" clip-path="url(#terminal-1360575461-line-1)">]</text><text class="terminal-1360575461-r2" x="256.2" y="44.4" textLength="12.2" clip-path="url(#terminal-1360575461-line-1)">[</text><text class="terminal-1360575461-r1" x="268.4" y="44.4" textLength="427" clip-path="url(#terminal-1360575461-line-1)">--commit-msg-file COMMIT_MSG_FILE |</text><text class="terminal-1360575461-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-1360575461-line-1)"> +</text><text class="terminal-1360575461-r1" x="0" y="68.8" textLength="610" clip-path="url(#terminal-1360575461-line-2)">                --rev-range REV_RANGE | -m MESSAGE</text><text class="terminal-1360575461-r2" x="610" y="68.8" textLength="12.2" clip-path="url(#terminal-1360575461-line-2)">]</text><text class="terminal-1360575461-r2" x="634.4" y="68.8" textLength="12.2" clip-path="url(#terminal-1360575461-line-2)">[</text><text class="terminal-1360575461-r1" x="646.6" y="68.8" textLength="158.6" clip-path="url(#terminal-1360575461-line-2)">--allow-abort</text><text class="terminal-1360575461-r2" x="805.2" y="68.8" textLength="12.2" clip-path="url(#terminal-1360575461-line-2)">]</text><text class="terminal-1360575461-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-1360575461-line-2)"> +</text><text class="terminal-1360575461-r2" x="195.2" y="93.2" textLength="12.2" clip-path="url(#terminal-1360575461-line-3)">[</text><text class="terminal-1360575461-r1" x="207.4" y="93.2" textLength="231.8" clip-path="url(#terminal-1360575461-line-3)">--allowed-prefixes </text><text class="terminal-1360575461-r2" x="439.2" y="93.2" textLength="12.2" clip-path="url(#terminal-1360575461-line-3)">[</text><text class="terminal-1360575461-r1" x="451.4" y="93.2" textLength="207.4" clip-path="url(#terminal-1360575461-line-3)">ALLOWED_PREFIXES </text><text class="terminal-1360575461-r3" x="658.8" y="93.2" textLength="36.6" clip-path="url(#terminal-1360575461-line-3)">...</text><text class="terminal-1360575461-r2" x="695.4" y="93.2" textLength="12.2" clip-path="url(#terminal-1360575461-line-3)">]</text><text class="terminal-1360575461-r2" x="707.6" y="93.2" textLength="12.2" clip-path="url(#terminal-1360575461-line-3)">]</text><text class="terminal-1360575461-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-1360575461-line-3)"> +</text><text class="terminal-1360575461-r2" x="195.2" y="117.6" textLength="12.2" clip-path="url(#terminal-1360575461-line-4)">[</text><text class="terminal-1360575461-r1" x="207.4" y="117.6" textLength="280.6" clip-path="url(#terminal-1360575461-line-4)">-l MESSAGE_LENGTH_LIMIT</text><text class="terminal-1360575461-r2" x="488" y="117.6" textLength="12.2" clip-path="url(#terminal-1360575461-line-4)">]</text><text class="terminal-1360575461-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-1360575461-line-4)"> +</text><text class="terminal-1360575461-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-1360575461-line-5)"> +</text><text class="terminal-1360575461-r1" x="0" y="166.4" textLength="744.2" clip-path="url(#terminal-1360575461-line-6)">validates that a commit message matches the commitizen schema</text><text class="terminal-1360575461-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-1360575461-line-6)"> +</text><text class="terminal-1360575461-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-1360575461-line-7)"> +</text><text class="terminal-1360575461-r1" x="0" y="215.2" textLength="97.6" clip-path="url(#terminal-1360575461-line-8)">options:</text><text class="terminal-1360575461-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-1360575461-line-8)"> +</text><text class="terminal-1360575461-r1" x="0" y="239.6" textLength="671" clip-path="url(#terminal-1360575461-line-9)">  -h, --help            show this help message and exit</text><text class="terminal-1360575461-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-1360575461-line-9)"> +</text><text class="terminal-1360575461-r1" x="0" y="264" textLength="427" clip-path="url(#terminal-1360575461-line-10)">  --commit-msg-file COMMIT_MSG_FILE</text><text class="terminal-1360575461-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-1360575461-line-10)"> +</text><text class="terminal-1360575461-r1" x="0" y="288.4" textLength="915" clip-path="url(#terminal-1360575461-line-11)">                        ask for the name of the temporal file that contains</text><text class="terminal-1360575461-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-1360575461-line-11)"> +</text><text class="terminal-1360575461-r1" x="0" y="312.8" textLength="902.8" clip-path="url(#terminal-1360575461-line-12)">                        the commit message. Using it in a git hook script:</text><text class="terminal-1360575461-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-1360575461-line-12)"> +</text><text class="terminal-1360575461-r3" x="292.8" y="337.2" textLength="97.6" clip-path="url(#terminal-1360575461-line-13)">MSG_FILE</text><text class="terminal-1360575461-r1" x="390.4" y="337.2" textLength="24.4" clip-path="url(#terminal-1360575461-line-13)">=$</text><text class="terminal-1360575461-r4" x="414.8" y="337.2" textLength="12.2" clip-path="url(#terminal-1360575461-line-13)">1</text><text class="terminal-1360575461-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-1360575461-line-13)"> +</text><text class="terminal-1360575461-r1" x="0" y="361.6" textLength="280.6" clip-path="url(#terminal-1360575461-line-14)">  --rev-range REV_RANGE</text><text class="terminal-1360575461-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-1360575461-line-14)"> +</text><text class="terminal-1360575461-r1" x="0" y="386" textLength="854" clip-path="url(#terminal-1360575461-line-15)">                        a range of git rev to check. e.g, master..HEAD</text><text class="terminal-1360575461-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-1360575461-line-15)"> +</text><text class="terminal-1360575461-r1" x="0" y="410.4" textLength="280.6" clip-path="url(#terminal-1360575461-line-16)">  -m, --message MESSAGE</text><text class="terminal-1360575461-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-1360575461-line-16)"> +</text><text class="terminal-1360575461-r1" x="0" y="434.8" textLength="768.6" clip-path="url(#terminal-1360575461-line-17)">                        commit message that needs to be checked</text><text class="terminal-1360575461-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-1360575461-line-17)"> +</text><text class="terminal-1360575461-r1" x="0" y="459.2" textLength="927.2" clip-path="url(#terminal-1360575461-line-18)">  --allow-abort         allow empty commit messages, which typically abort a</text><text class="terminal-1360575461-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-1360575461-line-18)"> +</text><text class="terminal-1360575461-r1" x="0" y="483.6" textLength="366" clip-path="url(#terminal-1360575461-line-19)">                        commit</text><text class="terminal-1360575461-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-1360575461-line-19)"> +</text><text class="terminal-1360575461-r1" x="0" y="508" textLength="256.2" clip-path="url(#terminal-1360575461-line-20)">  --allowed-prefixes </text><text class="terminal-1360575461-r2" x="256.2" y="508" textLength="12.2" clip-path="url(#terminal-1360575461-line-20)">[</text><text class="terminal-1360575461-r1" x="268.4" y="508" textLength="207.4" clip-path="url(#terminal-1360575461-line-20)">ALLOWED_PREFIXES </text><text class="terminal-1360575461-r3" x="475.8" y="508" textLength="36.6" clip-path="url(#terminal-1360575461-line-20)">...</text><text class="terminal-1360575461-r2" x="512.4" y="508" textLength="12.2" clip-path="url(#terminal-1360575461-line-20)">]</text><text class="terminal-1360575461-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-1360575461-line-20)"> +</text><text class="terminal-1360575461-r1" x="0" y="532.4" textLength="951.6" clip-path="url(#terminal-1360575461-line-21)">                        allowed commit message prefixes. If the message starts</text><text class="terminal-1360575461-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-1360575461-line-21)"> +</text><text class="terminal-1360575461-r1" x="0" y="556.8" textLength="951.6" clip-path="url(#terminal-1360575461-line-22)">                        by one of these prefixes, the message won't be checked</text><text class="terminal-1360575461-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-1360575461-line-22)"> +</text><text class="terminal-1360575461-r1" x="0" y="581.2" textLength="500.2" clip-path="url(#terminal-1360575461-line-23)">                        against the regex</text><text class="terminal-1360575461-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-1360575461-line-23)"> +</text><text class="terminal-1360575461-r1" x="0" y="605.6" textLength="597.8" clip-path="url(#terminal-1360575461-line-24)">  -l, --message-length-limit MESSAGE_LENGTH_LIMIT</text><text class="terminal-1360575461-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-1360575461-line-24)"> +</text><text class="terminal-1360575461-r1" x="0" y="630" textLength="732" clip-path="url(#terminal-1360575461-line-25)">                        length limit of the commit message; </text><text class="terminal-1360575461-r4" x="732" y="630" textLength="12.2" clip-path="url(#terminal-1360575461-line-25)">0</text><text class="terminal-1360575461-r1" x="744.2" y="630" textLength="158.6" clip-path="url(#terminal-1360575461-line-25)"> for no limit</text><text class="terminal-1360575461-r1" x="976" y="630" textLength="12.2" clip-path="url(#terminal-1360575461-line-25)"> +</text><text class="terminal-1360575461-r1" x="976" y="654.4" textLength="12.2" clip-path="url(#terminal-1360575461-line-26)"> +</text> + </g> + </g> +</svg> diff --git a/docs/images/cli_help/cz_commit___help.svg b/docs/images/cli_help/cz_commit___help.svg new file mode 100644 index 0000000..5aea022 --- /dev/null +++ b/docs/images/cli_help/cz_commit___help.svg @@ -0,0 +1,152 @@ +<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg"> + <!-- Generated with Rich https://www.textualize.io --> + <style> + + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Regular"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); + font-style: normal; + font-weight: 400; + } + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Bold"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); + font-style: bold; + font-weight: 700; + } + + .terminal-463778956-matrix { + font-family: Fira Code, monospace; + font-size: 20px; + line-height: 24.4px; + font-variant-east-asian: full-width; + } + + .terminal-463778956-title { + font-size: 18px; + font-weight: bold; + font-family: arial; + } + + .terminal-463778956-r1 { fill: #c5c8c6 } +.terminal-463778956-r2 { fill: #c5c8c6;font-weight: bold } +.terminal-463778956-r3 { fill: #68a0b3;font-weight: bold } + </style> + + <defs> + <clipPath id="terminal-463778956-clip-terminal"> + <rect x="0" y="0" width="975.0" height="584.5999999999999" /> + </clipPath> + <clipPath id="terminal-463778956-line-0"> + <rect x="0" y="1.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-1"> + <rect x="0" y="25.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-2"> + <rect x="0" y="50.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-3"> + <rect x="0" y="74.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-4"> + <rect x="0" y="99.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-5"> + <rect x="0" y="123.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-6"> + <rect x="0" y="147.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-7"> + <rect x="0" y="172.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-8"> + <rect x="0" y="196.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-9"> + <rect x="0" y="221.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-10"> + <rect x="0" y="245.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-11"> + <rect x="0" y="269.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-12"> + <rect x="0" y="294.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-13"> + <rect x="0" y="318.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-14"> + <rect x="0" y="343.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-15"> + <rect x="0" y="367.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-16"> + <rect x="0" y="391.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-17"> + <rect x="0" y="416.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-18"> + <rect x="0" y="440.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-19"> + <rect x="0" y="465.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-20"> + <rect x="0" y="489.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-21"> + <rect x="0" y="513.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-463778956-line-22"> + <rect x="0" y="538.3" width="976" height="24.65"/> + </clipPath> + </defs> + + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/> + <g transform="translate(26,22)"> + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> + <circle cx="22" cy="0" r="7" fill="#febc2e"/> + <circle cx="44" cy="0" r="7" fill="#28c840"/> + </g> + + <g transform="translate(9, 41)" clip-path="url(#terminal-463778956-clip-terminal)"> + + <g class="terminal-463778956-matrix"> + <text class="terminal-463778956-r1" x="0" y="20" textLength="219.6" clip-path="url(#terminal-463778956-line-0)">$ cz commit --help</text><text class="terminal-463778956-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-463778956-line-0)"> +</text><text class="terminal-463778956-r1" x="0" y="44.4" textLength="207.4" clip-path="url(#terminal-463778956-line-1)">usage: cz commit </text><text class="terminal-463778956-r2" x="207.4" y="44.4" textLength="12.2" clip-path="url(#terminal-463778956-line-1)">[</text><text class="terminal-463778956-r1" x="219.6" y="44.4" textLength="24.4" clip-path="url(#terminal-463778956-line-1)">-h</text><text class="terminal-463778956-r2" x="244" y="44.4" textLength="12.2" clip-path="url(#terminal-463778956-line-1)">]</text><text class="terminal-463778956-r2" x="268.4" y="44.4" textLength="12.2" clip-path="url(#terminal-463778956-line-1)">[</text><text class="terminal-463778956-r1" x="280.6" y="44.4" textLength="85.4" clip-path="url(#terminal-463778956-line-1)">--retry</text><text class="terminal-463778956-r2" x="366" y="44.4" textLength="12.2" clip-path="url(#terminal-463778956-line-1)">]</text><text class="terminal-463778956-r2" x="390.4" y="44.4" textLength="12.2" clip-path="url(#terminal-463778956-line-1)">[</text><text class="terminal-463778956-r1" x="402.6" y="44.4" textLength="122" clip-path="url(#terminal-463778956-line-1)">--no-retry</text><text class="terminal-463778956-r2" x="524.6" y="44.4" textLength="12.2" clip-path="url(#terminal-463778956-line-1)">]</text><text class="terminal-463778956-r2" x="549" y="44.4" textLength="12.2" clip-path="url(#terminal-463778956-line-1)">[</text><text class="terminal-463778956-r1" x="561.2" y="44.4" textLength="109.8" clip-path="url(#terminal-463778956-line-1)">--dry-run</text><text class="terminal-463778956-r2" x="671" y="44.4" textLength="12.2" clip-path="url(#terminal-463778956-line-1)">]</text><text class="terminal-463778956-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-463778956-line-1)"> +</text><text class="terminal-463778956-r2" x="207.4" y="68.8" textLength="12.2" clip-path="url(#terminal-463778956-line-2)">[</text><text class="terminal-463778956-r1" x="219.6" y="68.8" textLength="402.6" clip-path="url(#terminal-463778956-line-2)">--write-message-to-file FILE_PATH</text><text class="terminal-463778956-r2" x="622.2" y="68.8" textLength="12.2" clip-path="url(#terminal-463778956-line-2)">]</text><text class="terminal-463778956-r2" x="646.6" y="68.8" textLength="12.2" clip-path="url(#terminal-463778956-line-2)">[</text><text class="terminal-463778956-r1" x="658.8" y="68.8" textLength="24.4" clip-path="url(#terminal-463778956-line-2)">-s</text><text class="terminal-463778956-r2" x="683.2" y="68.8" textLength="12.2" clip-path="url(#terminal-463778956-line-2)">]</text><text class="terminal-463778956-r2" x="707.6" y="68.8" textLength="12.2" clip-path="url(#terminal-463778956-line-2)">[</text><text class="terminal-463778956-r1" x="719.8" y="68.8" textLength="24.4" clip-path="url(#terminal-463778956-line-2)">-a</text><text class="terminal-463778956-r2" x="744.2" y="68.8" textLength="12.2" clip-path="url(#terminal-463778956-line-2)">]</text><text class="terminal-463778956-r2" x="768.6" y="68.8" textLength="12.2" clip-path="url(#terminal-463778956-line-2)">[</text><text class="terminal-463778956-r1" x="780.8" y="68.8" textLength="24.4" clip-path="url(#terminal-463778956-line-2)">-e</text><text class="terminal-463778956-r2" x="805.2" y="68.8" textLength="12.2" clip-path="url(#terminal-463778956-line-2)">]</text><text class="terminal-463778956-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-463778956-line-2)"> +</text><text class="terminal-463778956-r2" x="207.4" y="93.2" textLength="12.2" clip-path="url(#terminal-463778956-line-3)">[</text><text class="terminal-463778956-r1" x="219.6" y="93.2" textLength="280.6" clip-path="url(#terminal-463778956-line-3)">-l MESSAGE_LENGTH_LIMIT</text><text class="terminal-463778956-r2" x="500.2" y="93.2" textLength="12.2" clip-path="url(#terminal-463778956-line-3)">]</text><text class="terminal-463778956-r2" x="524.6" y="93.2" textLength="12.2" clip-path="url(#terminal-463778956-line-3)">[</text><text class="terminal-463778956-r1" x="536.8" y="93.2" textLength="24.4" clip-path="url(#terminal-463778956-line-3)">--</text><text class="terminal-463778956-r2" x="561.2" y="93.2" textLength="12.2" clip-path="url(#terminal-463778956-line-3)">]</text><text class="terminal-463778956-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-463778956-line-3)"> +</text><text class="terminal-463778956-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-463778956-line-4)"> +</text><text class="terminal-463778956-r1" x="0" y="142" textLength="207.4" clip-path="url(#terminal-463778956-line-5)">create new commit</text><text class="terminal-463778956-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-463778956-line-5)"> +</text><text class="terminal-463778956-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-463778956-line-6)"> +</text><text class="terminal-463778956-r1" x="0" y="190.8" textLength="97.6" clip-path="url(#terminal-463778956-line-7)">options:</text><text class="terminal-463778956-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-463778956-line-7)"> +</text><text class="terminal-463778956-r1" x="0" y="215.2" textLength="671" clip-path="url(#terminal-463778956-line-8)">  -h, --help            show this help message and exit</text><text class="terminal-463778956-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-463778956-line-8)"> +</text><text class="terminal-463778956-r1" x="0" y="239.6" textLength="500.2" clip-path="url(#terminal-463778956-line-9)">  --retry               retry last commit</text><text class="terminal-463778956-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-463778956-line-9)"> +</text><text class="terminal-463778956-r1" x="0" y="264" textLength="878.4" clip-path="url(#terminal-463778956-line-10)">  --no-retry            skip retry if retry_after_failure is set to true</text><text class="terminal-463778956-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-463778956-line-10)"> +</text><text class="terminal-463778956-r1" x="0" y="288.4" textLength="915" clip-path="url(#terminal-463778956-line-11)">  --dry-run             show output to stdout, no commit, no modified files</text><text class="terminal-463778956-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-463778956-line-11)"> +</text><text class="terminal-463778956-r1" x="0" y="312.8" textLength="427" clip-path="url(#terminal-463778956-line-12)">  --write-message-to-file FILE_PATH</text><text class="terminal-463778956-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-463778956-line-12)"> +</text><text class="terminal-463778956-r1" x="0" y="337.2" textLength="780.8" clip-path="url(#terminal-463778956-line-13)">                        write message to file before committing </text><text class="terminal-463778956-r2" x="780.8" y="337.2" textLength="12.2" clip-path="url(#terminal-463778956-line-13)">(</text><text class="terminal-463778956-r1" x="793" y="337.2" textLength="73.2" clip-path="url(#terminal-463778956-line-13)">can be</text><text class="terminal-463778956-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-463778956-line-13)"> +</text><text class="terminal-463778956-r1" x="0" y="361.6" textLength="573.4" clip-path="url(#terminal-463778956-line-14)">                        combined with --dry-run</text><text class="terminal-463778956-r2" x="573.4" y="361.6" textLength="12.2" clip-path="url(#terminal-463778956-line-14)">)</text><text class="terminal-463778956-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-463778956-line-14)"> +</text><text class="terminal-463778956-r1" x="0" y="386" textLength="524.6" clip-path="url(#terminal-463778956-line-15)">  -s, --signoff         sign off the commit</text><text class="terminal-463778956-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-463778956-line-15)"> +</text><text class="terminal-463778956-r1" x="0" y="410.4" textLength="902.8" clip-path="url(#terminal-463778956-line-16)">  -a, --all             Tell the command to automatically stage files that</text><text class="terminal-463778956-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-463778956-line-16)"> +</text><text class="terminal-463778956-r1" x="0" y="434.8" textLength="951.6" clip-path="url(#terminal-463778956-line-17)">                        have been modified and deleted, but new files you have</text><text class="terminal-463778956-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-463778956-line-17)"> +</text><text class="terminal-463778956-r1" x="0" y="459.2" textLength="732" clip-path="url(#terminal-463778956-line-18)">                        not told Git about are not affected.</text><text class="terminal-463778956-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-463778956-line-18)"> +</text><text class="terminal-463778956-r1" x="0" y="483.6" textLength="793" clip-path="url(#terminal-463778956-line-19)">  -e, --edit            edit the commit message before committing</text><text class="terminal-463778956-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-463778956-line-19)"> +</text><text class="terminal-463778956-r1" x="0" y="508" textLength="597.8" clip-path="url(#terminal-463778956-line-20)">  -l, --message-length-limit MESSAGE_LENGTH_LIMIT</text><text class="terminal-463778956-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-463778956-line-20)"> +</text><text class="terminal-463778956-r1" x="0" y="532.4" textLength="732" clip-path="url(#terminal-463778956-line-21)">                        length limit of the commit message; </text><text class="terminal-463778956-r3" x="732" y="532.4" textLength="12.2" clip-path="url(#terminal-463778956-line-21)">0</text><text class="terminal-463778956-r1" x="744.2" y="532.4" textLength="158.6" clip-path="url(#terminal-463778956-line-21)"> for no limit</text><text class="terminal-463778956-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-463778956-line-21)"> +</text><text class="terminal-463778956-r1" x="0" y="556.8" textLength="671" clip-path="url(#terminal-463778956-line-22)">  --                    Positional arguments separator </text><text class="terminal-463778956-r2" x="671" y="556.8" textLength="12.2" clip-path="url(#terminal-463778956-line-22)">(</text><text class="terminal-463778956-r1" x="683.2" y="556.8" textLength="134.2" clip-path="url(#terminal-463778956-line-22)">recommended</text><text class="terminal-463778956-r2" x="817.4" y="556.8" textLength="12.2" clip-path="url(#terminal-463778956-line-22)">)</text><text class="terminal-463778956-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-463778956-line-22)"> +</text><text class="terminal-463778956-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-463778956-line-23)"> +</text> + </g> + </g> +</svg> diff --git a/docs/images/cli_help/cz_example___help.svg b/docs/images/cli_help/cz_example___help.svg new file mode 100644 index 0000000..9fe4fd6 --- /dev/null +++ b/docs/images/cli_help/cz_example___help.svg @@ -0,0 +1,87 @@ +<svg class="rich-terminal" viewBox="0 0 994 245.2" xmlns="http://www.w3.org/2000/svg"> + <!-- Generated with Rich https://www.textualize.io --> + <style> + + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Regular"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); + font-style: normal; + font-weight: 400; + } + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Bold"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); + font-style: bold; + font-weight: 700; + } + + .terminal-1643610534-matrix { + font-family: Fira Code, monospace; + font-size: 20px; + line-height: 24.4px; + font-variant-east-asian: full-width; + } + + .terminal-1643610534-title { + font-size: 18px; + font-weight: bold; + font-family: arial; + } + + .terminal-1643610534-r1 { fill: #c5c8c6 } +.terminal-1643610534-r2 { fill: #c5c8c6;font-weight: bold } + </style> + + <defs> + <clipPath id="terminal-1643610534-clip-terminal"> + <rect x="0" y="0" width="975.0" height="194.2" /> + </clipPath> + <clipPath id="terminal-1643610534-line-0"> + <rect x="0" y="1.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1643610534-line-1"> + <rect x="0" y="25.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1643610534-line-2"> + <rect x="0" y="50.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1643610534-line-3"> + <rect x="0" y="74.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1643610534-line-4"> + <rect x="0" y="99.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1643610534-line-5"> + <rect x="0" y="123.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1643610534-line-6"> + <rect x="0" y="147.9" width="976" height="24.65"/> + </clipPath> + </defs> + + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="243.2" rx="8"/> + <g transform="translate(26,22)"> + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> + <circle cx="22" cy="0" r="7" fill="#febc2e"/> + <circle cx="44" cy="0" r="7" fill="#28c840"/> + </g> + + <g transform="translate(9, 41)" clip-path="url(#terminal-1643610534-clip-terminal)"> + + <g class="terminal-1643610534-matrix"> + <text class="terminal-1643610534-r1" x="0" y="20" textLength="231.8" clip-path="url(#terminal-1643610534-line-0)">$ cz example --help</text><text class="terminal-1643610534-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-1643610534-line-0)"> +</text><text class="terminal-1643610534-r1" x="0" y="44.4" textLength="219.6" clip-path="url(#terminal-1643610534-line-1)">usage: cz example </text><text class="terminal-1643610534-r2" x="219.6" y="44.4" textLength="12.2" clip-path="url(#terminal-1643610534-line-1)">[</text><text class="terminal-1643610534-r1" x="231.8" y="44.4" textLength="24.4" clip-path="url(#terminal-1643610534-line-1)">-h</text><text class="terminal-1643610534-r2" x="256.2" y="44.4" textLength="12.2" clip-path="url(#terminal-1643610534-line-1)">]</text><text class="terminal-1643610534-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-1643610534-line-1)"> +</text><text class="terminal-1643610534-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-1643610534-line-2)"> +</text><text class="terminal-1643610534-r1" x="0" y="93.2" textLength="231.8" clip-path="url(#terminal-1643610534-line-3)">show commit example</text><text class="terminal-1643610534-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-1643610534-line-3)"> +</text><text class="terminal-1643610534-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-1643610534-line-4)"> +</text><text class="terminal-1643610534-r1" x="0" y="142" textLength="97.6" clip-path="url(#terminal-1643610534-line-5)">options:</text><text class="terminal-1643610534-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-1643610534-line-5)"> +</text><text class="terminal-1643610534-r1" x="0" y="166.4" textLength="549" clip-path="url(#terminal-1643610534-line-6)">  -h, --help  show this help message and exit</text><text class="terminal-1643610534-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-1643610534-line-6)"> +</text><text class="terminal-1643610534-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-1643610534-line-7)"> +</text> + </g> + </g> +</svg> diff --git a/docs/images/cli_help/cz_info___help.svg b/docs/images/cli_help/cz_info___help.svg new file mode 100644 index 0000000..b8827e3 --- /dev/null +++ b/docs/images/cli_help/cz_info___help.svg @@ -0,0 +1,87 @@ +<svg class="rich-terminal" viewBox="0 0 994 245.2" xmlns="http://www.w3.org/2000/svg"> + <!-- Generated with Rich https://www.textualize.io --> + <style> + + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Regular"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); + font-style: normal; + font-weight: 400; + } + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Bold"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); + font-style: bold; + font-weight: 700; + } + + .terminal-4196041424-matrix { + font-family: Fira Code, monospace; + font-size: 20px; + line-height: 24.4px; + font-variant-east-asian: full-width; + } + + .terminal-4196041424-title { + font-size: 18px; + font-weight: bold; + font-family: arial; + } + + .terminal-4196041424-r1 { fill: #c5c8c6 } +.terminal-4196041424-r2 { fill: #c5c8c6;font-weight: bold } + </style> + + <defs> + <clipPath id="terminal-4196041424-clip-terminal"> + <rect x="0" y="0" width="975.0" height="194.2" /> + </clipPath> + <clipPath id="terminal-4196041424-line-0"> + <rect x="0" y="1.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4196041424-line-1"> + <rect x="0" y="25.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4196041424-line-2"> + <rect x="0" y="50.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4196041424-line-3"> + <rect x="0" y="74.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4196041424-line-4"> + <rect x="0" y="99.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4196041424-line-5"> + <rect x="0" y="123.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4196041424-line-6"> + <rect x="0" y="147.9" width="976" height="24.65"/> + </clipPath> + </defs> + + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="243.2" rx="8"/> + <g transform="translate(26,22)"> + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> + <circle cx="22" cy="0" r="7" fill="#febc2e"/> + <circle cx="44" cy="0" r="7" fill="#28c840"/> + </g> + + <g transform="translate(9, 41)" clip-path="url(#terminal-4196041424-clip-terminal)"> + + <g class="terminal-4196041424-matrix"> + <text class="terminal-4196041424-r1" x="0" y="20" textLength="195.2" clip-path="url(#terminal-4196041424-line-0)">$ cz info --help</text><text class="terminal-4196041424-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-4196041424-line-0)"> +</text><text class="terminal-4196041424-r1" x="0" y="44.4" textLength="183" clip-path="url(#terminal-4196041424-line-1)">usage: cz info </text><text class="terminal-4196041424-r2" x="183" y="44.4" textLength="12.2" clip-path="url(#terminal-4196041424-line-1)">[</text><text class="terminal-4196041424-r1" x="195.2" y="44.4" textLength="24.4" clip-path="url(#terminal-4196041424-line-1)">-h</text><text class="terminal-4196041424-r2" x="219.6" y="44.4" textLength="12.2" clip-path="url(#terminal-4196041424-line-1)">]</text><text class="terminal-4196041424-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-4196041424-line-1)"> +</text><text class="terminal-4196041424-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-4196041424-line-2)"> +</text><text class="terminal-4196041424-r1" x="0" y="93.2" textLength="353.8" clip-path="url(#terminal-4196041424-line-3)">show information about the cz</text><text class="terminal-4196041424-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-4196041424-line-3)"> +</text><text class="terminal-4196041424-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-4196041424-line-4)"> +</text><text class="terminal-4196041424-r1" x="0" y="142" textLength="97.6" clip-path="url(#terminal-4196041424-line-5)">options:</text><text class="terminal-4196041424-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-4196041424-line-5)"> +</text><text class="terminal-4196041424-r1" x="0" y="166.4" textLength="549" clip-path="url(#terminal-4196041424-line-6)">  -h, --help  show this help message and exit</text><text class="terminal-4196041424-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-4196041424-line-6)"> +</text><text class="terminal-4196041424-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-4196041424-line-7)"> +</text> + </g> + </g> +</svg> diff --git a/docs/images/cli_help/cz_init___help.svg b/docs/images/cli_help/cz_init___help.svg new file mode 100644 index 0000000..41a950e --- /dev/null +++ b/docs/images/cli_help/cz_init___help.svg @@ -0,0 +1,87 @@ +<svg class="rich-terminal" viewBox="0 0 994 245.2" xmlns="http://www.w3.org/2000/svg"> + <!-- Generated with Rich https://www.textualize.io --> + <style> + + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Regular"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); + font-style: normal; + font-weight: 400; + } + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Bold"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); + font-style: bold; + font-weight: 700; + } + + .terminal-1838121835-matrix { + font-family: Fira Code, monospace; + font-size: 20px; + line-height: 24.4px; + font-variant-east-asian: full-width; + } + + .terminal-1838121835-title { + font-size: 18px; + font-weight: bold; + font-family: arial; + } + + .terminal-1838121835-r1 { fill: #c5c8c6 } +.terminal-1838121835-r2 { fill: #c5c8c6;font-weight: bold } + </style> + + <defs> + <clipPath id="terminal-1838121835-clip-terminal"> + <rect x="0" y="0" width="975.0" height="194.2" /> + </clipPath> + <clipPath id="terminal-1838121835-line-0"> + <rect x="0" y="1.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1838121835-line-1"> + <rect x="0" y="25.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1838121835-line-2"> + <rect x="0" y="50.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1838121835-line-3"> + <rect x="0" y="74.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1838121835-line-4"> + <rect x="0" y="99.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1838121835-line-5"> + <rect x="0" y="123.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1838121835-line-6"> + <rect x="0" y="147.9" width="976" height="24.65"/> + </clipPath> + </defs> + + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="243.2" rx="8"/> + <g transform="translate(26,22)"> + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> + <circle cx="22" cy="0" r="7" fill="#febc2e"/> + <circle cx="44" cy="0" r="7" fill="#28c840"/> + </g> + + <g transform="translate(9, 41)" clip-path="url(#terminal-1838121835-clip-terminal)"> + + <g class="terminal-1838121835-matrix"> + <text class="terminal-1838121835-r1" x="0" y="20" textLength="195.2" clip-path="url(#terminal-1838121835-line-0)">$ cz init --help</text><text class="terminal-1838121835-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-1838121835-line-0)"> +</text><text class="terminal-1838121835-r1" x="0" y="44.4" textLength="183" clip-path="url(#terminal-1838121835-line-1)">usage: cz init </text><text class="terminal-1838121835-r2" x="183" y="44.4" textLength="12.2" clip-path="url(#terminal-1838121835-line-1)">[</text><text class="terminal-1838121835-r1" x="195.2" y="44.4" textLength="24.4" clip-path="url(#terminal-1838121835-line-1)">-h</text><text class="terminal-1838121835-r2" x="219.6" y="44.4" textLength="12.2" clip-path="url(#terminal-1838121835-line-1)">]</text><text class="terminal-1838121835-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-1838121835-line-1)"> +</text><text class="terminal-1838121835-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-1838121835-line-2)"> +</text><text class="terminal-1838121835-r1" x="0" y="93.2" textLength="353.8" clip-path="url(#terminal-1838121835-line-3)">init commitizen configuration</text><text class="terminal-1838121835-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-1838121835-line-3)"> +</text><text class="terminal-1838121835-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-1838121835-line-4)"> +</text><text class="terminal-1838121835-r1" x="0" y="142" textLength="97.6" clip-path="url(#terminal-1838121835-line-5)">options:</text><text class="terminal-1838121835-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-1838121835-line-5)"> +</text><text class="terminal-1838121835-r1" x="0" y="166.4" textLength="549" clip-path="url(#terminal-1838121835-line-6)">  -h, --help  show this help message and exit</text><text class="terminal-1838121835-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-1838121835-line-6)"> +</text><text class="terminal-1838121835-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-1838121835-line-7)"> +</text> + </g> + </g> +</svg> diff --git a/docs/images/cli_help/cz_ls___help.svg b/docs/images/cli_help/cz_ls___help.svg new file mode 100644 index 0000000..3ec3532 --- /dev/null +++ b/docs/images/cli_help/cz_ls___help.svg @@ -0,0 +1,87 @@ +<svg class="rich-terminal" viewBox="0 0 994 245.2" xmlns="http://www.w3.org/2000/svg"> + <!-- Generated with Rich https://www.textualize.io --> + <style> + + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Regular"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); + font-style: normal; + font-weight: 400; + } + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Bold"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); + font-style: bold; + font-weight: 700; + } + + .terminal-589791338-matrix { + font-family: Fira Code, monospace; + font-size: 20px; + line-height: 24.4px; + font-variant-east-asian: full-width; + } + + .terminal-589791338-title { + font-size: 18px; + font-weight: bold; + font-family: arial; + } + + .terminal-589791338-r1 { fill: #c5c8c6 } +.terminal-589791338-r2 { fill: #c5c8c6;font-weight: bold } + </style> + + <defs> + <clipPath id="terminal-589791338-clip-terminal"> + <rect x="0" y="0" width="975.0" height="194.2" /> + </clipPath> + <clipPath id="terminal-589791338-line-0"> + <rect x="0" y="1.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-589791338-line-1"> + <rect x="0" y="25.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-589791338-line-2"> + <rect x="0" y="50.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-589791338-line-3"> + <rect x="0" y="74.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-589791338-line-4"> + <rect x="0" y="99.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-589791338-line-5"> + <rect x="0" y="123.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-589791338-line-6"> + <rect x="0" y="147.9" width="976" height="24.65"/> + </clipPath> + </defs> + + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="243.2" rx="8"/> + <g transform="translate(26,22)"> + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> + <circle cx="22" cy="0" r="7" fill="#febc2e"/> + <circle cx="44" cy="0" r="7" fill="#28c840"/> + </g> + + <g transform="translate(9, 41)" clip-path="url(#terminal-589791338-clip-terminal)"> + + <g class="terminal-589791338-matrix"> + <text class="terminal-589791338-r1" x="0" y="20" textLength="170.8" clip-path="url(#terminal-589791338-line-0)">$ cz ls --help</text><text class="terminal-589791338-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-589791338-line-0)"> +</text><text class="terminal-589791338-r1" x="0" y="44.4" textLength="158.6" clip-path="url(#terminal-589791338-line-1)">usage: cz ls </text><text class="terminal-589791338-r2" x="158.6" y="44.4" textLength="12.2" clip-path="url(#terminal-589791338-line-1)">[</text><text class="terminal-589791338-r1" x="170.8" y="44.4" textLength="24.4" clip-path="url(#terminal-589791338-line-1)">-h</text><text class="terminal-589791338-r2" x="195.2" y="44.4" textLength="12.2" clip-path="url(#terminal-589791338-line-1)">]</text><text class="terminal-589791338-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-589791338-line-1)"> +</text><text class="terminal-589791338-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-589791338-line-2)"> +</text><text class="terminal-589791338-r1" x="0" y="93.2" textLength="317.2" clip-path="url(#terminal-589791338-line-3)">show available commitizens</text><text class="terminal-589791338-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-589791338-line-3)"> +</text><text class="terminal-589791338-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-589791338-line-4)"> +</text><text class="terminal-589791338-r1" x="0" y="142" textLength="97.6" clip-path="url(#terminal-589791338-line-5)">options:</text><text class="terminal-589791338-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-589791338-line-5)"> +</text><text class="terminal-589791338-r1" x="0" y="166.4" textLength="549" clip-path="url(#terminal-589791338-line-6)">  -h, --help  show this help message and exit</text><text class="terminal-589791338-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-589791338-line-6)"> +</text><text class="terminal-589791338-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-589791338-line-7)"> +</text> + </g> + </g> +</svg> diff --git a/docs/images/cli_help/cz_schema___help.svg b/docs/images/cli_help/cz_schema___help.svg new file mode 100644 index 0000000..afe8982 --- /dev/null +++ b/docs/images/cli_help/cz_schema___help.svg @@ -0,0 +1,87 @@ +<svg class="rich-terminal" viewBox="0 0 994 245.2" xmlns="http://www.w3.org/2000/svg"> + <!-- Generated with Rich https://www.textualize.io --> + <style> + + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Regular"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); + font-style: normal; + font-weight: 400; + } + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Bold"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); + font-style: bold; + font-weight: 700; + } + + .terminal-1497071669-matrix { + font-family: Fira Code, monospace; + font-size: 20px; + line-height: 24.4px; + font-variant-east-asian: full-width; + } + + .terminal-1497071669-title { + font-size: 18px; + font-weight: bold; + font-family: arial; + } + + .terminal-1497071669-r1 { fill: #c5c8c6 } +.terminal-1497071669-r2 { fill: #c5c8c6;font-weight: bold } + </style> + + <defs> + <clipPath id="terminal-1497071669-clip-terminal"> + <rect x="0" y="0" width="975.0" height="194.2" /> + </clipPath> + <clipPath id="terminal-1497071669-line-0"> + <rect x="0" y="1.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1497071669-line-1"> + <rect x="0" y="25.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1497071669-line-2"> + <rect x="0" y="50.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1497071669-line-3"> + <rect x="0" y="74.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1497071669-line-4"> + <rect x="0" y="99.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1497071669-line-5"> + <rect x="0" y="123.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1497071669-line-6"> + <rect x="0" y="147.9" width="976" height="24.65"/> + </clipPath> + </defs> + + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="243.2" rx="8"/> + <g transform="translate(26,22)"> + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> + <circle cx="22" cy="0" r="7" fill="#febc2e"/> + <circle cx="44" cy="0" r="7" fill="#28c840"/> + </g> + + <g transform="translate(9, 41)" clip-path="url(#terminal-1497071669-clip-terminal)"> + + <g class="terminal-1497071669-matrix"> + <text class="terminal-1497071669-r1" x="0" y="20" textLength="219.6" clip-path="url(#terminal-1497071669-line-0)">$ cz schema --help</text><text class="terminal-1497071669-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-1497071669-line-0)"> +</text><text class="terminal-1497071669-r1" x="0" y="44.4" textLength="207.4" clip-path="url(#terminal-1497071669-line-1)">usage: cz schema </text><text class="terminal-1497071669-r2" x="207.4" y="44.4" textLength="12.2" clip-path="url(#terminal-1497071669-line-1)">[</text><text class="terminal-1497071669-r1" x="219.6" y="44.4" textLength="24.4" clip-path="url(#terminal-1497071669-line-1)">-h</text><text class="terminal-1497071669-r2" x="244" y="44.4" textLength="12.2" clip-path="url(#terminal-1497071669-line-1)">]</text><text class="terminal-1497071669-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-1497071669-line-1)"> +</text><text class="terminal-1497071669-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-1497071669-line-2)"> +</text><text class="terminal-1497071669-r1" x="0" y="93.2" textLength="219.6" clip-path="url(#terminal-1497071669-line-3)">show commit schema</text><text class="terminal-1497071669-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-1497071669-line-3)"> +</text><text class="terminal-1497071669-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-1497071669-line-4)"> +</text><text class="terminal-1497071669-r1" x="0" y="142" textLength="97.6" clip-path="url(#terminal-1497071669-line-5)">options:</text><text class="terminal-1497071669-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-1497071669-line-5)"> +</text><text class="terminal-1497071669-r1" x="0" y="166.4" textLength="549" clip-path="url(#terminal-1497071669-line-6)">  -h, --help  show this help message and exit</text><text class="terminal-1497071669-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-1497071669-line-6)"> +</text><text class="terminal-1497071669-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-1497071669-line-7)"> +</text> + </g> + </g> +</svg> diff --git a/docs/images/cli_help/cz_version___help.svg b/docs/images/cli_help/cz_version___help.svg new file mode 100644 index 0000000..c7777db --- /dev/null +++ b/docs/images/cli_help/cz_version___help.svg @@ -0,0 +1,111 @@ +<svg class="rich-terminal" viewBox="0 0 994 391.59999999999997" xmlns="http://www.w3.org/2000/svg"> + <!-- Generated with Rich https://www.textualize.io --> + <style> + + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Regular"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); + font-style: normal; + font-weight: 400; + } + @font-face { + font-family: "Fira Code"; + src: local("FiraCode-Bold"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); + font-style: bold; + font-weight: 700; + } + + .terminal-4023877003-matrix { + font-family: Fira Code, monospace; + font-size: 20px; + line-height: 24.4px; + font-variant-east-asian: full-width; + } + + .terminal-4023877003-title { + font-size: 18px; + font-weight: bold; + font-family: arial; + } + + .terminal-4023877003-r1 { fill: #c5c8c6 } +.terminal-4023877003-r2 { fill: #c5c8c6;font-weight: bold } + </style> + + <defs> + <clipPath id="terminal-4023877003-clip-terminal"> + <rect x="0" y="0" width="975.0" height="340.59999999999997" /> + </clipPath> + <clipPath id="terminal-4023877003-line-0"> + <rect x="0" y="1.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4023877003-line-1"> + <rect x="0" y="25.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4023877003-line-2"> + <rect x="0" y="50.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4023877003-line-3"> + <rect x="0" y="74.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4023877003-line-4"> + <rect x="0" y="99.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4023877003-line-5"> + <rect x="0" y="123.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4023877003-line-6"> + <rect x="0" y="147.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4023877003-line-7"> + <rect x="0" y="172.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4023877003-line-8"> + <rect x="0" y="196.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4023877003-line-9"> + <rect x="0" y="221.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4023877003-line-10"> + <rect x="0" y="245.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4023877003-line-11"> + <rect x="0" y="269.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4023877003-line-12"> + <rect x="0" y="294.3" width="976" height="24.65"/> + </clipPath> + </defs> + + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="389.6" rx="8"/> + <g transform="translate(26,22)"> + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> + <circle cx="22" cy="0" r="7" fill="#febc2e"/> + <circle cx="44" cy="0" r="7" fill="#28c840"/> + </g> + + <g transform="translate(9, 41)" clip-path="url(#terminal-4023877003-clip-terminal)"> + + <g class="terminal-4023877003-matrix"> + <text class="terminal-4023877003-r1" x="0" y="20" textLength="231.8" clip-path="url(#terminal-4023877003-line-0)">$ cz version --help</text><text class="terminal-4023877003-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-4023877003-line-0)"> +</text><text class="terminal-4023877003-r1" x="0" y="44.4" textLength="219.6" clip-path="url(#terminal-4023877003-line-1)">usage: cz version </text><text class="terminal-4023877003-r2" x="219.6" y="44.4" textLength="12.2" clip-path="url(#terminal-4023877003-line-1)">[</text><text class="terminal-4023877003-r1" x="231.8" y="44.4" textLength="24.4" clip-path="url(#terminal-4023877003-line-1)">-h</text><text class="terminal-4023877003-r2" x="256.2" y="44.4" textLength="12.2" clip-path="url(#terminal-4023877003-line-1)">]</text><text class="terminal-4023877003-r2" x="280.6" y="44.4" textLength="12.2" clip-path="url(#terminal-4023877003-line-1)">[</text><text class="terminal-4023877003-r1" x="292.8" y="44.4" textLength="207.4" clip-path="url(#terminal-4023877003-line-1)">-r | -p | -c | -v</text><text class="terminal-4023877003-r2" x="500.2" y="44.4" textLength="12.2" clip-path="url(#terminal-4023877003-line-1)">]</text><text class="terminal-4023877003-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-4023877003-line-1)"> +</text><text class="terminal-4023877003-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-4023877003-line-2)"> +</text><text class="terminal-4023877003-r1" x="0" y="93.2" textLength="817.4" clip-path="url(#terminal-4023877003-line-3)">get the version of the installed commitizen or the current project </text><text class="terminal-4023877003-r2" x="817.4" y="93.2" textLength="12.2" clip-path="url(#terminal-4023877003-line-3)">(</text><text class="terminal-4023877003-r1" x="829.6" y="93.2" textLength="97.6" clip-path="url(#terminal-4023877003-line-3)">default:</text><text class="terminal-4023877003-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-4023877003-line-3)"> +</text><text class="terminal-4023877003-r1" x="0" y="117.6" textLength="244" clip-path="url(#terminal-4023877003-line-4)">installed commitizen</text><text class="terminal-4023877003-r2" x="244" y="117.6" textLength="12.2" clip-path="url(#terminal-4023877003-line-4)">)</text><text class="terminal-4023877003-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-4023877003-line-4)"> +</text><text class="terminal-4023877003-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-4023877003-line-5)"> +</text><text class="terminal-4023877003-r1" x="0" y="166.4" textLength="97.6" clip-path="url(#terminal-4023877003-line-6)">options:</text><text class="terminal-4023877003-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-4023877003-line-6)"> +</text><text class="terminal-4023877003-r1" x="0" y="190.8" textLength="622.2" clip-path="url(#terminal-4023877003-line-7)">  -h, --help        show this help message and exit</text><text class="terminal-4023877003-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-4023877003-line-7)"> +</text><text class="terminal-4023877003-r1" x="0" y="215.2" textLength="744.2" clip-path="url(#terminal-4023877003-line-8)">  -r, --report      get system information for reporting bugs</text><text class="terminal-4023877003-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-4023877003-line-8)"> +</text><text class="terminal-4023877003-r1" x="0" y="239.6" textLength="707.6" clip-path="url(#terminal-4023877003-line-9)">  -p, --project     get the version of the current project</text><text class="terminal-4023877003-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-4023877003-line-9)"> +</text><text class="terminal-4023877003-r1" x="0" y="264" textLength="768.6" clip-path="url(#terminal-4023877003-line-10)">  -c, --commitizen  get the version of the installed commitizen</text><text class="terminal-4023877003-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-4023877003-line-10)"> +</text><text class="terminal-4023877003-r1" x="0" y="288.4" textLength="927.2" clip-path="url(#terminal-4023877003-line-11)">  -v, --verbose     get the version of both the installed commitizen and the</text><text class="terminal-4023877003-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-4023877003-line-11)"> +</text><text class="terminal-4023877003-r1" x="0" y="312.8" textLength="427" clip-path="url(#terminal-4023877003-line-12)">                    current project</text><text class="terminal-4023877003-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-4023877003-line-12)"> +</text><text class="terminal-4023877003-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-4023877003-line-13)"> +</text> + </g> + </g> +</svg> diff --git a/docs/images/commit.yml b/docs/images/commit.yml new file mode 100644 index 0000000..fd10ed7 --- /dev/null +++ b/docs/images/commit.yml @@ -0,0 +1,187 @@ +# The configurations that used for the recording, feel free to edit them +config: + + # Specify a command to be executed + # like `/bin/bash -l`, `ls`, or any other commands + # the default is bash for Linux + # or powershell.exe for Windows + command: bash -l + + # Specify the current working directory path + # the default is the current working directory path + cwd: ~/my-project + + # Export additional ENV variables + env: + recording: true + + # Explicitly set the number of columns + # or use `auto` to take the current + # number of columns of your shell + cols: 101 + + # Explicitly set the number of rows + # or use `auto` to take the current + # number of rows of your shell + rows: 22 + + # Amount of times to repeat GIF + # If value is -1, play once + # If value is 0, loop indefinitely + # If value is a positive number, loop n times + repeat: 0 + + # Quality + # 1 - 100 + quality: 85 + + # Delay between frames in ms + # If the value is `auto` use the actual recording delays + frameDelay: auto + + # Maximum delay between frames in ms + # Ignored if the `frameDelay` isn't set to `auto` + # Set to `auto` to prevent limiting the max idle time + maxIdleTime: 2000 + + # The surrounding frame box + # The `type` can be null, window, floating, or solid` + # To hide the title use the value null + # Don't forget to add a backgroundColor style with a null as type + frameBox: + type: floating + title: Commitizen + style: + border: 0px black solid + # boxShadow: none + # margin: 0px + + # Add a watermark image to the rendered gif + # You need to specify an absolute path for + # the image on your machine or a URL, and you can also + # add your own CSS styles + watermark: + imagePath: null + style: + position: absolute + right: 15px + bottom: 15px + width: 100px + opacity: 0.9 + + # Cursor style can be one of + # `block`, `underline`, or `bar` + cursorStyle: block + + # Font family + # You can use any font that is installed on your machine + # in CSS-like syntax + fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace" + + # The size of the font + fontSize: 12 + + # The height of lines + lineHeight: 1 + + # The spacing between letters + letterSpacing: 0 + + # Theme + theme: + background: "transparent" + foreground: "#afafaf" + cursor: "#c7c7c7" + black: "#232628" + red: "#fc4384" + green: "#b3e33b" + yellow: "#ffa727" + blue: "#75dff2" + magenta: "#ae89fe" + cyan: "#708387" + white: "#d5d5d0" + brightBlack: "#626566" + brightRed: "#ff7fac" + brightGreen: "#c8ed71" + brightYellow: "#ebdf86" + brightBlue: "#75dff2" + brightMagenta: "#ae89fe" + brightCyan: "#b1c6ca" + brightWhite: "#f9f9f4" + +# Records, feel free to edit them +records: + - delay: 987 + content: "\e[1;33m\e[0;32m\e[1;34m\e[1;32msantiago\e[1;34m@\e[1;31mhome\e[1;37m in \e[1;34m~/my-project\e[0;36m |master #|\e[1;32m\r\r\n\e[1;32m$\e[00m " + - delay: 731 + content: c + - delay: 345 + content: z + - delay: 135 + content: ' ' + - delay: 118 + content: c + - delay: 116 + content: o + - delay: 200 + content: m + - delay: 135 + content: m + - delay: 199 + content: i + - delay: 406 + content: t + - delay: 144 + content: "\r\n" + - delay: 209 + content: "\e[?1l\e[6n" + - delay: 7 + content: "\e[?2004h\e[?25l\e[0m\e[?7l\e[0m\e[J\e[0;38;5;67m?\e[0;1m Select the type of change you are committing \e[0m (Use arrow keys) \r\e[100C \e[0m\r\r\n\e[0m ยป fix: A bug fix. Correlates with PATCH in SemVer\e[0m\r\r\n\e[0m feat: A new feature. Correlates with MINOR in SemVer\e[0m\r\r\n\e[0m docs: Documentation only changes\e[0m\r\r\n\e[0m style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-\r\e[100Cc\e[0m\r\r\n\e[0m refactor: A code change that neither fixes a bug nor adds a feature\e[0m\r\r\n\e[0m perf: A code change that improves performance\e[0m\r\r\n\e[0m test: Adding missing or correcting existing tests\e[0m\r\r\n\e[0m build: Changes that affect the build system or external dependencies (example scopes: pip, docker\r\e[100C,\e[0m\r\r\n\e[0m ci: Changes to CI configuration files and scripts (example scopes: GitLabCI) \r\e[100C \r\e[9A\e[64C\e[?7h\e[0m\e[?12l\e[?25h" + - delay: 17 + content: "\e[?25l\e[?7l\e[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\e[0m ci: Changes to CI configuration files and scripts (example scopes: GitLabCI)\e[0m\e[K\e[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\e[0m \r\e[100C \r\e[19A\e[64C\e[?7h\e[0m\e[?12l\e[?25h" + - delay: 647 + content: "\e[?25l\e[?7l\e[0m\r\r\n\e[0m \e[0m\r\r\n\e[0m ยป \e[2A\e[61C\e[?7h\e[0m\e[?12l\e[?25h" + - delay: 574 + content: "\e[?25l\e[?7l\e[64D\e[0m\e[J\e[0;38;5;67m?\e[0;1m Select the type of change you are committing \e[0;38;5;214;1m feat: A new feature. Correlates with MINOR in SemVer\r\e[100C\e[0m \r\e[0m\r\r\n\e[J\e[?7h\e[0m\e[?12l\e[?25h\e[?2004l" + - delay: 20 + content: "\e[?1l\e[6n\e[?2004h\e[?25l\e[0m\e[?7l\e[0m\e[J\e[0;38;5;67m?\e[0;1m Scope. Could be anything specifying place of the commit change (users, db, poll):\e[0m \r\e[100C \e[0m\r\r\n\e[0;1m \e[0m \r\e[100C \r\e[C\e[?7h\e[0m\e[?12l\e[?25h" + - delay: 11 + content: "\e[?25l\e[?7l\b\e[0;1m \e[0m \e[0m\e[K\e[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\e[0m \r\e[100C \r\e[17A\e[C\e[?7h\e[0m\e[?12l\e[?25h" + - delay: 1388 + content: "\e[?25l\e[?7l\e[A\b\e[0m\e[J\e[0;38;5;67m?\e[0;1m Scope. Could be anything specifying place of the commit change (users, db, poll):\e[0m \r\e[100C \e[0m\r\r\n\e[0;1m \e[0m \r\e[100C \r\e[0m\r\r\n\e[J\e[?7h\e[0m\e[?12l\e[?25h\e[?2004l" + - delay: 12 + content: "\e[?1l\e[6n" + - delay: 5 + content: "\e[?2004h\e[?25l\e[0m\e[?7l\e[0m\e[J\e[0;38;5;67m?\e[0;1m Subject. Concise description of the changes. Imperative, lower case and no final dot:\e[0m \r\e[100C \e[0m\r\r\n\e[0;1m \e[0m \r\e[100C \r\e[C\e[?7h\e[0m\e[?12l\e[?25h" + - delay: 24 + content: "\e[?25l\e[?7l\b\e[0;1m \e[0m \e[0m\e[K\e[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\e[0m \r\e[100C \r\e[15A\e[C\e[?7h\e[0m\e[?12l\e[?25h" + - delay: 925 + content: "\e[?25l\e[?7l\e[0mallow provided config object to extend other configs \b\e[?7h\e[0m\e[?12l\e[?25h\e[?25l\e[?7l\e[?7h\e[0m\e[?12l\e[?25h" + - delay: 2880 + content: "\e[?25l\e[?7l\e[A\e[53D\e[0m\e[J\e[0;38;5;67m?\e[0;1m Subject. Concise description of the changes. Imperative, lower case and no final dot:\e[0m \r\e[100C \e[0m\r\r\n\e[0;1m \e[0mallow provided config object to extend other configs \r\e[100C \r\e[0m\r\r\n\e[J\e[?7h\e[0m\e[?12l\e[?25h\e[?2004l" + - delay: 13 + content: "\e[?1l\e[6n\e[?2004h\e[?25l\e[0m\e[?7l\e[0m\e[J\e[0;38;5;67m?\e[0;1m Is this a BREAKING CHANGE? Correlates with MAJOR in SemVer \e[0m (y/N) \r\e[100C \r\e[67C\e[?7h\e[0m\e[?12l\e[?25h" + - delay: 19 + content: "\e[?25l\e[?7l\e[67D\e[0;38;5;67m?\e[0;1m Is this a BREAKING CHANGE? Correlates with MAJOR in SemVer \e[0m (y/N) \e[0m\e[K\e[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\e[0m \r\e[100C \r\e[14A\e[67C\e[?7h\e[0m\e[?12l\e[?25h" + - delay: 1521 + content: "\e[?25l\e[?7l\e[67D\e[0m\e[J\e[0;38;5;67m?\e[0;1m Is this a BREAKING CHANGE? Correlates with MAJOR in SemVer \e[0;38;5;214;1m Yes\e[0m \r\e[100C \r\e[0m\r\r\n\e[J\e[?7h\e[0m\e[?12l\e[?25h\e[?2004l" + - delay: 15 + content: "\e[?1l\e[6n\e[?2004h\e[?25l\e[0m\e[?7l\e[0m\e[J\e[0;38;5;67m?\e[0;1m Body. Motivation for the change and contrast this with previous behavior:\e[0m \r\e[100C \e[0m\r\r\n\e[0;1m \e[0m \r\e[100C \r\e[C\e[?7h\e[0m\e[?12l\e[?25h" + - delay: 16 + content: "\e[?25l\e[?7l\b\e[0;1m \e[0m \e[0m\e[K\e[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\e[0m \r\e[100C \r\e[12A\e[C\e[?7h\e[0m\e[?12l\e[?25h" + - delay: 5659 + content: "\e[?25l\e[?7l\e[0mextends key in config file is now used for extending other config files \b\e[?7h\e[0m\e[?12l\e[?25h\e[?25l\e[?7l\e[?7h\e[0m\e[?12l\e[?25h" + - delay: 647 + content: "\e[?25l\e[?7l\e[A\e[72D\e[0m\e[J\e[0;38;5;67m?\e[0;1m Body. Motivation for the change and contrast this with previous behavior:\e[0m \r\e[100C \e[0m\r\r\n\e[0;1m \e[0mextends key in config file is now used for extending other config files \r\e[100C \r\e[0m\r\r\n\e[J\e[?7h\e[0m\e[?12l\e[?25h\e[?2004l" + - delay: 10 + content: "\e[?1l\e[6n" + - delay: 5 + content: "\e[?2004h\e[?25l\e[0m\e[?7l\e[0m\e[J\e[0;38;5;67m?\e[0;1m Footer. Information about Breaking Changes and reference issues that this commit closes:\e[0m \r\e[100C \e[0m\r\r\n\e[0;1m \e[0m \r\e[100C \r\e[C\e[?7h\e[0m\e[?12l\e[?25h" + - delay: 22 + content: "\e[?25l\e[?7l\b\e[0;1m \e[0m \e[0m\e[K\e[0m\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\r\r\n\e[0m \r\e[100C \r\e[10A\e[C\e[?7h\e[0m\e[?12l\e[?25h" + - delay: 1583 + content: "\e[?25l\e[?7l\e[A\b\e[0m\e[J\e[0;38;5;67m?\e[0;1m Footer. Information about Breaking Changes and reference issues that this commit closes:\e[0m \r\e[100C \e[0m\r\r\n\e[0;1m \e[0m \r\e[100C \r\e[0m\r\r\n\e[J\e[?7h\e[0m\e[?12l\e[?25h\e[?2004l" + - delay: 6 + content: "[master (root-commit) 76d9660] feat: allow provided config object to extend other configs\r\n 1 file changed, 0 insertions(+), 0 deletions(-)\r\n create mode 100644 fil.py\r\n\r\n\e[32mCommit successful!\e[0m\r\n\e[0m" + - delay: 102 + content: "\e[1;33m\e[0;32m\e[1;34m\e[1;32msantiago\e[1;34m@\e[1;31mhome\e[1;37m in \e[1;34m~/my-project\e[0;36m |master|\e[1;32m\r\r\n\e[1;32m$\e[00m " diff --git a/docs/images/example.png b/docs/images/example.png new file mode 100644 index 0000000..6ac0589 Binary files /dev/null and b/docs/images/example.png differ diff --git a/docs/images/gitlab_ci/gitlab_deploy_key.png b/docs/images/gitlab_ci/gitlab_deploy_key.png new file mode 100644 index 0000000..dd70443 Binary files /dev/null and b/docs/images/gitlab_ci/gitlab_deploy_key.png differ diff --git a/docs/images/gitlab_ci/gitlab_final_ci_result.png b/docs/images/gitlab_ci/gitlab_final_ci_result.png new file mode 100644 index 0000000..27749db Binary files /dev/null and b/docs/images/gitlab_ci/gitlab_final_ci_result.png differ diff --git a/docs/images/gitlab_ci/gitlab_variables.png b/docs/images/gitlab_ci/gitlab_variables.png new file mode 100644 index 0000000..d00ebfc Binary files /dev/null and b/docs/images/gitlab_ci/gitlab_variables.png differ diff --git a/docs/third-party-commitizen.md b/docs/third-party-commitizen.md new file mode 100644 index 0000000..1c8ef8a --- /dev/null +++ b/docs/third-party-commitizen.md @@ -0,0 +1,131 @@ +## Third-Party Commitizen Templates + +In addition to the native templates, some alternative commit format templates +are available as PyPI packages (installable with `pip`). + +### [Conventional JIRA](https://pypi.org/project/conventional-JIRA/) + +Just like _conventional commit_ format, but the scope has been restricted to a +JIRA issue format, i.e. `project-issueNumber`. This standardises scopes in a +meaningful way. + +#### Installation + +```sh +pip install conventional-JIRA +``` + +### [GitHub JIRA Conventional](https://pypi.org/project/cz-github-jira-conventional/) + +This plugin extends the commitizen tools by: + +- requiring a JIRA issue id in the commit message +- creating links to GitHub commits in the CHANGELOG.md +- creating links to JIRA issues in the CHANGELOG.md + +#### Installation + +```sh +pip install cz-github-jira-conventional +``` + +For installation instructions (configuration and pre-commit) please visit [https://github.com/apheris/cz-github-jira-conventional](https://github.com/apheris/cz-github-jira-conventional) + +### [cz-emoji](https://github.com/adam-grant-hendry/cz-emoji) + +_conventional commit_ format, but with emojis + +#### Installation + +```sh +pip install cz-emoji +``` + +#### Usage + +```sh +cz --name cz_emoji commit +``` + +### [cz-conventional-gitmoji](https://github.com/ljnsn/cz-conventional-gitmoji) + +*conventional commit*s, but with [gitmojis](https://gitmoji.dev). + +Includes a pre-commit hook that automatically adds the correct gitmoji to the commit message based on the conventional type. + +#### Installation + +```sh +pip install cz-conventional-gitmoji +``` + +#### Usage + +```sh +cz --name cz_gitmoji commit +``` + +### [Commitizen emoji](https://pypi.org/project/commitizen-emoji/) (Unmaintained) + +Just like _conventional commit_ format, but with emojis and optionally time spent and related tasks. + +#### Installation + +```sh +pip install commitizen-emoji +``` + +#### Usage + +```sh +cz --name cz_commitizen_emoji commit +``` + +### [Conventional Legacy (cz_legacy)][1] + +An extension of the _conventional commit_ format to include user-specified +legacy change types in the `CHANGELOG` while preventing the legacy change types +from being used in new commit messages + +#### Installation + +```sh +pip install cz_legacy +``` + +#### Usage + +See the [README][1] for instructions on configuration + +[1]: https://pypi.org/project/cz_legacy + +## Third-Party Commitizen Providers + +Commitizen can read and write version from different sources. In addition to the native providers, some alternative version sources are available as PyPI packages (installable with `pip`). + +### [commitizen-deno-provider](https://pypi.org/project/commitizen-deno-provider/) + +A provider for **Deno** projects. The provider updates the version in deno.json and jsr.json files. + +#### Installation + +```sh +pip install commitizen-deno-provider +``` + +#### Usage + +Add `deno-provider` to your configuration file. + +Example for `.cz.yaml`: + +```yaml +--- +commitizen: + major_version_zero: true + name: cz_conventional_commits + tag_format: $version + update_changelog_on_bump: true + version_provider: deno-provider + version_scheme: semver +``` diff --git a/docs/tutorials/auto_check.md b/docs/tutorials/auto_check.md new file mode 100644 index 0000000..2fce57f --- /dev/null +++ b/docs/tutorials/auto_check.md @@ -0,0 +1,68 @@ +# Automatically check message before commit + +## About + +To automatically check a commit message prior to committing, you can use a [git hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks). + +## How to + +There are two common methods for installing the hook: + +### Method 1: Add git hook through [pre-commit](https://pre-commit.com/) + +- Step 1: Install [pre-commit](https://pre-commit.com/) + +```sh +python -m pip install pre-commit +``` + +- Step 2: Create `.pre-commit-config.yaml` at your root directory with the following content + +```yaml +--- +repos: + - repo: https://github.com/commitizen-tools/commitizen + rev: v1.17.0 + hooks: + - id: commitizen + stages: [commit-msg] +``` + +- Step 3: Install the configuration into git hook through `pre-commit` + +```bash +pre-commit install --hook-type commit-msg +``` + +### Method 2: Manually add git hook + +The command might be included inside of a Git hook (inside of `.git/hooks/` at the root of the project). + +The selected hook might be the file called commit-msg. + +This example shows how to use the check command inside of commit-msg. + +At the root of the project: + +```bash +cd .git/hooks +touch commit-msg +chmod +x commit-msg +``` + +Open the file and edit it: + +```sh +#!/bin/bash +MSG_FILE=$1 +cz check --allow-abort --commit-msg-file $MSG_FILE +``` + +Where `$1` is the name of the temporary file that contains the current commit message. To be more explicit, the previous variable is stored in another variable called `$MSG_FILE`, for didactic purposes. + +The `--commit-msg-file` flag is required, not optional. + +Each time you create a commit, automatically, this hook will analyze it. +If the commit message is invalid, it'll be rejected. + +The commit should follow the given committing rules; otherwise, it won't be accepted. diff --git a/docs/tutorials/auto_prepare_commit_message.md b/docs/tutorials/auto_prepare_commit_message.md new file mode 100644 index 0000000..7e8295b --- /dev/null +++ b/docs/tutorials/auto_prepare_commit_message.md @@ -0,0 +1,46 @@ +# Automatically prepare message before commit + +## About + +It can be desirable to use commitizen for all types of commits (i.e. regular, merge, +squash) so that the complete git history adheres to the commit message convention +without ever having to call `cz commit`. + +To automatically prepare a commit message prior to committing, you can +use a [prepare-commit-msg Git hook][prepare-commit-msg-docs]: + +> This hook is invoked by git-commit right after preparing the +> default log message, and before the editor is started. + +To automatically perform arbitrary cleanup steps after a successful commit you can use a +[post-commit Git hook][post-commit-docs]: + +> This hook is invoked by git-commit. It takes no parameters, and is invoked after a +> commit is made. + +A combination of these two hooks allows for enforcing the usage of commitizen so that +whenever a commit is about to be created, commitizen is used for creating the commit +message. Running `git commit` or `git commit -m "..."` for example, would trigger +commitizen and use the generated commit message for the commit. + +## Installation + +Copy the hooks from [here](https://github.com/commitizen-tools/commitizen/tree/master/hooks) into the `.git/hooks` folder and make them + executable by running the following commands from the root of your Git repository: + +```bash +wget -O .git/hooks/prepare-commit-msg https://raw.githubusercontent.com/commitizen-tools/commitizen/master/hooks/prepare-commit-msg.py +chmod +x .git/hooks/prepare-commit-msg +wget -O .git/hooks/post-commit https://raw.githubusercontent.com/commitizen-tools/commitizen/master/hooks/post-commit.py +chmod +x .git/hooks/post-commit +``` + +## Features + +- Commits can be created using both `cz commit` and the regular `git commit` +- The hooks automatically create a backup of the commit message that can be reused if + the commit failed +- The commit message backup can also be used via `cz commit --retry` + +[post-commit-docs]: https://git-scm.com/docs/githooks#_post_commit +[prepare-commit-msg-docs]: https://git-scm.com/docs/githooks#_prepare_commit_msg diff --git a/docs/tutorials/dev_releases.md b/docs/tutorials/dev_releases.md new file mode 100644 index 0000000..8142334 --- /dev/null +++ b/docs/tutorials/dev_releases.md @@ -0,0 +1,78 @@ +# Dev Releases + +## About + +To make use of a `.dev` suffix, as per +[PEP440](https://peps.python.org/pep-0440/#developmental-releases). + +If more than one active branch attempts to create a tag, relative to the main +branch, there is the possibility that each will attempt to create the _same_ +tag, resulting in a collision. + +Developmental releases aim to avoid this by including a `.dev` segment which +includes a non-negative integer unique to that workflow: + +```txt +X.Y.devN +``` + +!!! note + As noted in + [PEP440](https://peps.python.org/pep-0440/#developmental-releases), + although developmental releases are useful in avoiding the situation + described above, depending on the value passed as the developmental + release, they can be _"difficult to parse for human readers"_. + +## How to + +### Example 1: CircleCI + +For example, CircleCI [provides](https://circleci.com/docs/variables/) +`CIRCLE_BUILD_NUM`, a unique number for each job which will increment with each +run: + +```sh +--devrelease ${CIRCLE_BUILD_NUM} +``` + +This will result in a unique developmental release of, for example: + +```sh +1.3.2.dev2478 +``` + +### Example 2: GitHub + +GitHub also +[provides](https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables) +`GITHUB_RUN_ID`, a _"unique number for each workflow run"_ which will also +provide a unique number for each workflow: + +```sh +--devrelease ${GITHUB_RUN_ID} +``` + +This will result in a unique developmental release of, for example: + +```sh +1.3.2.dev6048584598 +``` + +### Example 3: Unix time + +Equally, as the developmental release needs only a non-negative integer, it is +possible to use the Unix time (i.e. the number of seconds since 1st January +1970 UTC). + +This would create the possibility of a collision if two builds occur at +precisely the same second but this may be sufficient for many cases: + +```sh +--devrelease $(date +%s) +``` + +This will result in a unique developmental release of, for example: + +```sh +1.3.2.dev1696238452 +``` diff --git a/docs/tutorials/github_actions.md b/docs/tutorials/github_actions.md new file mode 100644 index 0000000..7a98abe --- /dev/null +++ b/docs/tutorials/github_actions.md @@ -0,0 +1,125 @@ +## Create a new release with Github Actions + +### Automatic bumping of version + +To execute `cz bump` in your CI, and push the new commit and +the new tag, back to your master branch, we have to: + +1. Create a personal access token. [Follow the instructions here](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line#creating-a-token). And copy the generated key +2. Create a secret called `PERSONAL_ACCESS_TOKEN`, with the copied key, by going to your + project repository and then `Settings > Secrets > Add new secret`. +3. In your repository create a new file `.github/workflows/bumpversion.yml` + with the following content. + +!!! warning + If you use `GITHUB_TOKEN` instead of `PERSONAL_ACCESS_TOKEN`, the job won't trigger another workflow. It's like using `[skip ci]` in other CI's. + +```yaml +name: Bump version + +on: + push: + branches: + - master + +jobs: + bump-version: + if: "!startsWith(github.event.head_commit.message, 'bump:')" + runs-on: ubuntu-latest + name: "Bump version and create changelog with commitizen" + steps: + - name: Check out + uses: actions/checkout@v3 + with: + token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" + fetch-depth: 0 + - name: Create bump and changelog + uses: commitizen-tools/commitizen-action@master + with: + github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} +``` + +Push to master and that's it. + +### Creating a github release + +You can modify the previous action. + +Add the variable `changelog_increment_filename` in the `commitizen-action`, specifying +where to output the content of the changelog for the newly created version. + +And then add a step using a github action to create the release: `softprops/action-gh-release` + +The commitizen action creates an env variable called `REVISION`, containing the +newely created version. + +```yaml +- name: Create bump and changelog + uses: commitizen-tools/commitizen-action@master + with: + github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + changelog_increment_filename: body.md +- name: Release + uses: softprops/action-gh-release@v1 + with: + body_path: "body.md" + tag_name: ${{ env.REVISION }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` + +### Publishing a python package + +Once the new tag is created, triggering an automatic publish command would be desired. + +In order to do so, the credential needs to be added with the information of our PyPI account. + +Instead of using username and password, we suggest using [api token](https://pypi.org/help/#apitoken) generated from PyPI. + +After generate api token, use the token as the PyPI password and `__token__` as the username. + +Go to `Settings > Secrets > Add new secret` and add the secret: `PYPI_PASSWORD`. + +Create a file in `.github/workflows/pythonpublish.yaml` with the following content: + +```yaml +name: Upload Python Package + +on: + push: + tags: + - "*" # Will trigger for every tag, alternative: 'v*' + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: latest + virtualenvs-in-project: true + virtualenvs-create: true + - name: Install dependencies + run: | + poetry --version + poetry install + - name: Build and publish + env: + PYPI_USERNAME: __token__ + PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + ./scripts/publish +``` + +Notice that we are using poetry, and we are calling a bash script in `./scripts/publish`. You should configure the action, and the publish with your tools (twine, poetry, etc.). Check [commitizen example](https://github.com/commitizen-tools/commitizen/blob/master/scripts/publish) +You can also use [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) to publish your package. + +Push the changes and that's it. diff --git a/docs/tutorials/gitlab_ci.md b/docs/tutorials/gitlab_ci.md new file mode 100644 index 0000000..85abb3f --- /dev/null +++ b/docs/tutorials/gitlab_ci.md @@ -0,0 +1,113 @@ +## Create a new release using GitLab CI + +For this example, we have a `python/django` application and `Docker` as a containerization tool. + +_Goal_: Bump a new version every time that a change occurs on the `master` branch. The bump should be executed automatically by the `CI` process. + +### Development Workflow + +1. A developer creates a new commit on any branch (except `master`) +2. A developer creates a merge request (MR) against `master` branch +3. When the `MR` is merged into master, the 2 stages of the CI are executed +4. For simplification, we store the software version in a file called `VERSION`. You can use any file that you want as `commitizen` supports it. +5. The commit message executed automatically by the `CI` must include `[skip-ci]` in the message; otherwise, the process will generate a loop. You can define the message structure in [commitizen](../commands/bump.md) as well. + +### Gitlab Configuration + +To be able to change files and push new changes with `Gitlab CI` runners, we need to have a `ssh` key and configure a git user. + +First, let's create a `ssh key`. The only requirement is to create it without a passphrase: + +```bash +ssh-keygen -f deploy_key -N "" +``` + +The previous command will create a private and public key under the files `deploy_key` and `deploy_key.pub`. We will use them later. + +For the git user, we need an email and username. You can choose whatever you want; in this example, we choose `ci-runner@myproject.com` and `admin`, respectively. + +Now, we need to create three environment variables that will be visible for the runners. They should be created in the `variables` section under `settings/ci_cd`: + +![gitlab variables](../images/gitlab_ci/gitlab_variables.png) + +Create `SSH_PRIVATE_KEY`, `CI_EMAIL`, `CI_USERNAME` variables, and fill them with the `private_key`, `email` and `username` that we have created previously. + +The latest step is to create a `deploy key.` To do this, we should create it under the section `settings/repository` and fill it with the `public key` generated before. Check `Write access allowed`; otherwise, the runner won't be able to write the changes to the repository. + +![gitlab deploy key](../images/gitlab_ci/gitlab_deploy_key.png) + +If you have more projects under the same organization, you can reuse the deploy key created before, but you will have to repeat the step where we have created the environment variables (ssh key, email, and username). + +tip: If the CI raise some errors, try to unprotected the private key. + +### Defining GitLab CI Pipeline + +1. Create a `.gitlab-ci.yaml` file that contains `stages` and `jobs` configurations. You can find more info [here](https://docs.gitlab.com/ee/ci/quick_start/). + +2. Define `stages` and `jobs`. For this example, we define two `stages` with one `job` each one. + - Test the application. + - Auto bump the version. This means changing the file/s that reflects the version, creating a new commit and git tag. + +### Stages and Jobs + +```yaml +image: docker:latest + +services: + - docker:dind + +variables: + API_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME + +before_script: + - apk add --no-cache py-pip + - pip install docker-compose + +stages: + - test + - auto-bump + +test: + stage: test + script: + - docker-compose run -e DJANGO_ENVIRONMENT=dev your_project python manage.py test # run tests + +auto-bump: + stage: auto-bump + image: python:3.9 + before_script: + - "which ssh-agent || ( apt-get update -qy && apt-get install openssh-client -qqy )" + - eval `ssh-agent -s` + - echo "${SSH_PRIVATE_KEY}" | tr -d '\r' | ssh-add - > /dev/null # add ssh key + - pip3 install -U commitizen # install commitizen + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + - echo "$SSH_PUBLIC_KEY" >> ~/.ssh/id_rsa.pub + - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' + dependencies: + - test + script: + - git remote set-url origin git@gitlab.com:discover/rentee-core.git # git configuration + - git config --global user.email "${CI_EMAIL}" && git config --global user.name "${CI_USERNAME}" + - 'exists=`git show-ref refs/heads/master` && if [ -n "$exists" ]; then git branch -D master; fi' + - git checkout -b master + - cz bump --yes # execute auto bump and push to master + - git push origin master:$CI_COMMIT_REF_NAME + - TAG=$(head -n 1 VERSION) # get the new software version and save into artifacts + - echo "#!/bin/sh" >> variables + - echo "export TAG='$TAG'" >> variables + - git push origin $TAG + only: + refs: + - master + artifacts: + paths: + - variables +``` + +So, every time that a developer push to any branch, the `test` job is executed. If the branch is `master` and the test jobs success, the `auto-bump` takes place. +To be able to push using the Gitlab runner, we have to set the ssh key, configure git, and finally execute the auto bump. + +After merging the new changed into master, we have the final result: + +![gitlab final ci result](../images/gitlab_ci/gitlab_final_ci_result.png) diff --git a/docs/tutorials/jenkins_pipeline.md b/docs/tutorials/jenkins_pipeline.md new file mode 100644 index 0000000..fb87820 --- /dev/null +++ b/docs/tutorials/jenkins_pipeline.md @@ -0,0 +1,53 @@ +# Create a new release with Jenkins Pipelines + +For this we are using the modern approach of [declarative pipelines](https://www.jenkins.io/doc/book/pipeline/). + +You must also ensure your jenkins instance supports docker. +Most modern jenkins systems do have support for it, [they have embraced it](https://www.jenkins.io/doc/book/pipeline/docker/). + +```groovy +pipeline { + agent { + any + } + environment { + CI = 'true' + } + stages { + stage('Bump version') { + when { + beforeAgent true + expression { env.BRANCH_IS_PRIMARY } + not { + changelog '^bump:.+' + } + } + steps { + script { + useCz { + sh "cz bump --changelog" + } + // Here push back to your repository the new commit and tag + } + } + } + } +} + +def useCz(String authorName = 'Jenkins CI Server', String authorEmail = 'your-jenkins@email.com', String image = 'registry.hub.docker.com/commitizen/commitizen:latest', Closure body) { + docker + .image(image) + .inside("-u 0 -v $WORKSPACE:/workspace -w /workspace -e GIT_AUTHOR_NAME='${authorName}' -e GIT_AUTHOR_EMAIL='${authorEmail}' -entrypoint='/bin/sh'") { + sh 'git config --global --add safe.directory "*"' + sh "git config --global user.email '${authorName}'" + sh "git config --global user.name '${authorEmail}'" + body() + } +} +``` + +!!! warning + Using jenkins pipeline with any git plugin may require many different configurations, + you'll have to tinker with it until your pipelines properly detects git events. Check your + webhook in your git repository and check the "behaviors" and "build strategies" in + your pipeline settings. diff --git a/docs/tutorials/monorepo_guidance.md b/docs/tutorials/monorepo_guidance.md new file mode 100644 index 0000000..817f923 --- /dev/null +++ b/docs/tutorials/monorepo_guidance.md @@ -0,0 +1,81 @@ +# Configuring commitizen in a monorepo + +This tutorial assumes the monorepo layout is designed with multiple components that can be released independently of each +other, it also assumes that conventional commits with scopes are in use. Some suggested layouts: + +```shell-session +. +โ”œโ”€โ”€ library-b +โ”‚ย ย  โ””โ”€โ”€ .cz.toml +โ””โ”€โ”€ library-z + โ””โ”€โ”€ .cz.toml +``` + +```shell-session +src +โ”œโ”€โ”€ library-b +โ”‚ย ย  โ””โ”€โ”€ .cz.toml +โ””โ”€โ”€ library-z + โ””โ”€โ”€ .cz.toml +``` + +Sample `.cz.toml` for each component: + +```toml +# library-b/.cz.toml +[tool.commitizen] +name = "cz_customize" +version = "0.0.0" +tag_format = "${version}-library-b" # the component name can be a prefix or suffix with or without a separator +ignored_tag_formats = ["${version}-library-*"] # Avoid noise from other tags +update_changelog_on_bump = true +``` + +```toml +# library-z/.cz.toml +[tool.commitizen] +name = "cz_customize" +version = "0.0.0" +tag_format = "${version}-library-z" +ignored_tag_formats = ["${version}-library-*"] # Avoid noise from other tags +update_changelog_on_bump = true +``` + +And finally, to bump each of these: + +```sh +cz --config library-b/.cz.toml bump --yes +cz --config library-z/.cz.toml bump --yes +``` + + +## Changelog per component + +In order to filter the correct commits for each component, you'll have to come up with a strategy. + +For example: + +- Trigger the pipeline based on the changed path, which can have some downsides, as you'll rely on the developer not including files from other files + - [github actions](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#onpushpull_requestpull_request_targetpathspaths-ignore) uses `path` + - [Jenkins](https://www.jenkins.io/doc/book/pipeline/syntax/#built-in-conditions) uses `changeset` + - [Gitlab](https://docs.gitlab.com/ee/ci/yaml/#ruleschanges) uses `rules:changes` +- Filter certain pattern of the commit message (recommended) + + +### Example with scope in conventional commits + +For this example, to include the message in the changelog, we will require commits to use a specific scope. +This way, only relevant commits will be included in the appropriate change log for a given component, and any other commit will be ignored. + +Example config and commit for `library-b`: + +```toml +[tool.commitizen.customize] +changelog_pattern = "^(feat|fix)\\(library-b\\)(!)?:" #the pattern on types can be a wild card or any types you wish to include +``` + +A commit message looking like this, would be included: + +``` +fix:(library-b) Some awesome message +``` diff --git a/docs/tutorials/tag_format.md b/docs/tutorials/tag_format.md new file mode 100644 index 0000000..59c42be --- /dev/null +++ b/docs/tutorials/tag_format.md @@ -0,0 +1,101 @@ +# Managing tag formats + +## Tag format and version scheme + +For most projects, the tag format is simply the version number which is set like this: + +```yaml +[tool.commitizen] +tag_format: $version +version_scheme: pep440 +``` + +As this is the default value so you don't have to specify it. + +This setting means that: + +- The tag generated on bump will have this format: `1.0.0` : + - the version is generated following PEP440 scheme + - the tag is exactly the generated version +- All tags having this format will be recognized as version tag when: + - searching the last while bumping a release + - searching previous versions while: + - generating incremental changelog + - generating a changelog for a version range +- The changelog versions (section titles) will have this format +- The `scm` version provider will identify the current version using this tag format + +The version format will change depending on your configured version scheme. +For most, it will only impact pre-releases and [developmental releases](dev_releases.md) formats (i.e. `1.0.0-rc.1` vs. `1.0.0.rc1`) + +But you may need a different tagging convention, let's say using `semver` and prefixed with a `v`. +In this case you will define your settings like this: + +```yaml +[tool.commitizen] +tag_format: v${version} +version_scheme: semver +``` + +As a result, the tag generated on bump will have this format: `v1.0.0` and the version will be generated following `semver` scheme. + +!!! note + Both `$version` and `${version}` syntaxes are strictly equivalent. You can use the one you prefer. + +See [the `version_scheme` section in `bump` command documentation](../commands/bump.md#version_scheme) for more details on version schemes and how to define your own. +See [`tag_format`](../config.md#tag_format) and [`version_scheme`](../config.md#version_scheme) settings in [Configuration reference](../config.md) for more details on these settings. + +## Changing convention + +Now, let's say you need to change the tag format for some reason (company convention, [migration to a monorepo](monorepo_guidance.md)...). +You will obviously want to keep all those features working as expected. + +Commitizen can deal with it as long as you provide the legacy tag format in the configuration. + +Using the previous example, let say you want to move from `v${version}` to `component-${version}`. +Then `component-${version}` will be the new tag format and `v${version}` the legacy one. + +```yaml +[tool.commitizen] +tag_format: component-${version} +legacy_tag_formats: + - v${version} +``` + +This way, you won't loose your version history, you'll still be able to generate you changelog properly +and on the next version bump, your last version in the form `v${version}` will be properly recognizef if you use the `scm` version provider. +Your new tag will be in the form `component-${version}`. + +## Known tags to ignore + +Now let's say you have some known tags you want to ignore, either because they are not versions, either because they are not versions of the component you are dealing with. +As a consequence, you don't want them to trigger a warning because Commitizen detected an unknown tag format: + +Then you can tell Commitizen about it using the [`ignored_tag_formats`](../config.md#ignored_tag_formats) setting: + +```yaml +[tool.commitizen] +ignored_tag_formats: + - prod + - other-component-${version} + - prefix-* +``` + +This will ignore: + +- The `prod` tag +- Any version tag prefixed with `other-component-` +- Any tag prefixed with `prefix-` + + +!!! tip + Note the `*` in the `prefix-*` pattern. This is a wildcard and only exists for `ignored_tag_formats`. + It will match any string from any length. This allows to exclude by prefix, whether it is followed by a version or not. + +!!! tip + If you don't want to be warned when Commitizen detect an unknown tag, you can by setting: + ``` + [tool.commitizen] + ignored_tag_formats = ["*"] + ``` + But be aware that you will not be warned if you have a typo in your tag formats. diff --git a/docs/tutorials/writing_commits.md b/docs/tutorials/writing_commits.md new file mode 100644 index 0000000..9ba151c --- /dev/null +++ b/docs/tutorials/writing_commits.md @@ -0,0 +1,46 @@ +For this project to work well in your pipeline, a commit convention must be followed. + +By default commitizen uses the known [conventional commits][conventional_commits], but +you can create your own following the docs information over at +[customization][customization]. + +## Conventional commits + +If you are using [conventional commits][conventional_commits], the most important +thing to know is that you must begin your commits with at least one of these tags: +`fix`, `feat`. And if you introduce a breaking change, then, you must +add to your commit body the following `BREAKING CHANGE`. +Using these 3 keywords will allow the proper identification of the semantic version. +Of course, there are other keywords, but I'll leave it to the reader to explore them. + +## Writing commits + +Now to the important part, when writing commits, it's important to think about: + +- Your future self +- Your colleagues + +You may think this is trivial, but it's not. It's important for the reader to +understand what happened. + +Emojis may be added as well (e.g. see [cz-emoji][cz_emoji]), which requires the `utf-8`, or equivalent, character encoding to support unicode characters. By default, `commitizen` uses the `utf-8` character encoding, but a different encoding may be set through the `encoding` [configuration option][configuration]. + +### Recommendations + +- **Keep the message short**: Makes the list of commits more readable (~50 chars). +- **Talk imperative**: Follow this rule: `If applied, this commit will <commit message>` +- **Think about the CHANGELOG**: Your commits will probably end up in the changelog + so try writing for it, but also keep in mind that you can skip sending commits to the + CHANGELOG by using different keywords (like `build`). +- **Use a commit per new feature**: if you introduce multiple things related to the same + commit, squash them. This is useful for auto-generating CHANGELOG. + +| Do's | Don'ts | +| ---- | ------ | +| `fix(commands): bump error when no user provided` | `fix: stuff` | +| `feat: add new commit command` | `feat: commit command introduced` | + +[customization]: ../customization.md +[conventional_commits]: https://www.conventionalcommits.org +[cz_emoji]: https://commitizen-tools.github.io/commitizen/third-party-commitizen/#cz-emoji +[configuration]: ../config.md#encoding diff --git a/hooks/post-commit.py b/hooks/post-commit.py new file mode 100755 index 0000000..c7dea82 --- /dev/null +++ b/hooks/post-commit.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +from pathlib import Path + +try: + from commitizen.cz.utils import get_backup_file_path +except ImportError as error: + print(f"could not import commitizen:\n{error}") + exit(1) + + +def post_commit() -> None: + backup_file = Path(get_backup_file_path()) + + # remove backup file if it exists + if backup_file.is_file(): + backup_file.unlink() + + +if __name__ == "__main__": + post_commit() + exit(0) diff --git a/hooks/prepare-commit-msg.py b/hooks/prepare-commit-msg.py new file mode 100755 index 0000000..e666fa6 --- /dev/null +++ b/hooks/prepare-commit-msg.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +import shutil +import subprocess +import sys +from pathlib import Path +from subprocess import CalledProcessError + +try: + from commitizen.cz.utils import get_backup_file_path +except ImportError as error: + print("could not import commitizen:") + print(error) + exit(1) + + +def prepare_commit_msg(commit_msg_file: str) -> int: + # check if the commit message needs to be generated using commitizen + exit_code = subprocess.run( + [ + "cz", + "check", + "--commit-msg-file", + commit_msg_file, + ], + capture_output=True, + ).returncode + if exit_code != 0: + backup_file = Path(get_backup_file_path()) + if backup_file.is_file(): + # confirm if commit message from backup file should be reused + answer = input("retry with previous message? [y/N]: ") + if answer.lower() == "y": + shutil.copyfile(backup_file, commit_msg_file) + return 0 + + # use commitizen to generate the commit message + try: + subprocess.run( + [ + "cz", + "commit", + "--dry-run", + "--write-message-to-file", + commit_msg_file, + ], + stdin=sys.stdin, + stdout=sys.stdout, + ).check_returncode() + except CalledProcessError as error: + return error.returncode + + # write message to backup file + shutil.copyfile(commit_msg_file, backup_file) + return 0 + + +if __name__ == "__main__": + # make hook interactive by attaching /dev/tty to stdin + with open("/dev/tty") as tty: + sys.stdin = tty + exit(prepare_commit_msg(sys.argv[1])) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..6a64216 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,78 @@ +site_name: Commitizen +site_description: commit rules, semantic version, conventional commits + +theme: + name: "material" + palette: + - primary: 'deep purple' + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to system preference + +repo_name: commitizen-tools/commitizen +repo_url: https://github.com/commitizen-tools/commitizen +edit_uri: "" + +nav: + - Introduction: "README.md" + - Getting Started: "getting_started.md" + - Commands: + - init: "commands/init.md" + - commit: "commands/commit.md" + - bump: "commands/bump.md" + - check: "commands/check.md" + - changelog: "commands/changelog.md" + - example: "commands/example.md" + - info: "commands/info.md" + - ls: "commands/ls.md" + - schema: "commands/schema.md" + - version: "commands/version.md" + - Configuration: "config.md" + - Customization: "customization.md" + - Tutorials: + - Writing commits: "tutorials/writing_commits.md" + - Managing tags formats: "tutorials/tag_format.md" + - Auto check commits: "tutorials/auto_check.md" + - Auto prepare commit message: "tutorials/auto_prepare_commit_message.md" + - GitLab CI: "tutorials/gitlab_ci.md" + - Github Actions: "tutorials/github_actions.md" + - Jenkins pipeline: "tutorials/jenkins_pipeline.md" + - Developmental releases: "tutorials/dev_releases.md" + - Monorepo support: "tutorials/monorepo_guidance.md" + - FAQ: "faq.md" + - Exit Codes: "exit_codes.md" + - Third-Party Commitizen Templates: "third-party-commitizen.md" + - Contributing: "contributing.md" + - Resources: "external_links.md" + +markdown_extensions: + - markdown.extensions.codehilite: + guess_lang: false + - admonition + - codehilite + - extra + - pymdownx.highlight + - pymdownx.superfences + - toc: + permalink: true + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..073e1bc --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1968 @@ +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. + +[[package]] +name = "argcomplete" +version = "3.5.3" +description = "Bash tab completion for argparse" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "argcomplete-3.5.3-py3-none-any.whl", hash = "sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61"}, + {file = "argcomplete-3.5.3.tar.gz", hash = "sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392"}, +] + +[package.extras] +test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] + +[[package]] +name = "asttokens" +version = "2.4.1" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, +] + +[package.dependencies] +six = ">=1.12.0" + +[package.extras] +astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] +test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] + +[[package]] +name = "babel" +version = "2.16.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +groups = ["documentation"] +files = [ + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "cachetools" +version = "5.5.1" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb"}, + {file = "cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95"}, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +groups = ["documentation"] +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +groups = ["linters"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main", "documentation"] +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["documentation"] +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev", "documentation", "test"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.6.8" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["test"] +files = [ + {file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"}, + {file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"}, + {file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"}, + {file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"}, + {file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"}, + {file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"}, + {file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"}, + {file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"}, + {file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"}, + {file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"}, + {file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"}, + {file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"}, + {file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"}, + {file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"}, + {file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"}, + {file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"}, + {file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"}, + {file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"}, + {file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"}, + {file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"}, + {file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"}, + {file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"}, + {file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"}, + {file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"}, + {file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"}, + {file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "decli" +version = "0.6.2" +description = "Minimal, easy-to-use, declarative cli tool" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "decli-0.6.2-py3-none-any.whl", hash = "sha256:2fc84106ce9a8f523ed501ca543bdb7e416c064917c12a59ebdc7f311a97b7ed"}, + {file = "decli-0.6.2.tar.gz", hash = "sha256:36f71eb55fd0093895efb4f416ec32b7f6e00147dda448e3365cf73ceab42d6f"}, +] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "deprecated" +version = "1.2.18" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["test"] +files = [ + {file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"}, + {file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools", "tox"] + +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev", "linters"] +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["dev", "test"] +markers = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "execnet" +version = "2.1.1" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.8" +groups = ["test"] +files = [ + {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, + {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + +[[package]] +name = "executing" +version = "2.1.0" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf"}, + {file = "executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + +[[package]] +name = "filelock" +version = "3.16.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +groups = ["dev", "linters"] +files = [ + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2)"] + +[[package]] +name = "freezegun" +version = "1.5.1" +description = "Let your Python tests travel through time" +optional = false +python-versions = ">=3.7" +groups = ["test"] +files = [ + {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, + {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, +] + +[package.dependencies] +python-dateutil = ">=2.7" + +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +groups = ["documentation"] +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "identify" +version = "2.6.3" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +groups = ["linters"] +files = [ + {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, + {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["documentation"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "importlib-metadata" +version = "8.6.1" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main", "documentation"] +markers = "python_version < \"3.10\"" +files = [ + {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, + {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +groups = ["test"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "ipython" +version = "8.18.1" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, + {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +prompt-toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" +typing-extensions = {version = "*", markers = "python_version < \"3.10\""} + +[package.extras] +all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] + +[[package]] +name = "jedi" +version = "0.19.2" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, + {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, +] + +[package.dependencies] +parso = ">=0.8.4,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] + +[[package]] +name = "jinja2" +version = "3.1.5" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main", "documentation"] +files = [ + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markdown" +version = "3.7" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +groups = ["documentation"] +files = [ + {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, + {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +groups = ["script"] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main", "documentation"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, + {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["script"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for ๐Ÿ." +optional = false +python-versions = ">=3.6" +groups = ["documentation"] +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +groups = ["documentation"] +files = [ + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +groups = ["documentation"] +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mkdocs-material" +version = "9.5.50" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +groups = ["documentation"] +files = [ + {file = "mkdocs_material-9.5.50-py3-none-any.whl", hash = "sha256:f24100f234741f4d423a9d672a909d859668a4f404796be3cf035f10d6050385"}, + {file = "mkdocs_material-9.5.50.tar.gz", hash = "sha256:ae5fe16f3d7c9ccd05bb6916a7da7420cf99a9ce5e33debd9d40403a090d5825"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.6,<2.0" +mkdocs-material-extensions = ">=1.3,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<3)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.8" +groups = ["documentation"] +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + +[[package]] +name = "mypy" +version = "1.14.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +groups = ["linters"] +files = [ + {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, + {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"}, + {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"}, + {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"}, + {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"}, + {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"}, + {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"}, + {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"}, + {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"}, + {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"}, + {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"}, + {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"}, + {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"}, + {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"}, + {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"}, + {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +groups = ["linters"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["linters"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev", "documentation", "test"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "paginate" +version = "0.5.7" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +groups = ["documentation"] +files = [ + {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, + {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, +] + +[package.extras] +dev = ["pytest", "tox"] +lint = ["black"] + +[[package]] +name = "parso" +version = "0.8.4" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, + {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, +] + +[package.extras] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["docopt", "pytest"] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["documentation"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +groups = ["dev"] +markers = "sys_platform != \"win32\"" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +groups = ["dev", "documentation", "linters"] +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["dev", "test"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "4.1.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +groups = ["linters"] +files = [ + {file = "pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b"}, + {file = "pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "prompt-toolkit" +version = "3.0.48" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +groups = ["main", "dev"] +files = [ + {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, + {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +groups = ["dev"] +markers = "sys_platform != \"win32\"" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, + {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev", "documentation", "script"] +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pymdown-extensions" +version = "10.12" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +groups = ["documentation"] +files = [ + {file = "pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77"}, + {file = "pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7"}, +] + +[package.dependencies] +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + +[[package]] +name = "pyproject-api" +version = "1.9.0" +description = "API to interact with the python pyproject.toml based projects" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pyproject_api-1.9.0-py3-none-any.whl", hash = "sha256:326df9d68dea22d9d98b5243c46e3ca3161b07a1b9b18e213d1e24fd0e605766"}, + {file = "pyproject_api-1.9.0.tar.gz", hash = "sha256:7e8a9854b2dfb49454fae421cb86af43efbb2b2454e5646ffb7623540321ae6e"}, +] + +[package.dependencies] +packaging = ">=24.2" +tomli = {version = ">=2.2.1", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "setuptools (>=75.8)"] + +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +groups = ["test"] +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["test"] +files = [ + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, +] + +[package.dependencies] +coverage = {version = ">=7.5", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-datadir" +version = "1.5.0" +description = "pytest plugin for test data directories and files" +optional = false +python-versions = ">=3.8" +groups = ["test"] +files = [ + {file = "pytest-datadir-1.5.0.tar.gz", hash = "sha256:1617ed92f9afda0c877e4eac91904b5f779d24ba8f5e438752e3ae39d8d2ee3f"}, + {file = "pytest_datadir-1.5.0-py3-none-any.whl", hash = "sha256:34adf361bcc7b37961bbc1dfa8d25a4829e778bab461703c38a5c50ca9c36dc8"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[[package]] +name = "pytest-freezer" +version = "0.4.9" +description = "Pytest plugin providing a fixture interface for spulec/freezegun" +optional = false +python-versions = ">=3.6" +groups = ["test"] +files = [ + {file = "pytest_freezer-0.4.9-py3-none-any.whl", hash = "sha256:8b6c50523b7d4aec4590b52bfa5ff766d772ce506e2bf4846c88041ea9ccae59"}, + {file = "pytest_freezer-0.4.9.tar.gz", hash = "sha256:21bf16bc9cc46bf98f94382c4b5c3c389be7056ff0be33029111ae11b3f1c82a"}, +] + +[package.dependencies] +freezegun = ">=1.1" +pytest = ">=3.6" + +[[package]] +name = "pytest-mock" +version = "3.14.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +groups = ["test"] +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "pytest-regressions" +version = "2.7.0" +description = "Easy to use fixtures to write regression tests." +optional = false +python-versions = ">=3.9" +groups = ["test"] +files = [ + {file = "pytest_regressions-2.7.0-py3-none-any.whl", hash = "sha256:69f5e3f03493cf0ef84d96d23e50a546617c198b1d7746f2e2b9e441cbab4847"}, + {file = "pytest_regressions-2.7.0.tar.gz", hash = "sha256:4c30064e0923929012c94f5d6f35205be06fd8709c7f0dba0228e05c460af05e"}, +] + +[package.dependencies] +pytest = ">=6.2.0" +pytest-datadir = ">=1.2.0" +pyyaml = "*" + +[package.extras] +dataframe = ["numpy", "pandas"] +dev = ["matplotlib", "mypy", "numpy", "pandas", "pillow", "pre-commit", "pyarrow", "restructuredtext-lint", "tox"] +image = ["numpy", "pillow"] +num = ["numpy", "pandas"] + +[[package]] +name = "pytest-xdist" +version = "3.6.1" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +optional = false +python-versions = ">=3.8" +groups = ["test"] +files = [ + {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, + {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, +] + +[package.dependencies] +execnet = ">=2.1" +pytest = ">=7.0.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["documentation", "test"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main", "documentation", "linters", "test"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +groups = ["documentation"] +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "questionary" +version = "2.1.0" +description = "Python library to build pretty command line user prompts โญ๏ธ" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec"}, + {file = "questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587"}, +] + +[package.dependencies] +prompt_toolkit = ">=2.0,<4.0" + +[[package]] +name = "regex" +version = "2024.11.6" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.8" +groups = ["documentation"] +files = [ + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, + {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, + {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, + {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, + {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, + {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, + {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, + {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, + {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"}, + {file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"}, + {file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"}, + {file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"}, + {file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"}, + {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +groups = ["documentation"] +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["script"] +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "ruff" +version = "0.9.4" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["linters"] +files = [ + {file = "ruff-0.9.4-py3-none-linux_armv6l.whl", hash = "sha256:64e73d25b954f71ff100bb70f39f1ee09e880728efb4250c632ceed4e4cdf706"}, + {file = "ruff-0.9.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6ce6743ed64d9afab4fafeaea70d3631b4d4b28b592db21a5c2d1f0ef52934bf"}, + {file = "ruff-0.9.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:54499fb08408e32b57360f6f9de7157a5fec24ad79cb3f42ef2c3f3f728dfe2b"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37c892540108314a6f01f105040b5106aeb829fa5fb0561d2dcaf71485021137"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de9edf2ce4b9ddf43fd93e20ef635a900e25f622f87ed6e3047a664d0e8f810e"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c90c32357c74f11deb7fbb065126d91771b207bf9bfaaee01277ca59b574ec"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56acd6c694da3695a7461cc55775f3a409c3815ac467279dfa126061d84b314b"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0c93e7d47ed951b9394cf352d6695b31498e68fd5782d6cbc282425655f687a"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4c8772670aecf037d1bf7a07c39106574d143b26cfe5ed1787d2f31e800214"}, + {file = "ruff-0.9.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfc5f1d7afeda8d5d37660eeca6d389b142d7f2b5a1ab659d9214ebd0e025231"}, + {file = "ruff-0.9.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faa935fc00ae854d8b638c16a5f1ce881bc3f67446957dd6f2af440a5fc8526b"}, + {file = "ruff-0.9.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6c634fc6f5a0ceae1ab3e13c58183978185d131a29c425e4eaa9f40afe1e6d6"}, + {file = "ruff-0.9.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:433dedf6ddfdec7f1ac7575ec1eb9844fa60c4c8c2f8887a070672b8d353d34c"}, + {file = "ruff-0.9.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d612dbd0f3a919a8cc1d12037168bfa536862066808960e0cc901404b77968f0"}, + {file = "ruff-0.9.4-py3-none-win32.whl", hash = "sha256:db1192ddda2200671f9ef61d9597fcef89d934f5d1705e571a93a67fb13a4402"}, + {file = "ruff-0.9.4-py3-none-win_amd64.whl", hash = "sha256:05bebf4cdbe3ef75430d26c375773978950bbf4ee3c95ccb5448940dc092408e"}, + {file = "ruff-0.9.4-py3-none-win_arm64.whl", hash = "sha256:585792f1e81509e38ac5123492f8875fbc36f3ede8185af0a26df348e5154f41"}, + {file = "ruff-0.9.4.tar.gz", hash = "sha256:6907ee3529244bb0ed066683e075f09285b38dd5b4039370df6ff06041ca19e7"}, +] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["dev", "documentation", "test"] +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "termcolor" +version = "2.5.0" +description = "ANSI color formatting for output in terminal" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"}, + {file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev", "linters", "test"] +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] +markers = {dev = "python_version < \"3.11\"", linters = "python_version < \"3.11\"", test = "python_full_version <= \"3.11.0a6\""} + +[[package]] +name = "tomlkit" +version = "0.13.2" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + +[[package]] +name = "tox" +version = "4.24.1" +description = "tox is a generic virtualenv management and test command line tool" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tox-4.24.1-py3-none-any.whl", hash = "sha256:57ba7df7d199002c6df8c2db9e6484f3de6ca8f42013c083ea2d4d1e5c6bdc75"}, + {file = "tox-4.24.1.tar.gz", hash = "sha256:083a720adbc6166fff0b7d1df9d154f9d00bfccb9403b8abf6bc0ee435d6a62e"}, +] + +[package.dependencies] +cachetools = ">=5.5" +chardet = ">=5.2" +colorama = ">=0.4.6" +filelock = ">=3.16.1" +packaging = ">=24.2" +platformdirs = ">=4.3.6" +pluggy = ">=1.5" +pyproject-api = ">=1.8" +tomli = {version = ">=2.1", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.12.2", markers = "python_version < \"3.11\""} +virtualenv = ">=20.27.1" + +[package.extras] +test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.3)", "pytest-mock (>=3.14)"] + +[[package]] +name = "traitlets" +version = "5.14.3" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] + +[[package]] +name = "types-deprecated" +version = "1.2.15.20241117" +description = "Typing stubs for Deprecated" +optional = false +python-versions = ">=3.8" +groups = ["linters"] +files = [ + {file = "types-Deprecated-1.2.15.20241117.tar.gz", hash = "sha256:924002c8b7fddec51ba4949788a702411a2e3636cd9b2a33abd8ee119701d77e"}, + {file = "types_Deprecated-1.2.15.20241117-py3-none-any.whl", hash = "sha256:a0cc5e39f769fc54089fd8e005416b55d74aa03f6964d2ed1a0b0b2e28751884"}, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20241206" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.8" +groups = ["linters"] +files = [ + {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, + {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20241230" +description = "Typing stubs for PyYAML" +optional = false +python-versions = ">=3.8" +groups = ["linters"] +files = [ + {file = "types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6"}, + {file = "types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c"}, +] + +[[package]] +name = "types-termcolor" +version = "0.1.1" +description = "Typing stubs for termcolor" +optional = false +python-versions = "*" +groups = ["linters"] +files = [ + {file = "types-termcolor-0.1.1.tar.gz", hash = "sha256:4d9e09ce7f3267985f5280b22e25790c98cb64628b6466e1fb915dbb52ad7136"}, + {file = "types_termcolor-0.1.1-py2.py3-none-any.whl", hash = "sha256:3694c312e32f71fdc0f469c334ea21645f3130d90c93cd53bcb06b1233e174d5"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev", "linters", "script"] +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] +markers = {main = "python_version < \"3.11\"", dev = "python_version < \"3.11\"", script = "python_version < \"3.11\""} + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +groups = ["documentation"] +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.27.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev", "linters"] +files = [ + {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, + {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "watchdog" +version = "6.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.9" +groups = ["documentation"] +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +groups = ["main", "dev"] +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "wrapt" +version = "1.17.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.8" +groups = ["test"] +files = [ + {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df"}, + {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb"}, + {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301"}, + {file = "wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22"}, + {file = "wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575"}, + {file = "wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489"}, + {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d"}, + {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b"}, + {file = "wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346"}, + {file = "wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a"}, + {file = "wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451"}, + {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada"}, + {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4"}, + {file = "wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635"}, + {file = "wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7"}, + {file = "wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4"}, + {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90"}, + {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a"}, + {file = "wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045"}, + {file = "wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838"}, + {file = "wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d"}, + {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b"}, + {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab"}, + {file = "wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf"}, + {file = "wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a"}, + {file = "wrapt-1.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c"}, + {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627"}, + {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f"}, + {file = "wrapt-1.17.0-cp38-cp38-win32.whl", hash = "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea"}, + {file = "wrapt-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed"}, + {file = "wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578"}, + {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9"}, + {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0"}, + {file = "wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88"}, + {file = "wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977"}, + {file = "wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371"}, + {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, +] + +[[package]] +name = "zipp" +version = "3.21.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["main", "documentation"] +markers = "python_version < \"3.10\"" +files = [ + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.9,<4.0" +content-hash = "b0f8544806163bc0dddc039eb313f9d82119b845b3a19dedc381e9c88e8f4466" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6475ec0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,277 @@ +[project] +name = "commitizen" +version = "4.6.0" +description = "Python commitizen client tool" +authors = [{ name = "Santiago Fraire", email = "santiwilly@gmail.com" }] +maintainers = [ + { name = "Wei Lee", email = "weilee.rx@gmail.com" }, + { name = "Axel H.", email = "noirbizarre@gmail.com" }, +] +license = { file = "LICENSE" } +readme = "docs/README.md" +requires-python = ">=3.9,<4.0" +dependencies = [ + "questionary (>=2.0,<3.0)", + "decli (>=0.6.0,<1.0)", + "colorama (>=0.4.1,<1.0)", + "termcolor (>=1.1,<3)", + "packaging>=19", + "tomlkit (>=0.5.3,<1.0.0)", + "jinja2>=2.10.3", + "pyyaml>=3.08", + "argcomplete >=1.12.1,<3.6", + "typing-extensions (>=4.0.1,<5.0.0) ; python_version < '3.11'", + "charset-normalizer (>=2.1.0,<4)", + # Use the Python 3.11 and 3.12 compatible API: https://github.com/python/importlib_metadata#compatibility + "importlib_metadata (>=8.0.0,<9) ; python_version < '3.10'", + +] +keywords = ["commitizen", "conventional", "commits", "git"] +# See also: https://pypi.org/classifiers/ +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation :: CPython", + "License :: OSI Approved :: MIT License", +] + +[project.urls] +Homepage = "https://github.com/commitizen-tools/commitizen" +Documentation = "https://commitizen-tools.github.io/commitizen/" +Repository = "https://github.com/commitizen-tools/commitizen" +Issues = "https://github.com/commitizen-tools/commitizen/issues" +Changelog = "https://github.com/commitizen-tools/commitizen/blob/master/CHANGELOG.md" + +[project.scripts] +cz = "commitizen.cli:main" +git-cz = "commitizen.cli:main" + +[project.entry-points."commitizen.plugin"] +cz_conventional_commits = "commitizen.cz.conventional_commits:ConventionalCommitsCz" +cz_jira = "commitizen.cz.jira:JiraSmartCz" +cz_customize = "commitizen.cz.customize:CustomizeCommitsCz" + +[project.entry-points."commitizen.changelog_format"] +markdown = "commitizen.changelog_formats.markdown:Markdown" +asciidoc = "commitizen.changelog_formats.asciidoc:AsciiDoc" +textile = "commitizen.changelog_formats.textile:Textile" +restructuredtext = "commitizen.changelog_formats.restructuredtext:RestructuredText" + +[project.entry-points."commitizen.provider"] +cargo = "commitizen.providers:CargoProvider" +commitizen = "commitizen.providers:CommitizenProvider" +composer = "commitizen.providers:ComposerProvider" +npm = "commitizen.providers:NpmProvider" +pep621 = "commitizen.providers:Pep621Provider" +poetry = "commitizen.providers:PoetryProvider" +scm = "commitizen.providers:ScmProvider" +uv = "commitizen.providers:UvProvider" + +[project.entry-points."commitizen.scheme"] +pep440 = "commitizen.version_schemes:Pep440" +semver = "commitizen.version_schemes:SemVer" +semver2 = "commitizen.version_schemes:SemVer2" + +[build-system] +requires = ["poetry-core>=2.0"] +build-backend = "poetry.core.masonry.api" + + +[tool.commitizen] +version = "4.6.0" +tag_format = "v$version" +version_files = [ + "pyproject.toml:version", + "commitizen/__version__.py", + ".pre-commit-config.yaml:rev:.+Commitizen", +] +version_scheme = "pep440" + + +[tool.poetry] +packages = [{ include = "commitizen" }, { include = "commitizen/py.typed" }] + +[tool.poetry.requires-plugins] +"poethepoet" = ">=0.32.2" + +[tool.poetry.group.dev.dependencies] +ipython = "^8.0" +tox = ">4" + +[tool.poetry.group.test.dependencies] +pytest = ">=7.2,<9.0" +pytest-cov = ">=4,<7" +pytest-mock = "^3.10" +pytest-regressions = "^2.4.0" +pytest-freezer = "^0.4.6" +pytest-xdist = "^3.1.0" +deprecated = "^1.2.13" + +[tool.poetry.group.linters.dependencies] +ruff = ">=0.5.0,<0.10.0" +pre-commit = ">=2.18,<5.0" +mypy = "^1.4" +types-deprecated = "^1.2.9.2" +types-python-dateutil = "^2.8.19.13" +types-PyYAML = ">=5.4.3,<7.0.0" +types-termcolor = "^0.1.1" + +[tool.poetry.group.documentation.dependencies] +mkdocs = "^1.4.2" +mkdocs-material = "^9.1.6" + +[tool.poetry.group.script.dependencies] +# for scripts/gen_cli_help_screenshots.py +rich = "^13.7.1" + + +[tool.coverage] +[tool.coverage.report] +show_missing = true +exclude_lines = [ + # Have to re-enable the standard pragma + 'pragma: no cover', + + # Don't complain about missing debug-only code: + 'def __repr__', + 'if self\.debug', + + # Don't complain if tests don't hit defensive assertion code: + 'raise AssertionError', + 'raise NotImplementedError', + + # Don't complain if non-runnable code isn't run: + 'if 0:', + 'if __name__ == .__main__.:', + 'if TYPE_CHECKING:', +] +omit = [ + 'env/*', + 'venv/*', + '.venv/*', + '*/virtualenv/*', + '*/virtualenvs/*', + '*/tests/*', +] + + +[tool.pytest.ini_options] +addopts = "--strict-markers" +testpaths = ["tests/"] + +[tool.tox] +requires = ["tox>=4.22"] +env_list = ["3.9", "3.10", "3.11", "3.12", "3.13"] + +[tool.tox.env_run_base] +description = "Run tests suite against Python {base_python}" +skip_install = true +deps = ["poetry>=2.0"] +commands_pre = [["poetry", "install", "--only", "main,test"]] +commands = [["pytest", { replace = "posargs", extend = true }]] + +[tool.ruff] +line-length = 88 + +[tool.ruff.lint] +select = [ + # pycodestyle + "E", + # Pyflakes + "F", + # pyupgrade + "UP", + # isort + "I", +] +ignore = ["E501", "D1", "D415"] + +[tool.ruff.lint.isort] +known-first-party = ["commitizen", "tests"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.mypy] +files = "commitizen" +disallow_untyped_decorators = true +disallow_subclassing_any = true +warn_return_any = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_unused_configs = true + +[[tool.mypy.overrides]] +module = "py.*" # Legacy pytest dependencies +ignore_missing_imports = true + +[tool.codespell] +# Ref: https://github.com/codespell-project/codespell#using-a-config-file +skip = '.git*,*.svg,*.lock' +check-hidden = true +ignore-words-list = 'asend' + +[tool.poe] +poetry_command = "" + +[tool.poe.tasks] +format.help = "Format the code" +format.sequence = [ + { cmd = "ruff check --fix commitizen tests" }, + { cmd = "ruff format commitizen tests" }, +] + +lint.help = "Lint the code" +lint.sequence = [ + { cmd = "ruff check commitizen/ tests/ --fix" }, + { cmd = "mypy commitizen/ tests/" }, +] + +check-commit.help = "Check the commit message" +check-commit.cmd = "cz -nr 3 check --rev-range origin/master.." + +test.help = "Run the test suite" +test.cmd = "pytest -n 3 --dist=loadfile" + +"test:all".help = "Run the test suite on all supported Python versions" +"test:all".cmd = "tox --parallel" + +cover.help = "Run the test suite with coverage" +cover.ref = "test --cov-report term-missing --cov-report=xml:coverage.xml --cov=commitizen" + +all.help = "Run all tasks" +all.sequence = [ + "format", + "lint", + "cover", + "check-commit", +] + +"doc:screenshots".help = "Render documentation screeenshots" +"doc:screenshots".script = "scripts.gen_cli_help_screenshots:gen_cli_help_screenshots" + +"doc:build".help = "Build the documentation" +"doc:build".cmd = "mkdocs build" + +doc.help = "Live documentation server" +doc.cmd = "mkdocs serve" + +ci.help = "Run all tasks in CI" +ci.sequence = [ + { cmd = "pre-commit run --all-files" }, + "cover", +] +ci.env = { SKIP = "no-commit-to-branch" } + +setup-pre-commit.help = "Install pre-commit hooks" +setup-pre-commit.cmd = "pre-commit install" diff --git a/scripts/gen_cli_help_screenshots.py b/scripts/gen_cli_help_screenshots.py new file mode 100644 index 0000000..0706612 --- /dev/null +++ b/scripts/gen_cli_help_screenshots.py @@ -0,0 +1,42 @@ +import os +import subprocess +from pathlib import Path + +from rich.console import Console + +from commitizen.cli import data + +project_root = Path(__file__).parent.parent.absolute() +images_root = project_root / Path("docs") / Path("images") / Path("cli_help") + + +def gen_cli_help_screenshots() -> None: + """Generate the screenshot for help message on each cli command and save them as svg files.""" + if not os.path.exists(images_root): + os.makedirs(images_root) + print(f"Created {images_root}") + + help_cmds = _list_help_cmds() + for cmd in help_cmds: + file_name = f"{cmd.replace(' ', '_').replace('-', '_')}.svg" + _export_cmd_as_svg(cmd, f"{images_root}/{file_name}") + + +def _list_help_cmds() -> list[str]: + cmds = [f"{data['prog']} --help"] + [ + f"{data['prog']} {sub_c['name'] if isinstance(sub_c['name'], str) else sub_c['name'][0]} --help" + for sub_c in data["subcommands"]["commands"] + ] + + return cmds + + +def _export_cmd_as_svg(cmd: str, file_name: str) -> None: + stdout = subprocess.run(cmd, shell=True, capture_output=True).stdout.decode("utf-8") + console = Console(record=True, width=80) + console.print(f"$ {cmd}\n{stdout}") + console.save_svg(file_name, title="") + + +if __name__ == "__main__": + gen_cli_help_screenshots() diff --git a/tests/CHANGELOG_FOR_TEST.md b/tests/CHANGELOG_FOR_TEST.md new file mode 100644 index 0000000..e92ca1c --- /dev/null +++ b/tests/CHANGELOG_FOR_TEST.md @@ -0,0 +1,129 @@ + +## v1.2.0 (2019-04-19) + +### feat + +- custom cz plugins now support bumping version + +## v1.1.1 (2019-04-18) + +### refactor + +- changed stdout statements +- **schema**: command logic removed from commitizen base +- **info**: command logic removed from commitizen base +- **example**: command logic removed from commitizen base +- **commit**: moved most of the commit logic to the commit command + +### fix + +- **bump**: commit message now fits better with semver +- conventional commit 'breaking change' in body instead of title + +## v1.1.0 (2019-04-14) + +### feat + +- new working bump command +- create version tag +- update given files with new version +- **config**: new set key, used to set version to cfg +- support for pyproject.toml +- first semantic version bump implementation + +### fix + +- removed all from commit +- fix config file not working + +### refactor + +- added commands folder, better integration with decli + +## v1.0.0 (2019-03-01) + +### refactor + +- removed delegator, added decli and many tests + +### BREAKING CHANGE + +- API is stable + +## 1.0.0b2 (2019-01-18) + +## v1.0.0b1 (2019-01-17) + +### feat + +- py3 only, tests and conventional commits 1.0 + +## v0.9.11 (2018-12-17) + +### fix + +- **config**: load config reads in order without failing if there is no commitizen section + +## v0.9.10 (2018-09-22) + +### fix + +- parse scope (this is my punishment for not having tests) + +## v0.9.9 (2018-09-22) + +### fix + +- parse scope empty + +## v0.9.8 (2018-09-22) + +### fix + +- **scope**: parse correctly again + +## v0.9.7 (2018-09-22) + +### fix + +- **scope**: parse correctly + +## v0.9.6 (2018-09-19) + +### refactor + +- **conventionalCommit**: moved filters to questions instead of message + +### fix + +- **manifest**: included missing files + +## v0.9.5 (2018-08-24) + +### fix + +- **config**: home path for python versions between 3.0 and 3.5 + +## v0.9.4 (2018-08-02) + +### feat + +- **cli**: added version + +## v0.9.3 (2018-07-28) + +### feat + +- **committer**: conventional commit is a bit more intelligent now + +## v0.9.2 (2017-11-11) + +### refactor + +- renamed conventional_changelog to conventional_commits, not backward compatible + +## v0.9.1 (2017-11-11) + +### fix + +- **setup.py**: future is now required for every python version diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/commands/conftest.py b/tests/commands/conftest.py new file mode 100644 index 0000000..9193184 --- /dev/null +++ b/tests/commands/conftest.py @@ -0,0 +1,53 @@ +import os + +import pytest + +from commitizen import defaults +from commitizen.config import BaseConfig, JsonConfig + + +@pytest.fixture() +def config(): + _config = BaseConfig() + _config.settings.update({"name": defaults.DEFAULT_SETTINGS["name"]}) + return _config + + +@pytest.fixture() +def config_customize(): + json_string = r"""{ + "commitizen": { + "name": "cz_customize", + "version": "3.0.0", + "changelog_incremental": "true", + "customize": { + "message_template": "{{prefix}}({{scope}}): {{subject}}\n\n{{body}}{% if is_breaking_change %}\nBREAKING CHANGE: {{footer}}{% endif %}", + "schema": "<type>(<scope>): <subject>\n<BLANK LINE>\n<body>\n<BLANK LINE>\n(BREAKING CHANGE: <footer>)", + "schema_pattern": "(build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert|bump)(\\(\\S+\\))?!?:(\\s.*)", + "change_type_map": { + "feat": "Feat", + "fix": "Fix", + "refactor": "Refactor", + "perf": "Perf" + }, + "change_type_order": ["Refactor", "Feat"], + "commit_parser": "^(?P<change_type>feat|fix|refactor|perf|BREAKING CHANGE)(?:\\((?P<scope>[^()\\r\\n]*)\\)|\\()?(?P<breaking>!)?:\\s(?P<message>.*)?", + "changelog_pattern": "^(BREAKING[\\-\\ ]CHANGE|feat|fix|refactor|perf)(\\(.+\\))?(!)?", + "questions": [ + + ] + } + } + }""" + _config = JsonConfig(data=json_string, path="not_exist.json") + return _config + + +@pytest.fixture() +def changelog_path() -> str: + return os.path.join(os.getcwd(), "CHANGELOG.md") + + +@pytest.fixture() +def config_path() -> str: + return os.path.join(os.getcwd(), "pyproject.toml") diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py new file mode 100644 index 0000000..b5ff7e6 --- /dev/null +++ b/tests/commands/test_bump_command.py @@ -0,0 +1,1658 @@ +from __future__ import annotations + +import inspect +import re +import sys +from pathlib import Path +from textwrap import dedent +from unittest.mock import MagicMock, call + +import py +import pytest +from pytest_mock import MockFixture + +import commitizen.commands.bump as bump +from commitizen import cli, cmd, git, hooks +from commitizen.changelog_formats import ChangelogFormat +from commitizen.cz.base import BaseCommitizen +from commitizen.exceptions import ( + BumpTagFailedError, + CommitizenException, + CurrentVersionNotFoundError, + DryRunExit, + ExitCode, + ExpectedExit, + GetNextExit, + InvalidManualVersion, + NoCommitsFoundError, + NoneIncrementExit, + NoPatternMapError, + NotAGitProjectError, + NotAllowed, + NoVersionSpecifiedError, +) +from tests.utils import create_file_and_commit, create_tag, skip_below_py_3_13 + + +@pytest.mark.parametrize( + "commit_msg", + ( + "fix: username exception", + "fix(user): username exception", + "refactor: remove ini configuration support", + "refactor(config): remove ini configuration support", + "perf: update to use multiproess", + "perf(worker): update to use multiproess", + ), +) +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_patch_increment(commit_msg, mocker: MockFixture): + create_file_and_commit(commit_msg) + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + tag_exists = git.tag_exist("0.1.1") + assert tag_exists is True + + +@pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file")) +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_minor_increment(commit_msg, mocker: MockFixture): + create_file_and_commit(commit_msg) + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + tag_exists = git.tag_exist("0.2.0") + cmd_res = cmd.run('git for-each-ref refs/tags --format "%(objecttype):%(refname)"') + assert tag_exists is True and "commit:refs/tags/0.2.0\n" in cmd_res.out + + +@pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file")) +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_minor_increment_annotated(commit_msg, mocker: MockFixture): + create_file_and_commit(commit_msg) + testargs = ["cz", "bump", "--yes", "--annotated-tag"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + tag_exists = git.tag_exist("0.2.0") + cmd_res = cmd.run('git for-each-ref refs/tags --format "%(objecttype):%(refname)"') + assert tag_exists is True and "tag:refs/tags/0.2.0\n" in cmd_res.out + + _is_signed = git.is_signed_tag("0.2.0") + assert _is_signed is False + + +@pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file")) +@pytest.mark.usefixtures("tmp_commitizen_project_with_gpg") +def test_bump_minor_increment_signed(commit_msg, mocker: MockFixture): + create_file_and_commit(commit_msg) + testargs = ["cz", "bump", "--yes", "--gpg-sign"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + tag_exists = git.tag_exist("0.2.0") + cmd_res = cmd.run('git for-each-ref refs/tags --format "%(objecttype):%(refname)"') + assert tag_exists is True and "tag:refs/tags/0.2.0\n" in cmd_res.out + + _is_signed = git.is_signed_tag("0.2.0") + assert _is_signed is True + + +@pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file")) +def test_bump_minor_increment_annotated_config_file( + commit_msg, mocker: MockFixture, tmp_commitizen_project +): + tmp_commitizen_cfg_file = tmp_commitizen_project.join("pyproject.toml") + tmp_commitizen_cfg_file.write( + f"{tmp_commitizen_cfg_file.read()}\nannotated_tag = 1" + ) + create_file_and_commit(commit_msg) + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + tag_exists = git.tag_exist("0.2.0") + cmd_res = cmd.run('git for-each-ref refs/tags --format "%(objecttype):%(refname)"') + assert tag_exists is True and "tag:refs/tags/0.2.0\n" in cmd_res.out + + _is_signed = git.is_signed_tag("0.2.0") + assert _is_signed is False + + +@pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file")) +def test_bump_minor_increment_signed_config_file( + commit_msg, mocker: MockFixture, tmp_commitizen_project_with_gpg +): + tmp_commitizen_cfg_file = tmp_commitizen_project_with_gpg.join("pyproject.toml") + tmp_commitizen_cfg_file.write(f"{tmp_commitizen_cfg_file.read()}\ngpg_sign = 1") + create_file_and_commit(commit_msg) + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + tag_exists = git.tag_exist("0.2.0") + cmd_res = cmd.run('git for-each-ref refs/tags --format "%(objecttype):%(refname)"') + assert tag_exists is True and "tag:refs/tags/0.2.0\n" in cmd_res.out + + _is_signed = git.is_signed_tag("0.2.0") + assert _is_signed is True + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.parametrize( + "commit_msg", + ( + "feat: new user interface\n\nBREAKING CHANGE: age is no longer supported", + "feat!: new user interface\n\nBREAKING CHANGE: age is no longer supported", + "feat!: new user interface", + "feat(user): new user interface\n\nBREAKING CHANGE: age is no longer supported", + "feat(user)!: new user interface\n\nBREAKING CHANGE: age is no longer supported", + "feat(user)!: new user interface", + "BREAKING CHANGE: age is no longer supported", + "BREAKING-CHANGE: age is no longer supported", + ), +) +def test_bump_major_increment(commit_msg, mocker: MockFixture): + create_file_and_commit(commit_msg) + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("1.0.0") + assert tag_exists is True + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.parametrize( + "commit_msg", + ( + "feat: new user interface\n\nBREAKING CHANGE: age is no longer supported", + "feat!: new user interface\n\nBREAKING CHANGE: age is no longer supported", + "feat!: new user interface", + "feat(user): new user interface\n\nBREAKING CHANGE: age is no longer supported", + "feat(user)!: new user interface\n\nBREAKING CHANGE: age is no longer supported", + "feat(user)!: new user interface", + "BREAKING CHANGE: age is no longer supported", + "BREAKING-CHANGE: age is no longer supported", + ), +) +def test_bump_major_increment_major_version_zero(commit_msg, mocker): + create_file_and_commit(commit_msg) + + testargs = ["cz", "bump", "--yes", "--major-version-zero"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is True + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.parametrize( + "commit_msg,increment,expected_tag", + [ + ("feat: new file", "PATCH", "0.1.1"), + ("fix: username exception", "major", "1.0.0"), + ("refactor: remove ini configuration support", "patch", "0.1.1"), + ("BREAKING CHANGE: age is no longer supported", "minor", "0.2.0"), + ], +) +def test_bump_command_increment_option( + commit_msg, increment, expected_tag, mocker: MockFixture +): + create_file_and_commit(commit_msg) + + testargs = ["cz", "bump", "--increment", increment, "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist(expected_tag) + assert tag_exists is True + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_command_prelease(mocker: MockFixture): + create_file_and_commit("feat: location") + + # Create an alpha pre-release. + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0a0") + assert tag_exists is True + + # Create a beta pre-release. + testargs = ["cz", "bump", "--prerelease", "beta", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0b0") + assert tag_exists is True + + # With a current beta pre-release, bumping alpha must bump beta + # because we can't bump "backwards". + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0a1") + assert tag_exists is False + tag_exists = git.tag_exist("0.2.0b1") + assert tag_exists is True + + # Create a rc pre-release. + testargs = ["cz", "bump", "--prerelease", "rc", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0rc0") + assert tag_exists is True + + # With a current rc pre-release, bumping alpha must bump rc. + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0a1") + assert tag_exists is False + tag_exists = git.tag_exist("0.2.0b2") + assert tag_exists is False + tag_exists = git.tag_exist("0.2.0rc1") + assert tag_exists is True + + # With a current rc pre-release, bumping beta must bump rc. + testargs = ["cz", "bump", "--prerelease", "beta", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0a2") + assert tag_exists is False + tag_exists = git.tag_exist("0.2.0b2") + assert tag_exists is False + tag_exists = git.tag_exist("0.2.0rc2") + assert tag_exists is True + + # Create a final release from the current pre-release. + testargs = ["cz", "bump"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is True + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_command_prelease_increment(mocker: MockFixture): + # FINAL RELEASE + create_file_and_commit("fix: location") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + assert git.tag_exist("0.1.1") + + # PRERELEASE + create_file_and_commit("fix: location") + + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + assert git.tag_exist("0.1.2a0") + + create_file_and_commit("feat: location") + + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + assert git.tag_exist("0.2.0a0") + + create_file_and_commit("feat!: breaking") + + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + assert git.tag_exist("1.0.0a0") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_command_prelease_exact_mode(mocker: MockFixture): + # PRERELEASE + create_file_and_commit("feat: location") + + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0a0") + assert tag_exists is True + + # PRERELEASE + PATCH BUMP + testargs = [ + "cz", + "bump", + "--prerelease", + "alpha", + "--yes", + "--increment-mode=exact", + ] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0a1") + assert tag_exists is True + + # PRERELEASE + MINOR BUMP + # --increment-mode allows the minor version to bump, and restart the prerelease + create_file_and_commit("feat: location") + + testargs = [ + "cz", + "bump", + "--prerelease", + "alpha", + "--yes", + "--increment-mode=exact", + ] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.3.0a0") + assert tag_exists is True + + # PRERELEASE + MAJOR BUMP + # --increment-mode=exact allows the major version to bump, and restart the prerelease + testargs = [ + "cz", + "bump", + "--prerelease", + "alpha", + "--yes", + "--increment=MAJOR", + "--increment-mode=exact", + ] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("1.0.0a0") + assert tag_exists is True + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_on_git_with_hooks_no_verify_disabled(mocker: MockFixture): + """Bump commit without --no-verify""" + cmd.run("mkdir .git/hooks") + with open(".git/hooks/pre-commit", "w", encoding="utf-8") as f: + f.write('#!/usr/bin/env bash\necho "0.1.0"') + cmd.run("chmod +x .git/hooks/pre-commit") + + # MINOR + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + + cli.main() + + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is True + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_tag_exists_raises_exception(mocker: MockFixture): + cmd.run("mkdir .git/hooks") + with open(".git/hooks/post-commit", "w", encoding="utf-8") as f: + f.write("#!/usr/bin/env bash\nexit 9") + cmd.run("chmod +x .git/hooks/post-commit") + + # MINOR + create_file_and_commit("feat: new file") + git.tag("0.2.0") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(BumpTagFailedError) as excinfo: + cli.main() + assert "0.2.0" in str(excinfo.value) # This should be a fatal error + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_on_git_with_hooks_no_verify_enabled(mocker: MockFixture): + cmd.run("mkdir .git/hooks") + with open(".git/hooks/pre-commit", "w", encoding="utf-8") as f: + f.write('#!/usr/bin/env bash\necho "0.1.0"') + cmd.run("chmod +x .git/hooks/pre-commit") + + # MINOR + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes", "--no-verify"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is True + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_when_bumpping_is_not_support(mocker: MockFixture): + create_file_and_commit( + "feat: new user interface\n\nBREAKING CHANGE: age is no longer supported" + ) + + testargs = ["cz", "-n", "cz_jira", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NoPatternMapError) as excinfo: + cli.main() + + assert "'cz_jira' rule does not support bump" in str(excinfo.value) + + +@pytest.mark.usefixtures("tmp_git_project") +def test_bump_when_version_is_not_specify(mocker: MockFixture): + mocker.patch.object(sys, "argv", ["cz", "bump"]) + + with pytest.raises(NoVersionSpecifiedError) as excinfo: + cli.main() + + assert NoVersionSpecifiedError.message in str(excinfo.value) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_when_no_new_commit(mocker: MockFixture): + """bump without any commits since the last bump.""" + # We need this first commit otherwise the revision is invalid. + create_file_and_commit("feat: initial") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + + # First bump. + # The next bump should fail since + # there is not a commit between the two bumps. + cli.main() + + # bump without a new commit. + with pytest.raises(NoCommitsFoundError) as excinfo: + cli.main() + + expected_error_message = "[NO_COMMITS_FOUND]\nNo new commits found." + assert expected_error_message in str(excinfo.value) + + +def test_bump_when_version_inconsistent_in_version_files( + tmp_commitizen_project, mocker: MockFixture +): + tmp_version_file = tmp_commitizen_project.join("__version__.py") + tmp_version_file.write("100.999.10000") + tmp_commitizen_cfg_file = tmp_commitizen_project.join("pyproject.toml") + tmp_version_file_string = str(tmp_version_file).replace("\\", "/") + tmp_commitizen_cfg_file.write( + f"{tmp_commitizen_cfg_file.read()}\n" + f'version_files = ["{tmp_version_file_string}"]' + ) + + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes", "--check-consistency"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(CurrentVersionNotFoundError) as excinfo: + cli.main() + + partial_expected_error_message = "Current version 0.1.0 is not found in" + assert partial_expected_error_message in str(excinfo.value) + + +def test_bump_major_version_zero_when_major_is_not_zero(mocker, tmp_commitizen_project): + tmp_version_file = tmp_commitizen_project.join("__version__.py") + tmp_version_file.write("1.0.0") + tmp_version_file_string = str(tmp_version_file).replace("\\", "/") + tmp_commitizen_cfg_file = tmp_commitizen_project.join("pyproject.toml") + tmp_commitizen_cfg_file.write( + f"[tool.commitizen]\n" + 'version="1.0.0"\n' + f'version_files = ["{str(tmp_version_file_string)}"]' + ) + tmp_changelog_file = tmp_commitizen_project.join("CHANGELOG.md") + tmp_changelog_file.write("## v1.0.0") + + create_file_and_commit("feat(user): new file") + create_tag("v1.0.0") + create_file_and_commit("feat(user)!: new file") + + testargs = ["cz", "bump", "--yes", "--major-version-zero"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NotAllowed) as excinfo: + cli.main() + + expected_error_message = ( + "--major-version-zero is meaningless for current version 1.0.0" + ) + assert expected_error_message in str(excinfo.value) + + +def test_bump_files_only(mocker: MockFixture, tmp_commitizen_project): + tmp_version_file = tmp_commitizen_project.join("__version__.py") + tmp_version_file.write("0.1.0") + tmp_commitizen_cfg_file = tmp_commitizen_project.join("pyproject.toml") + tmp_version_file_string = str(tmp_version_file).replace("\\", "/") + tmp_commitizen_cfg_file.write( + f"{tmp_commitizen_cfg_file.read()}\n" + f'version_files = ["{tmp_version_file_string}"]' + ) + + create_file_and_commit("feat: new user interface") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is True + + create_file_and_commit("feat: another new feature") + testargs = ["cz", "bump", "--yes", "--files-only"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(ExpectedExit): + cli.main() + + tag_exists = git.tag_exist("0.3.0") + assert tag_exists is False + + with open(tmp_version_file, encoding="utf-8") as f: + assert "0.3.0" in f.read() + + with open(tmp_commitizen_cfg_file, encoding="utf-8") as f: + assert "0.3.0" in f.read() + + +def test_bump_local_version(mocker: MockFixture, tmp_commitizen_project): + tmp_version_file = tmp_commitizen_project.join("__version__.py") + tmp_version_file.write("4.5.1+0.1.0") + tmp_commitizen_cfg_file = tmp_commitizen_project.join("pyproject.toml") + tmp_version_file_string = str(tmp_version_file).replace("\\", "/") + tmp_commitizen_cfg_file.write( + f"[tool.commitizen]\n" + 'version="4.5.1+0.1.0"\n' + f'version_files = ["{tmp_version_file_string}"]' + ) + + create_file_and_commit("feat: new user interface") + testargs = ["cz", "bump", "--yes", "--local-version"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + tag_exists = git.tag_exist("4.5.1+0.2.0") + assert tag_exists is True + + with open(tmp_version_file, encoding="utf-8") as f: + assert "4.5.1+0.2.0" in f.read() + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_dry_run(mocker: MockFixture, capsys): + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(DryRunExit): + cli.main() + + out, _ = capsys.readouterr() + assert "0.2.0" in out + + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is False + + +def test_bump_in_non_git_project(tmpdir, config, mocker: MockFixture): + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + + with tmpdir.as_cwd(): + with pytest.raises(NotAGitProjectError): + with pytest.raises(ExpectedExit): + cli.main() + + +def test_none_increment_exit_should_be_a_class(): + assert inspect.isclass(NoneIncrementExit) + + +def test_none_increment_exit_should_be_expected_exit_subclass(): + assert issubclass(NoneIncrementExit, CommitizenException) + + +def test_none_increment_exit_should_exist_in_bump(): + assert hasattr(bump, "NoneIncrementExit") + + +def test_none_increment_exit_is_exception(): + assert bump.NoneIncrementExit == NoneIncrementExit + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_none_increment_should_not_call_git_tag_and_error_code_is_not_zero( + mocker: MockFixture, +): + create_file_and_commit("test(test_get_all_droplets): fix bad comparison test") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + + # stash git.tag for later restore + stashed_git_tag = git.tag + dummy_value = git.tag("0.0.2") + git.tag = MagicMock(return_value=dummy_value) + + with pytest.raises(NoneIncrementExit): + try: + cli.main() + except NoneIncrementExit as e: + git.tag.assert_not_called() + assert e.exit_code == ExitCode.NO_INCREMENT + raise e + + # restore pop stashed + git.tag = stashed_git_tag + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_with_changelog_arg(mocker: MockFixture, changelog_path): + create_file_and_commit("feat(user): new file") + testargs = ["cz", "bump", "--yes", "--changelog"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is True + + with open(changelog_path, encoding="utf-8") as f: + out = f.read() + assert out.startswith("#") + assert "0.2.0" in out + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_with_changelog_config(mocker: MockFixture, changelog_path, config_path): + create_file_and_commit("feat(user): new file") + with open(config_path, "a", encoding="utf-8") as fp: + fp.write("update_changelog_on_bump = true\n") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is True + + with open(changelog_path, encoding="utf-8") as f: + out = f.read() + assert out.startswith("#") + assert "0.2.0" in out + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_prevent_prerelease_when_no_increment_detected(mocker: MockFixture, capsys): + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + + cli.main() + out, _ = capsys.readouterr() + + assert "0.2.0" in out + + create_file_and_commit("test: new file") + testargs = ["cz", "bump", "-pr", "beta"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NoCommitsFoundError) as excinfo: + cli.main() + + expected_error_message = ( + "[NO_COMMITS_FOUND]\nNo commits found to generate a pre-release." + ) + assert expected_error_message in str(excinfo.value) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_with_changelog_to_stdout_arg(mocker: MockFixture, capsys, changelog_path): + create_file_and_commit("feat(user): this should appear in stdout") + testargs = ["cz", "bump", "--yes", "--changelog-to-stdout"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + out, _ = capsys.readouterr() + + assert "this should appear in stdout" in out + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is True + + with open(changelog_path, encoding="utf-8") as f: + out = f.read() + assert out.startswith("#") + assert "0.2.0" in out + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_with_changelog_to_stdout_dry_run_arg( + mocker: MockFixture, capsys, changelog_path +): + create_file_and_commit( + "feat(user): this should appear in stdout with dry-run enabled" + ) + testargs = ["cz", "bump", "--yes", "--changelog-to-stdout", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(DryRunExit): + cli.main() + out, _ = capsys.readouterr() + + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is False + assert out.startswith("#") + assert "this should appear in stdout with dry-run enabled" in out + assert "0.2.0" in out + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_without_git_to_stdout_arg(mocker: MockFixture, capsys, changelog_path): + create_file_and_commit("feat(user): this should appear in stdout") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + out, _ = capsys.readouterr() + + assert ( + re.search(r"^\[master \w+] bump: version 0.1.0 โ†’ 0.2.0", out, re.MULTILINE) + is not None + ) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_with_git_to_stdout_arg(mocker: MockFixture, capsys, changelog_path): + create_file_and_commit("feat(user): this should appear in stdout") + testargs = ["cz", "bump", "--yes", "--git-output-to-stderr"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + out, _ = capsys.readouterr() + + assert ( + re.search(r"^\[master \w+] bump: version 0.1.0 โ†’ 0.2.0", out, re.MULTILINE) + is None + ) + + +@pytest.mark.parametrize( + "version_filepath, version_regex, version_file_content", + [ + pytest.param( + "pyproject.toml", + "pyproject.toml:^version", + """ +[tool.poetry] +name = "my_package" +version = "0.1.0" +""", + id="version in pyproject.toml with regex", + ), + pytest.param( + "pyproject.toml", + "pyproject.toml", + """ +[tool.poetry] +name = "my_package" +version = "0.1.0" +""", + id="version in pyproject.toml without regex", + ), + pytest.param( + "__init__.py", + "__init__.py:^__version__", + """ +'''This is a test file.''' +__version__ = "0.1.0" +""", + id="version in __init__.py with regex", + ), + pytest.param( + "pyproject.toml", + "*.toml:^version", + """ +[tool.poetry] +name = "my_package" +version = "0.1.0" +""", + id="version in pyproject.toml with glob and regex", + ), + ], +) +@pytest.mark.parametrize( + "cli_bump_changelog_args", + [ + ("cz", "bump", "--changelog", "--yes"), + ( + "cz", + "bump", + "--changelog", + "--changelog-to-stdout", + "--annotated-tag", + "--check-consistency", + "--yes", + ), + ], + ids=lambda cmd_tuple: " ".join(cmd_tuple), +) +def test_bump_changelog_command_commits_untracked_changelog_and_version_files( + tmp_commitizen_project, + mocker, + cli_bump_changelog_args: tuple[str, ...], + version_filepath: str, + version_regex: str, + version_file_content: str, +): + """Ensure that changelog always gets committed, no matter what version file or cli options get passed. + + Steps: + - Append the version file's name and regex commitizen configuration lines to `pyproject.toml`. + - Append to or create the version file. + - Add a commit of type fix to be eligible for a version bump. + - Call commitizen main cli and assert that the `CHANGELOG.md` and the version file were committed. + """ + + with tmp_commitizen_project.join("pyproject.toml").open( + mode="a", + encoding="utf-8", + ) as commitizen_config: + commitizen_config.write(f"version_files = [\n'{version_regex}'\n]") + + with tmp_commitizen_project.join(version_filepath).open( + mode="a+", encoding="utf-8" + ) as version_file: + version_file.write(version_file_content) + + create_file_and_commit("fix: some test commit") + + mocker.patch.object(sys, "argv", cli_bump_changelog_args) + cli.main() + + commit_file_names = git.get_filenames_in_commit() + assert "CHANGELOG.md" in commit_file_names + assert version_filepath in commit_file_names + + +@pytest.mark.parametrize( + "testargs", + [ + ["cz", "bump", "--local-version", "1.2.3"], + ["cz", "bump", "--prerelease", "rc", "1.2.3"], + ["cz", "bump", "--devrelease", "0", "1.2.3"], + ["cz", "bump", "--devrelease", "1", "1.2.3"], + ["cz", "bump", "--increment", "PATCH", "1.2.3"], + ["cz", "bump", "--build-metadata=a.b.c", "1.2.3"], + ["cz", "bump", "--local-version", "--build-metadata=a.b.c"], + ], +) +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_invalid_manual_args_raises_exception(mocker, testargs): + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NotAllowed): + cli.main() + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.parametrize( + "manual_version", + [ + "noversion", + "1.2..3", + ], +) +def test_bump_invalid_manual_version_raises_exception(mocker, manual_version): + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes", manual_version] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(InvalidManualVersion) as excinfo: + cli.main() + + expected_error_message = ( + f"[INVALID_MANUAL_VERSION]\nInvalid manual version: '{manual_version}'" + ) + assert expected_error_message in str(excinfo.value) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.parametrize( + "manual_version", + [ + "0.0.1", + "0.1.0rc2", + "0.1.0.dev2", + "0.1.0+1.0.0", + "0.1.0rc2.dev2+1.0.0", + "0.1.1", + "0.2.0", + "1.0.0", + ], +) +def test_bump_manual_version(mocker, manual_version): + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes", manual_version] + mocker.patch.object(sys, "argv", testargs) + cli.main() + tag_exists = git.tag_exist(manual_version) + assert tag_exists is True + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_manual_version_disallows_major_version_zero(mocker): + create_file_and_commit("feat: new file") + + manual_version = "0.2.0" + testargs = ["cz", "bump", "--yes", "--major-version-zero", manual_version] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NotAllowed) as excinfo: + cli.main() + + expected_error_message = ( + "--major-version-zero cannot be combined with MANUAL_VERSION" + ) + assert expected_error_message in str(excinfo.value) + + +@pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file")) +def test_bump_with_pre_bump_hooks( + commit_msg, mocker: MockFixture, tmp_commitizen_project +): + pre_bump_hook = "scripts/pre_bump_hook.sh" + post_bump_hook = "scripts/post_bump_hook.sh" + + tmp_commitizen_cfg_file = tmp_commitizen_project.join("pyproject.toml") + tmp_commitizen_cfg_file.write( + f"{tmp_commitizen_cfg_file.read()}\n" + f'pre_bump_hooks = ["{pre_bump_hook}"]\n' + f'post_bump_hooks = ["{post_bump_hook}"]\n' + ) + + run_mock = mocker.Mock() + mocker.patch.object(hooks, "run", run_mock) + + create_file_and_commit(commit_msg) + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is True + + run_mock.assert_has_calls( + [ + call( + [pre_bump_hook], + _env_prefix="CZ_PRE_", + is_initial=True, + current_version="0.1.0", + current_tag_version="0.1.0", + new_version="0.2.0", + new_tag_version="0.2.0", + message="bump: version 0.1.0 โ†’ 0.2.0", + increment="MINOR", + changelog_file_name=None, + ), + call( + [post_bump_hook], + _env_prefix="CZ_POST_", + was_initial=True, + previous_version="0.1.0", + previous_tag_version="0.1.0", + current_version="0.2.0", + current_tag_version="0.2.0", + message="bump: version 0.1.0 โ†’ 0.2.0", + increment="MINOR", + changelog_file_name=None, + ), + ] + ) + + +def test_bump_with_hooks_and_increment(mocker: MockFixture, tmp_commitizen_project): + pre_bump_hook = "scripts/pre_bump_hook.sh" + post_bump_hook = "scripts/post_bump_hook.sh" + + tmp_commitizen_cfg_file = tmp_commitizen_project.join("pyproject.toml") + tmp_commitizen_cfg_file.write( + f"{tmp_commitizen_cfg_file.read()}\n" + f'pre_bump_hooks = ["{pre_bump_hook}"]\n' + f'post_bump_hooks = ["{post_bump_hook}"]\n' + ) + + run_mock = mocker.Mock() + mocker.patch.object(hooks, "run", run_mock) + + create_file_and_commit("test: some test") + testargs = ["cz", "bump", "--yes", "--increment", "MINOR"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is True + + +@pytest.mark.usefixtures("tmp_git_project") +def test_bump_use_version_provider(mocker: MockFixture): + mock = mocker.MagicMock(name="provider") + mock.get_version.return_value = "0.0.0" + get_provider = mocker.patch( + "commitizen.commands.bump.get_provider", return_value=mock + ) + + create_file_and_commit("fix: fake commit") + testargs = ["cz", "bump", "--yes", "--changelog"] + mocker.patch.object(sys, "argv", testargs) + + cli.main() + + assert git.tag_exist("0.0.1") + get_provider.assert_called_once() + mock.get_version.assert_called_once() + mock.set_version.assert_called_once_with("0.0.1") + + +def test_bump_command_prelease_scheme_via_cli( + tmp_commitizen_project_initial, mocker: MockFixture +): + tmp_commitizen_project = tmp_commitizen_project_initial() + tmp_version_file = tmp_commitizen_project.join("__version__.py") + tmp_commitizen_cfg_file = tmp_commitizen_project.join("pyproject.toml") + + testargs = [ + "cz", + "bump", + "--prerelease", + "alpha", + "--yes", + "--version-scheme", + "semver", + ] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0-a0") + assert tag_exists is True + + for version_file in [tmp_version_file, tmp_commitizen_cfg_file]: + with open(version_file) as f: + assert "0.2.0-a0" in f.read() + + # PRERELEASE BUMP CREATES VERSION WITHOUT PRERELEASE + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is True + + for version_file in [tmp_version_file, tmp_commitizen_cfg_file]: + with open(version_file) as f: + assert "0.2.0" in f.read() + + +def test_bump_command_prelease_scheme_via_config( + tmp_commitizen_project_initial, mocker: MockFixture +): + tmp_commitizen_project = tmp_commitizen_project_initial( + config_extra='version_scheme = "semver"\n', + ) + tmp_version_file = tmp_commitizen_project.join("__version__.py") + tmp_commitizen_cfg_file = tmp_commitizen_project.join("pyproject.toml") + + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0-a0") + assert tag_exists is True + + for version_file in [tmp_version_file, tmp_commitizen_cfg_file]: + with open(version_file) as f: + assert "0.2.0-a0" in f.read() + + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0-a1") + assert tag_exists is True + + for version_file in [tmp_version_file, tmp_commitizen_cfg_file]: + with open(version_file) as f: + assert "0.2.0-a1" in f.read() + + # PRERELEASE BUMP CREATES VERSION WITHOUT PRERELEASE + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is True + + for version_file in [tmp_version_file, tmp_commitizen_cfg_file]: + with open(version_file) as f: + assert "0.2.0" in f.read() + + +def test_bump_command_prelease_scheme_check_old_tags( + tmp_commitizen_project_initial, mocker: MockFixture +): + tmp_commitizen_project = tmp_commitizen_project_initial( + config_extra=('tag_format = "v$version"\nversion_scheme = "semver"\n'), + ) + tmp_version_file = tmp_commitizen_project.join("__version__.py") + tmp_commitizen_cfg_file = tmp_commitizen_project.join("pyproject.toml") + + testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("v0.2.0-a0") + assert tag_exists is True + + for version_file in [tmp_version_file, tmp_commitizen_cfg_file]: + with open(version_file) as f: + assert "0.2.0-a0" in f.read() + + testargs = ["cz", "bump", "--prerelease", "alpha"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("v0.2.0-a1") + assert tag_exists is True + + for version_file in [tmp_version_file, tmp_commitizen_cfg_file]: + with open(version_file) as f: + assert "0.2.0-a1" in f.read() + + # PRERELEASE BUMP CREATES VERSION WITHOUT PRERELEASE + testargs = ["cz", "bump"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist("v0.2.0") + assert tag_exists is True + + for version_file in [tmp_version_file, tmp_commitizen_cfg_file]: + with open(version_file) as f: + assert "0.2.0" in f.read() + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.usefixtures("use_cz_semver") +@pytest.mark.parametrize( + "message, expected_tag", + [ + ("minor: add users", "0.2.0"), + ("patch: bug affecting users", "0.1.1"), + ("major: bug affecting users", "1.0.0"), + ], +) +def test_bump_with_plugin(mocker: MockFixture, message: str, expected_tag: str): + create_file_and_commit(message) + + testargs = ["cz", "--name", "cz_semver", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist(expected_tag) + assert tag_exists is True + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.usefixtures("use_cz_semver") +@pytest.mark.parametrize( + "message, expected_tag", + [ + ("minor: add users", "0.2.0"), + ("patch: bug affecting users", "0.1.1"), + ("major: bug affecting users", "0.2.0"), + ], +) +def test_bump_with_major_version_zero_with_plugin( + mocker: MockFixture, message: str, expected_tag: str +): + create_file_and_commit(message) + + testargs = ["cz", "--name", "cz_semver", "bump", "--yes", "--major-version-zero"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + tag_exists = git.tag_exist(expected_tag) + assert tag_exists is True + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_command_version_type_deprecation(mocker: MockFixture): + create_file_and_commit("feat: check deprecation on --version-type") + + testargs = [ + "cz", + "bump", + "--prerelease", + "alpha", + "--yes", + "--version-type", + "semver", + ] + mocker.patch.object(sys, "argv", testargs) + with pytest.warns(DeprecationWarning): + cli.main() + + assert git.tag_exist("0.2.0-a0") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_command_version_scheme_priority_over_version_type(mocker: MockFixture): + create_file_and_commit("feat: check deprecation on --version-type") + + testargs = [ + "cz", + "bump", + "--prerelease", + "alpha", + "--yes", + "--version-type", + "semver", + "--version-scheme", + "pep440", + ] + mocker.patch.object(sys, "argv", testargs) + with pytest.warns(DeprecationWarning): + cli.main() + + assert git.tag_exist("0.2.0a0") + + +@pytest.mark.parametrize( + "arg, cfg, expected", + ( + pytest.param("", "", "default", id="default"), + pytest.param("", "changelog.cfg", "from config", id="from-config"), + pytest.param( + "--template=changelog.cmd", "changelog.cfg", "from cmd", id="from-command" + ), + ), +) +def test_bump_template_option_precedance( + mocker: MockFixture, + tmp_commitizen_project: Path, + any_changelog_format: ChangelogFormat, + arg: str, + cfg: str, + expected: str, +): + project_root = Path(tmp_commitizen_project) + cfg_template = project_root / "changelog.cfg" + cmd_template = project_root / "changelog.cmd" + default_template = project_root / any_changelog_format.template + changelog = project_root / any_changelog_format.default_changelog_file + + cfg_template.write_text("from config") + cmd_template.write_text("from cmd") + default_template.write_text("default") + + create_file_and_commit("feat: new file") + + if cfg: + pyproject = project_root / "pyproject.toml" + pyproject.write_text( + dedent( + f"""\ + [tool.commitizen] + version = "0.1.0" + template = "{cfg}" + """ + ) + ) + + testargs = ["cz", "bump", "--yes", "--changelog"] + if arg: + testargs.append(arg) + mocker.patch.object(sys, "argv", testargs + ["0.1.1"]) + cli.main() + + out = changelog.read_text() + assert out == expected + + +def test_bump_template_extras_precedance( + mocker: MockFixture, + tmp_commitizen_project: Path, + any_changelog_format: ChangelogFormat, + mock_plugin: BaseCommitizen, +): + project_root = Path(tmp_commitizen_project) + changelog_tpl = project_root / any_changelog_format.template + changelog_tpl.write_text("{{first}} - {{second}} - {{third}}") + + mock_plugin.template_extras = dict( + first="from-plugin", second="from-plugin", third="from-plugin" + ) + + pyproject = project_root / "pyproject.toml" + pyproject.write_text( + dedent( + """\ + [tool.commitizen] + version = "0.1.0" + [tool.commitizen.extras] + first = "from-config" + second = "from-config" + """ + ) + ) + + create_file_and_commit("feat: new file") + + testargs = [ + "cz", + "bump", + "--yes", + "--changelog", + "--extra", + "first=from-command", + "0.1.1", + ] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + changelog = project_root / any_changelog_format.default_changelog_file + assert changelog.read_text() == "from-command - from-config - from-plugin" + + +def test_bump_template_extra_quotes( + mocker: MockFixture, + tmp_commitizen_project: Path, + any_changelog_format: ChangelogFormat, +): + project_root = Path(tmp_commitizen_project) + changelog_tpl = project_root / any_changelog_format.template + changelog_tpl.write_text("{{first}} - {{second}} - {{third}}") + + create_file_and_commit("feat: new file") + + testargs = [ + "cz", + "bump", + "--changelog", + "--yes", + "-e", + "first=no-quote", + "-e", + "second='single quotes'", + "-e", + 'third="double quotes"', + "0.1.1", + ] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + changelog = project_root / any_changelog_format.default_changelog_file + assert changelog.read_text() == "no-quote - single quotes - double quotes" + + +def test_bump_changelog_contains_increment_only(mocker, tmp_commitizen_project, capsys): + """Issue 1024""" + # Initialize commitizen up to v1.0.0 + project_root = Path(tmp_commitizen_project) + tmp_commitizen_cfg_file = project_root / "pyproject.toml" + tmp_commitizen_cfg_file.write_text( + '[tool.commitizen]\nversion="1.0.0"\nupdate_changelog_on_bump = true\n' + ) + tmp_changelog_file = project_root / "CHANGELOG.md" + tmp_changelog_file.write_text("## v1.0.0") + create_file_and_commit("feat(user): new file") + create_tag("v1.0.0") + + # Add a commit and bump to v2.0.0 + create_file_and_commit("feat(user)!: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + _ = capsys.readouterr() + + # Add a commit and create the incremental changelog to v3.0.0 + # it should only include v3 changes + create_file_and_commit("feat(next)!: next version") + testargs = ["cz", "bump", "--yes", "--files-only", "--changelog-to-stdout"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(ExpectedExit): + cli.main() + out, _ = capsys.readouterr() + + assert "3.0.0" in out + assert "2.0.0" not in out + + +@skip_below_py_3_13 +def test_bump_command_shows_description_when_use_help_option( + mocker: MockFixture, capsys, file_regression +): + testargs = ["cz", "bump", "--help"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(SystemExit): + cli.main() + + out, _ = capsys.readouterr() + file_regression.check(out, extension=".txt") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_get_next(mocker: MockFixture, capsys): + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes", "--get-next"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(GetNextExit): + cli.main() + + out, _ = capsys.readouterr() + assert "0.2.0" in out + + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is False + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_get_next_update_changelog_on_bump( + mocker: MockFixture, capsys, config_path +): + create_file_and_commit("feat: new file") + with open(config_path, "a", encoding="utf-8") as fp: + fp.write("update_changelog_on_bump = true\n") + + testargs = ["cz", "bump", "--yes", "--get-next"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(GetNextExit): + cli.main() + + out, _ = capsys.readouterr() + assert "0.2.0" in out + + tag_exists = git.tag_exist("0.2.0") + assert tag_exists is False + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_get_next__changelog_is_not_allowed(mocker: MockFixture): + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes", "--get-next", "--changelog"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NotAllowed): + cli.main() + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_get_next__changelog_to_stdout_is_not_allowed(mocker: MockFixture): + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes", "--get-next", "--changelog-to-stdout"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NotAllowed): + cli.main() + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_get_next__manual_version_is_not_allowed(mocker: MockFixture): + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes", "--get-next", "0.2.1"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NotAllowed): + cli.main() + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_get_next__no_eligible_commits_raises(mocker: MockFixture): + create_file_and_commit("chore: new commit") + + testargs = ["cz", "bump", "--yes", "--get-next"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NoneIncrementExit): + cli.main() + + +def test_bump_allow_no_commit_with_no_commit(mocker, tmp_commitizen_project, capsys): + with tmp_commitizen_project.as_cwd(): + # Create the first commit and bump to 1.0.0 + create_file_and_commit("feat(user)!: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + # Verify NoCommitsFoundError should be raised + # when there's no new commit and "--allow-no-commit" is not set + with pytest.raises(NoCommitsFoundError): + testargs = ["cz", "bump"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + # bump to 1.0.1 with new commit when "--allow-no-commit" is set + testargs = ["cz", "bump", "--allow-no-commit"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + out, _ = capsys.readouterr() + assert "bump: version 1.0.0 โ†’ 1.0.1" in out + + +def test_bump_allow_no_commit_with_no_eligible_commit( + mocker, tmp_commitizen_project, capsys +): + with tmp_commitizen_project.as_cwd(): + # Create the first commit and bump to 1.0.0 + create_file_and_commit("feat(user)!: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + # Create a commit that is ineligible to bump + create_file_and_commit("docs(bump): add description for allow no commit") + + # Verify NoneIncrementExit should be raised + # when there's no eligible bumping commit and "--allow-no-commit" is not set + with pytest.raises(NoneIncrementExit): + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + # bump to 1.0.1 with ineligible commit when "--allow-no-commit" is set + testargs = ["cz", "bump", "--allow-no-commit"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + out, _ = capsys.readouterr() + assert "bump: version 1.0.0 โ†’ 1.0.1" in out + + +def test_bump_allow_no_commit_with_increment(mocker, tmp_commitizen_project, capsys): + with tmp_commitizen_project.as_cwd(): + # # Create the first commit and bump to 1.0.0 + create_file_and_commit("feat(user)!: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + # Verify NoCommitsFoundError should be raised + # when there's no new commit and "--allow-no-commit" is not set + with pytest.raises(NoCommitsFoundError): + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + # bump to 1.1.0 with no new commit when "--allow-no-commit" is set + # and increment is specified + testargs = ["cz", "bump", "--yes", "--allow-no-commit", "--increment", "MINOR"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + out, _ = capsys.readouterr() + assert "bump: version 1.0.0 โ†’ 1.1.0" in out + + +def test_bump_allow_no_commit_with_manual_version( + mocker, tmp_commitizen_project, capsys +): + with tmp_commitizen_project.as_cwd(): + # # Create the first commit and bump to 1.0.0 + create_file_and_commit("feat(user)!: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + # Verify NoCommitsFoundError should be raised + # when there's no new commit and "--allow-no-commit" is not set + with pytest.raises(NoCommitsFoundError): + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + # bump to 1.1.0 with no new commit when "--allow-no-commit" is set + # and increment is specified + testargs = ["cz", "bump", "--yes", "--allow-no-commit", "2.0.0"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + out, _ = capsys.readouterr() + assert "bump: version 1.0.0 โ†’ 2.0.0" in out + + +def test_bump_detect_legacy_tags_from_scm( + tmp_commitizen_project: py.path.local, mocker: MockFixture +): + project_root = Path(tmp_commitizen_project) + tmp_commitizen_cfg_file = project_root / "pyproject.toml" + tmp_commitizen_cfg_file.write_text( + "\n".join( + [ + "[tool.commitizen]", + 'version_provider = "scm"', + 'tag_format = "v$version"', + "legacy_tag_formats = [", + ' "legacy-${version}"', + "]", + ] + ), + ) + create_file_and_commit("feat: new file") + create_tag("legacy-0.4.2") + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--increment", "patch", "--changelog"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + assert git.tag_exist("v0.4.3") diff --git a/tests/commands/test_bump_command/test_bump_command_shows_description_when_use_help_option.txt b/tests/commands/test_bump_command/test_bump_command_shows_description_when_use_help_option.txt new file mode 100644 index 0000000..5d44388 --- /dev/null +++ b/tests/commands/test_bump_command/test_bump_command_shows_description_when_use_help_option.txt @@ -0,0 +1,81 @@ +usage: cz bump [-h] [--dry-run] [--files-only] [--local-version] [--changelog] + [--no-verify] [--yes] [--tag-format TAG_FORMAT] + [--bump-message BUMP_MESSAGE] [--prerelease {alpha,beta,rc}] + [--devrelease DEVRELEASE] [--increment {MAJOR,MINOR,PATCH}] + [--increment-mode {linear,exact}] [--check-consistency] + [--annotated-tag] + [--annotated-tag-message ANNOTATED_TAG_MESSAGE] [--gpg-sign] + [--changelog-to-stdout] [--git-output-to-stderr] [--retry] + [--major-version-zero] [--template TEMPLATE] [--extra EXTRA] + [--file-name FILE_NAME] [--prerelease-offset PRERELEASE_OFFSET] + [--version-scheme {pep440,semver,semver2}] + [--version-type {pep440,semver,semver2}] + [--build-metadata BUILD_METADATA] [--get-next] + [--allow-no-commit] + [MANUAL_VERSION] + +bump semantic version based on the git log + +positional arguments: + MANUAL_VERSION bump to the given version (e.g: 1.5.3) + +options: + -h, --help show this help message and exit + --dry-run show output to stdout, no commit, no modified files + --files-only bump version in the files from the config + --local-version bump only the local version portion + --changelog, -ch generate the changelog for the newest version + --no-verify this option bypasses the pre-commit and commit-msg + hooks + --yes accept automatically questions done + --tag-format TAG_FORMAT + the format used to tag the commit and read it, use it + in existing projects, wrap around simple quotes + --bump-message BUMP_MESSAGE + template used to create the release commit, useful + when working with CI + --prerelease, -pr {alpha,beta,rc} + choose type of prerelease + --devrelease, -d DEVRELEASE + specify non-negative integer for dev. release + --increment {MAJOR,MINOR,PATCH} + manually specify the desired increment + --increment-mode {linear,exact} + set the method by which the new version is chosen. + 'linear' (default) guesses the next version based on + typical linear version progression, such that bumping + of a pre-release with lower precedence than the + current pre-release phase maintains the current phase + of higher precedence. 'exact' applies the changes that + have been specified (or determined from the commit + log) without interpretation, such that the increment + and pre-release are always honored + --check-consistency, -cc + check consistency among versions defined in commitizen + configuration and version_files + --annotated-tag, -at create annotated tag instead of lightweight one + --annotated-tag-message, -atm ANNOTATED_TAG_MESSAGE + create annotated tag message + --gpg-sign, -s sign tag instead of lightweight one + --changelog-to-stdout + Output changelog to the stdout + --git-output-to-stderr + Redirect git output to stderr + --retry retry commit if it fails the 1st time + --major-version-zero keep major version at zero, even for breaking changes + --template, -t TEMPLATE + changelog template file name (relative to the current + working directory) + --extra, -e EXTRA a changelog extra variable (in the form 'key=value') + --file-name FILE_NAME + file name of changelog (default: 'CHANGELOG.md') + --prerelease-offset PRERELEASE_OFFSET + start pre-releases with this offset + --version-scheme {pep440,semver,semver2} + choose version scheme + --version-type {pep440,semver,semver2} + Deprecated, use --version-scheme + --build-metadata BUILD_METADATA + Add additional build-metadata to the version-number + --get-next Determine the next version and write to stdout + --allow-no-commit bump version without eligible commits diff --git a/tests/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py new file mode 100644 index 0000000..f794b8d --- /dev/null +++ b/tests/commands/test_changelog_command.py @@ -0,0 +1,1927 @@ +import itertools +import sys +from datetime import datetime +from pathlib import Path +from textwrap import dedent + +import pytest +from dateutil import relativedelta +from jinja2 import FileSystemLoader +from pytest_mock import MockFixture +from pytest_regressions.file_regression import FileRegressionFixture + +from commitizen import __file__ as commitizen_init +from commitizen import cli, git +from commitizen.changelog_formats import ChangelogFormat +from commitizen.commands.changelog import Changelog +from commitizen.config.base_config import BaseConfig +from commitizen.cz.base import BaseCommitizen +from commitizen.exceptions import ( + DryRunExit, + InvalidCommandArgumentError, + NoCommitsFoundError, + NoRevisionError, + NotAGitProjectError, + NotAllowed, +) +from tests.utils import ( + create_branch, + create_file_and_commit, + create_tag, + get_current_branch, + merge_branch, + skip_below_py_3_13, + switch_branch, + wait_for_tag, +) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_from_version_zero_point_two( + mocker: MockFixture, capsys, file_regression +): + create_file_and_commit("feat: new file") + create_file_and_commit("refactor: not in changelog") + + # create tag + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + capsys.readouterr() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: after 0.2") + + testargs = ["cz", "changelog", "--start-rev", "0.2.0", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(DryRunExit): + cli.main() + + out, _ = capsys.readouterr() + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_with_different_cz(mocker: MockFixture, capsys, file_regression): + create_file_and_commit("JRA-34 #comment corrected indent issue") + create_file_and_commit("JRA-35 #time 1w 2d 4h 30m Total work logged") + + testargs = ["cz", "-n", "cz_jira", "changelog", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(DryRunExit): + cli.main() + out, _ = capsys.readouterr() + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_from_start( + mocker: MockFixture, capsys, changelog_format: ChangelogFormat, file_regression +): + create_file_and_commit("feat: new file") + create_file_and_commit("refactor: is in changelog") + create_file_and_commit("Merge into master") + changelog_file = f"CHANGELOG.{changelog_format.extension}" + template = f"CHANGELOG.{changelog_format.extension}.j2" + + testargs = [ + "cz", + "changelog", + "--file-name", + changelog_file, + "--template", + template, + ] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_file, encoding="utf-8") as f: + out = f.read() + file_regression.check(out, extension=changelog_format.ext) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_replacing_unreleased_using_incremental( + mocker: MockFixture, capsys, changelog_format: ChangelogFormat, file_regression +): + create_file_and_commit("feat: add new output") + create_file_and_commit("fix: output glitch") + create_file_and_commit("Merge into master") + changelog_file = f"CHANGELOG.{changelog_format.extension}" + template = f"CHANGELOG.{changelog_format.extension}.j2" + + testargs = [ + "cz", + "changelog", + "--file-name", + changelog_file, + "--template", + template, + ] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + testargs = [ + "cz", + "bump", + "--yes", + "--file-name", + changelog_file, + "--template", + template, + ] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + create_file_and_commit("fix: mama gotta work") + create_file_and_commit("feat: add more stuff") + create_file_and_commit("Merge into master") + + testargs = [ + "cz", + "changelog", + "--incremental", + "--file-name", + changelog_file, + "--template", + template, + ] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_file, encoding="utf-8") as f: + out = f.read().replace( + datetime.strftime(datetime.now(), "%Y-%m-%d"), "2022-08-14" + ) + + file_regression.check(out, extension=changelog_format.ext) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_is_persisted_using_incremental( + mocker: MockFixture, capsys, changelog_path, file_regression +): + create_file_and_commit("feat: add new output") + create_file_and_commit("fix: output glitch") + create_file_and_commit("Merge into master") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + testargs = ["cz", "changelog"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path, "a", encoding="utf-8") as f: + f.write("\nnote: this should be persisted using increment\n") + + create_file_and_commit("fix: mama gotta work") + create_file_and_commit("feat: add more stuff") + create_file_and_commit("Merge into master") + + testargs = ["cz", "changelog", "--incremental"] + + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path, encoding="utf-8") as f: + out = f.read().replace( + datetime.strftime(datetime.now(), "%Y-%m-%d"), "2022-08-14" + ) + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_incremental_angular_sample( + mocker: MockFixture, capsys, changelog_path, file_regression +): + with open(changelog_path, "w", encoding="utf-8") as f: + f.write( + "# [10.0.0-rc.3](https://github.com/angular/angular/compare/10.0.0-rc.2...10.0.0-rc.3) (2020-04-22)\n" + "\n" + "### Bug Fixes" + "\n" + "* **common:** format day-periods that cross midnight ([#36611](https://github.com/angular/angular/issues/36611)) ([c6e5fc4](https://github.com/angular/angular/commit/c6e5fc4)), closes [#36566](https://github.com/angular/angular/issues/36566)\n" + ) + create_file_and_commit("irrelevant commit") + git.tag("10.0.0-rc.3") + + create_file_and_commit("feat: add new output") + create_file_and_commit("fix: output glitch") + create_file_and_commit("fix: mama gotta work") + create_file_and_commit("feat: add more stuff") + create_file_and_commit("Merge into master") + + testargs = ["cz", "changelog", "--incremental"] + + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path, encoding="utf-8") as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +KEEP_A_CHANGELOG = """# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). +""" + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_incremental_keep_a_changelog_sample( + mocker: MockFixture, capsys, changelog_path, file_regression +): + with open(changelog_path, "w", encoding="utf-8") as f: + f.write(KEEP_A_CHANGELOG) + create_file_and_commit("irrelevant commit") + git.tag("1.0.0") + + create_file_and_commit("feat: add new output") + create_file_and_commit("fix: output glitch") + create_file_and_commit("fix: mama gotta work") + create_file_and_commit("feat: add more stuff") + create_file_and_commit("Merge into master") + + testargs = ["cz", "changelog", "--incremental"] + + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path, encoding="utf-8") as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.parametrize("dry_run", [True, False]) +def test_changelog_hook(mocker: MockFixture, config: BaseConfig, dry_run: bool): + changelog_hook_mock = mocker.Mock() + changelog_hook_mock.return_value = "cool changelog hook" + + create_file_and_commit("feat: new file") + create_file_and_commit("refactor: is in changelog") + create_file_and_commit("Merge into master") + + config.settings["change_type_order"] = ["Refactor", "Feat"] # type: ignore[typeddict-unknown-key] + changelog = Changelog( + config, {"unreleased_version": None, "incremental": True, "dry_run": dry_run} + ) + mocker.patch.object(changelog.cz, "changelog_hook", changelog_hook_mock) + try: + changelog() + except DryRunExit: + pass + + full_changelog = ( + "## Unreleased\n\n### Refactor\n\n- is in changelog\n\n### Feat\n\n- new file\n" + ) + partial_changelog = full_changelog + if dry_run: + partial_changelog = "" + + changelog_hook_mock.assert_called_with(full_changelog, partial_changelog) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_hook_customize(mocker: MockFixture, config_customize): + changelog_hook_mock = mocker.Mock() + changelog_hook_mock.return_value = "cool changelog hook" + + create_file_and_commit("feat: new file") + create_file_and_commit("refactor: is in changelog") + create_file_and_commit("Merge into master") + + changelog = Changelog( + config_customize, + {"unreleased_version": None, "incremental": True, "dry_run": False}, + ) + mocker.patch.object(changelog.cz, "changelog_hook", changelog_hook_mock) + changelog() + full_changelog = ( + "## Unreleased\n\n### Refactor\n\n- is in changelog\n\n### Feat\n\n- new file\n" + ) + + changelog_hook_mock.assert_called_with(full_changelog, full_changelog) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_release_hook(mocker: MockFixture, config): + def changelog_release_hook(release: dict, tag: git.GitTag) -> dict: + return release + + for i in range(3): + create_file_and_commit("feat: new file") + create_file_and_commit("refactor: is in changelog") + create_file_and_commit("Merge into master") + git.tag(f"0.{i + 1}.0") + + # changelog = Changelog(config, {}) + changelog = Changelog( + config, {"unreleased_version": None, "incremental": True, "dry_run": False} + ) + mocker.patch.object(changelog.cz, "changelog_release_hook", changelog_release_hook) + spy = mocker.spy(changelog.cz, "changelog_release_hook") + changelog() + + assert spy.call_count == 3 + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_with_non_linear_merges_commit_order( + mocker: MockFixture, config_customize +): + """Test that commits merged non-linearly are correctly ordered in the changelog + + A typical scenario is having two branches from main like so: + * feat: I will be merged first - (2023-03-01 11:35:51 +0100) | (branchB) + | * feat: I will be merged second - (2023-03-01 11:35:22 +0100) | (branchA) + |/ + * feat: initial commit - (2023-03-01 11:34:54 +0100) | (HEAD -> main) + + And merging them, for example in the reverse order they were created on would give the following: + * Merge branch 'branchA' - (2023-03-01 11:42:59 +0100) | (HEAD -> main) + |\ + | * feat: I will be merged second - (2023-03-01 11:35:22 +0100) | (branchA) + * | feat: I will be merged first - (2023-03-01 11:35:51 +0100) | (branchB) + |/ + * feat: initial commit - (2023-03-01 11:34:54 +0100) | + + In this case we want the changelog to reflect the topological order of commits, + i.e. the order in which they were merged into the main branch + + So the above example should result in the following: + ## Unreleased + + ### Feat + - I will be merged second + - I will be merged first + - initial commit + """ + changelog_hook_mock = mocker.Mock() + changelog_hook_mock.return_value = "cool changelog hook" + + create_file_and_commit("feat: initial commit") + + main_branch = get_current_branch() + + create_branch("branchA") + create_branch("branchB") + + switch_branch("branchA") + create_file_and_commit("feat: I will be merged second") + + switch_branch("branchB") + create_file_and_commit("feat: I will be merged first") + + # Note we merge branches opposite order than author_date + switch_branch(main_branch) + merge_branch("branchB") + merge_branch("branchA") + + changelog = Changelog( + config_customize, + {"unreleased_version": None, "incremental": True, "dry_run": False}, + ) + mocker.patch.object(changelog.cz, "changelog_hook", changelog_hook_mock) + changelog() + full_changelog = "\ +## Unreleased\n\n\ +\ +### Feat\n\n\ +- I will be merged second\n\ +- I will be merged first\n\ +- initial commit\n" + + changelog_hook_mock.assert_called_with(full_changelog, full_changelog) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_multiple_incremental_do_not_add_new_lines( + mocker: MockFixture, capsys, changelog_path, file_regression +): + """Test for bug https://github.com/commitizen-tools/commitizen/issues/192""" + create_file_and_commit("feat: add new output") + + testargs = ["cz", "changelog", "--incremental"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + create_file_and_commit("fix: output glitch") + + testargs = ["cz", "changelog", "--incremental"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + create_file_and_commit("fix: no more explosions") + + testargs = ["cz", "changelog", "--incremental"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + create_file_and_commit("feat: add more stuff") + + testargs = ["cz", "changelog", "--incremental"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path, encoding="utf-8") as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_incremental_newline_separates_new_content_from_old( + mocker: MockFixture, changelog_path +): + """Test for https://github.com/commitizen-tools/commitizen/issues/509""" + with open(changelog_path, "w", encoding="utf-8") as f: + f.write("Pre-existing content that should be kept\n") + + create_file_and_commit("feat: add more cat videos") + + testargs = ["cz", "changelog", "--incremental"] + + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path, encoding="utf-8") as f: + out = f.read() + + assert ( + out + == "Pre-existing content that should be kept\n\n## Unreleased\n\n### Feat\n\n- add more cat videos\n" + ) + + +def test_changelog_without_revision(mocker: MockFixture, tmp_commitizen_project): + changelog_file = tmp_commitizen_project.join("CHANGELOG.md") + changelog_file.write( + """ + # Unreleased + + ## v1.0.0 + """ + ) + + # create_file_and_commit("feat: new file") + testargs = ["cz", "changelog", "--incremental"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NoRevisionError): + cli.main() + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_incremental_with_revision(mocker): + """combining incremental with a revision doesn't make sense""" + testargs = ["cz", "changelog", "--incremental", "0.2.0"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NotAllowed): + cli.main() + + +def test_changelog_with_different_tag_name_and_changelog_content( + mocker: MockFixture, tmp_commitizen_project +): + changelog_file = tmp_commitizen_project.join("CHANGELOG.md") + changelog_file.write( + """ + # Unreleased + + ## v1.0.0 + """ + ) + create_file_and_commit("feat: new file") + git.tag("2.0.0") + + # create_file_and_commit("feat: new file") + testargs = ["cz", "changelog", "--incremental"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NoRevisionError): + cli.main() + + +def test_changelog_in_non_git_project(tmpdir, config, mocker: MockFixture): + testargs = ["cz", "changelog", "--incremental"] + mocker.patch.object(sys, "argv", testargs) + + with tmpdir.as_cwd(): + with pytest.raises(NotAGitProjectError): + cli.main() + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_breaking_change_content_v1_beta(mocker: MockFixture, capsys, file_regression): + commit_message = ( + "feat(users): email pattern corrected\n\n" + "BREAKING CHANGE: migrate by renaming user to users\n\n" + "footer content" + ) + create_file_and_commit(commit_message) + testargs = ["cz", "changelog", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(DryRunExit): + cli.main() + out, _ = capsys.readouterr() + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_breaking_change_content_v1(mocker: MockFixture, capsys, file_regression): + commit_message = ( + "feat(users): email pattern corrected\n\n" + "body content\n\n" + "BREAKING CHANGE: migrate by renaming user to users" + ) + create_file_and_commit(commit_message) + testargs = ["cz", "changelog", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(DryRunExit): + cli.main() + out, _ = capsys.readouterr() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_breaking_change_content_v1_multiline( + mocker: MockFixture, capsys, file_regression +): + commit_message = ( + "feat(users): email pattern corrected\n\n" + "body content\n\n" + "BREAKING CHANGE: migrate by renaming user to users.\n" + "and then connect the thingy with the other thingy\n\n" + "footer content" + ) + create_file_and_commit(commit_message) + testargs = ["cz", "changelog", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(DryRunExit): + cli.main() + out, _ = capsys.readouterr() + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_breaking_change_content_v1_with_exclamation_mark( + mocker: MockFixture, capsys, file_regression +): + commit_message = "chore!: drop support for py36" + create_file_and_commit(commit_message) + testargs = ["cz", "changelog", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(DryRunExit): + cli.main() + out, _ = capsys.readouterr() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_breaking_change_content_v1_with_exclamation_mark_feat( + mocker: MockFixture, capsys, file_regression +): + commit_message = "feat(pipeline)!: some text with breaking change" + create_file_and_commit(commit_message) + testargs = ["cz", "changelog", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(DryRunExit): + cli.main() + out, _ = capsys.readouterr() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_config_flag_increment( + mocker: MockFixture, changelog_path, config_path, file_regression +): + with open(config_path, "a", encoding="utf-8") as f: + f.write("changelog_incremental = true\n") + with open(changelog_path, "a", encoding="utf-8") as f: + f.write("\nnote: this should be persisted using increment\n") + + create_file_and_commit("feat: add new output") + + testargs = ["cz", "changelog"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path, encoding="utf-8") as f: + out = f.read() + + assert "this should be persisted using increment" in out + file_regression.check(out, extension=".md") + + +@pytest.mark.parametrize("test_input", ["rc", "alpha", "beta"]) +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_config_flag_merge_prerelease( + mocker: MockFixture, changelog_path, config_path, file_regression, test_input +): + with open(config_path, "a") as f: + f.write("changelog_merge_prerelease = true\n") + + create_file_and_commit("irrelevant commit") + mocker.patch("commitizen.git.GitTag.date", "1970-01-01") + git.tag("1.0.0") + + create_file_and_commit("feat: add new output") + create_file_and_commit("fix: output glitch") + + testargs = ["cz", "bump", "--prerelease", test_input, "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + create_file_and_commit("fix: mama gotta work") + create_file_and_commit("feat: add more stuff") + create_file_and_commit("Merge into master") + + testargs = ["cz", "changelog"] + + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path) as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_config_start_rev_option( + mocker: MockFixture, capsys, config_path, file_regression +): + # create commit and tag + create_file_and_commit("feat: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + capsys.readouterr() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: after 0.2") + + with open(config_path, "a", encoding="utf-8") as f: + f.write('changelog_start_rev = "0.2.0"\n') + + testargs = ["cz", "changelog", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(DryRunExit): + cli.main() + + out, _ = capsys.readouterr() + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_incremental_keep_a_changelog_sample_with_annotated_tag( + mocker: MockFixture, capsys, changelog_path, file_regression +): + """Fix #378""" + with open(changelog_path, "w", encoding="utf-8") as f: + f.write(KEEP_A_CHANGELOG) + create_file_and_commit("irrelevant commit") + git.tag("1.0.0", annotated=True) + + create_file_and_commit("feat: add new output") + create_file_and_commit("fix: output glitch") + create_file_and_commit("fix: mama gotta work") + create_file_and_commit("feat: add more stuff") + create_file_and_commit("Merge into master") + + testargs = ["cz", "changelog", "--incremental"] + + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path, encoding="utf-8") as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.parametrize("test_input", ["rc", "alpha", "beta"]) +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2021-06-11") +def test_changelog_incremental_with_release_candidate_version( + mocker: MockFixture, changelog_path, file_regression, test_input +): + """Fix #357""" + with open(changelog_path, "w", encoding="utf-8") as f: + f.write(KEEP_A_CHANGELOG) + create_file_and_commit("irrelevant commit") + git.tag("1.0.0", annotated=True) + + create_file_and_commit("feat: add new output") + create_file_and_commit("fix: output glitch") + + testargs = ["cz", "bump", "--changelog", "--prerelease", test_input, "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + create_file_and_commit("fix: mama gotta work") + create_file_and_commit("feat: add more stuff") + create_file_and_commit("Merge into master") + + testargs = ["cz", "changelog", "--incremental"] + + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path, encoding="utf-8") as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.parametrize( + "from_pre,to_pre", itertools.product(["alpha", "beta", "rc"], repeat=2) +) +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2021-06-11") +def test_changelog_incremental_with_prerelease_version_to_prerelease_version( + mocker: MockFixture, changelog_path, file_regression, from_pre, to_pre +): + with open(changelog_path, "w") as f: + f.write(KEEP_A_CHANGELOG) + create_file_and_commit("irrelevant commit") + git.tag("1.0.0", annotated=True) + + create_file_and_commit("feat: add new output") + create_file_and_commit("fix: output glitch") + + testargs = ["cz", "bump", "--changelog", "--prerelease", from_pre, "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + testargs = ["cz", "bump", "--changelog", "--prerelease", to_pre, "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path) as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.parametrize("test_input", ["rc", "alpha", "beta"]) +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_release_candidate_version_with_merge_prerelease( + mocker: MockFixture, changelog_path, file_regression, test_input +): + """Fix #357""" + with open(changelog_path, "w") as f: + f.write(KEEP_A_CHANGELOG) + create_file_and_commit("irrelevant commit") + mocker.patch("commitizen.git.GitTag.date", "1970-01-01") + git.tag("1.0.0") + + create_file_and_commit("feat: add new output") + create_file_and_commit("fix: output glitch") + + testargs = ["cz", "bump", "--prerelease", test_input, "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + create_file_and_commit("fix: mama gotta work") + create_file_and_commit("feat: add more stuff") + create_file_and_commit("Merge into master") + + testargs = ["cz", "changelog", "--merge-prerelease"] + + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path) as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.parametrize("test_input", ["rc", "alpha", "beta"]) +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2023-04-16") +def test_changelog_incremental_with_merge_prerelease( + mocker: MockFixture, changelog_path, file_regression, test_input +): + """Fix #357""" + with open(changelog_path, "w") as f: + f.write(KEEP_A_CHANGELOG) + create_file_and_commit("irrelevant commit") + mocker.patch("commitizen.git.GitTag.date", "1970-01-01") + git.tag("1.0.0") + + create_file_and_commit("feat: add new output") + + testargs = ["cz", "bump", "--prerelease", test_input, "--yes", "--changelog"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + create_file_and_commit("fix: output glitch") + + testargs = ["cz", "bump", "--prerelease", test_input, "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + create_file_and_commit("fix: mama gotta work") + create_file_and_commit("feat: add more stuff") + create_file_and_commit("Merge into master") + + testargs = ["cz", "changelog", "--merge-prerelease", "--incremental"] + + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path) as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_with_filename_as_empty_string( + mocker: MockFixture, changelog_path, config_path +): + with open(config_path, "a", encoding="utf-8") as f: + f.write("changelog_file = true\n") + + create_file_and_commit("feat: add new output") + + testargs = ["cz", "changelog"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(NotAllowed): + cli.main() + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_first_version_from_arg( + mocker: MockFixture, config_path, changelog_path, file_regression +): + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + + with open(config_path, "a", encoding="utf-8") as f: + f.write('tag_format = "$version"\n') + + # create commit and tag + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + testargs = ["cz", "changelog", "0.2.0"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + with open(changelog_path, encoding="utf-8") as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_latest_version_from_arg( + mocker: MockFixture, config_path, changelog_path, file_regression +): + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + + with open(config_path, "a", encoding="utf-8") as f: + f.write('tag_format = "$version"\n') + + # create commit and tag + create_file_and_commit("feat: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + wait_for_tag() + + testargs = ["cz", "changelog", "0.3.0"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path) as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +@pytest.mark.parametrize( + "rev_range,tag", + ( + pytest.param("0.8.0", "0.2.0", id="single-not-found"), + pytest.param("0.1.0..0.3.0", "0.3.0", id="lower-bound-not-found"), + pytest.param("0.1.0..0.3.0", "0.1.0", id="upper-bound-not-found"), + pytest.param("0.3.0..0.4.0", "0.2.0", id="none-found"), + ), +) +def test_changelog_from_rev_range_not_found( + mocker: MockFixture, config_path, rev_range: str, tag: str +): + """Provides an invalid revision ID to changelog command""" + with open(config_path, "a", encoding="utf-8") as f: + f.write('tag_format = "$version"\n') + + # create commit and tag + create_file_and_commit("feat: new file") + create_tag(tag) + create_file_and_commit("feat: new file") + create_tag("1.0.0") + + testargs = ["cz", "changelog", rev_range] # it shouldn't exist + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(NoCommitsFoundError) as excinfo: + cli.main() + + assert "Could not find a valid revision" in str(excinfo) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_multiple_matching_tags( + mocker: MockFixture, config_path, changelog_path +): + with open(config_path, "a", encoding="utf-8") as f: + f.write('tag_format = "new-$version"\nlegacy_tag_formats = ["legacy-$version"]') + + create_file_and_commit("feat: new file") + create_tag("legacy-1.0.0") + create_file_and_commit("feat: new file") + create_tag("legacy-2.0.0") + create_tag("new-2.0.0") + + testargs = ["cz", "changelog", "1.0.0..2.0.0"] # it shouldn't exist + mocker.patch.object(sys, "argv", testargs) + with pytest.warns() as warnings: + cli.main() + + assert len(warnings) == 1 + warning = warnings[0] + assert "Multiple tags found for version 2.0.0" in str(warning.message) + + with open(changelog_path) as f: + out = f.read() + + # Ensure only one tag is rendered + assert out.count("2.0.0") == 1 + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_range_default_tag_format( + mocker, config_path, changelog_path +): + """Checks that rev_range is calculated with the default (None) tag format""" + # create commit and tag + create_file_and_commit("feat: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + testargs = ["cz", "changelog", "0.3.0"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + with open(changelog_path) as f: + out = f.read() + + assert "new file" not in out + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_version_range_including_first_tag( + mocker: MockFixture, config_path, changelog_path, file_regression +): + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + + with open(config_path, "a", encoding="utf-8") as f: + f.write('tag_format = "$version"\n') + + # create commit and tag + create_file_and_commit("feat: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + testargs = ["cz", "changelog", "0.2.0..0.3.0"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + with open(changelog_path, encoding="utf-8") as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_version_range_from_arg( + mocker: MockFixture, config_path, changelog_path, file_regression +): + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + + with open(config_path, "a", encoding="utf-8") as f: + f.write('tag_format = "$version"\n') + + # create commit and tag + create_file_and_commit("feat: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + create_file_and_commit("feat: getting ready for this") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + testargs = ["cz", "changelog", "0.3.0..0.4.0"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + with open(changelog_path) as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_version_range_with_legacy_tags( + mocker: MockFixture, config_path, changelog_path, file_regression +): + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + + changelog = Path(changelog_path) + Path(config_path).write_text( + "\n".join( + [ + "[tool.commitizen]", + 'version_provider = "scm"', + 'tag_format = "v$version"', + "legacy_tag_formats = [", + ' "legacy-${version}",', + ' "old-${version}",', + "]", + ] + ), + ) + + create_file_and_commit("feat: new file") + create_tag("old-0.2.0") + create_file_and_commit("feat: new file") + create_tag("legacy-0.3.0") + create_file_and_commit("feat: new file") + create_tag("legacy-0.4.0") + + testargs = ["cz", "changelog", "0.2.0..0.4.0"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + file_regression.check(changelog.read_text(), extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_version_with_big_range_from_arg( + mocker: MockFixture, config_path, changelog_path, file_regression +): + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + + with open(config_path, "a", encoding="utf-8") as f: + f.write('tag_format = "$version"\n') + + # create commit and tag + create_file_and_commit("feat: new file") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + + testargs = ["cz", "bump", "--yes"] # 0.3.0 + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + create_file_and_commit("feat: getting ready for this") + + testargs = ["cz", "bump", "--yes"] # 0.4.0 + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + create_file_and_commit("fix: small error") + + testargs = ["cz", "bump", "--yes"] # 0.4.1 + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + create_file_and_commit("feat: new shinny feature") + + testargs = ["cz", "bump", "--yes"] # 0.5.0 + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + create_file_and_commit("feat: amazing different shinny feature") + # dirty hack to avoid same time between tags + + testargs = ["cz", "bump", "--yes"] # 0.6.0 + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + testargs = ["cz", "changelog", "0.3.0..0.5.0"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + with open(changelog_path) as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_from_rev_latest_version_dry_run( + mocker: MockFixture, capsys, config_path, changelog_path, file_regression +): + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + + with open(config_path, "a") as f: + f.write('tag_format = "$version"\n') + + # create commit and tag + create_file_and_commit("feat: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + capsys.readouterr() + wait_for_tag() + + testargs = ["cz", "changelog", "0.3.0", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(DryRunExit): + cli.main() + + out, _ = capsys.readouterr() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_invalid_subject_is_skipped(mocker: MockFixture, capsys): + """Fix #510""" + non_conformant_commit_title = ( + "Merge pull request #487 from manang/master\n\n" + "feat: skip merge messages that start with Pull request\n" + ) + create_file_and_commit(non_conformant_commit_title) + create_file_and_commit("feat: a new world") + testargs = ["cz", "changelog", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(DryRunExit): + cli.main() + out, _ = capsys.readouterr() + + assert out == ("## Unreleased\n\n### Feat\n\n- a new world\n\n") + + +@pytest.mark.freeze_time("2022-02-13") +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_with_customized_change_type_order( + mocker, config_path, changelog_path, file_regression +): + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + + with open(config_path, "a") as f: + f.write('tag_format = "$version"\n') + f.write( + 'change_type_order = ["BREAKING CHANGE", "Perf", "Fix", "Feat", "Refactor"]\n' + ) + + # create commit and tag + create_file_and_commit("feat: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + create_file_and_commit("fix: fix bug") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + create_file_and_commit("feat: getting ready for this") + create_file_and_commit("perf: perf improvement") + + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + testargs = ["cz", "changelog", "0.3.0..0.4.0"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + with open(changelog_path) as f: + out = f.read() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_empty_commit_list(mocker): + create_file_and_commit("feat: a new world") + + # test changelog properly handles when no commits are found for the revision + mocker.patch("commitizen.git.get_commits", return_value=[]) + testargs = ["cz", "changelog"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(NoCommitsFoundError): + cli.main() + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") +def test_changelog_prerelease_rev_with_use_scheme_semver( + mocker: MockFixture, capsys, config_path, changelog_path, file_regression +): + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + + with open(config_path, "a") as f: + f.write('tag_format = "$version"\nversion_scheme = "semver"') + + # create commit and tag + create_file_and_commit("feat: new file") + testargs = ["cz", "bump", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + + create_file_and_commit("feat: after 0.2.0") + create_file_and_commit("feat: another feature") + + testargs = ["cz", "bump", "--yes", "--prerelease", "alpha"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + capsys.readouterr() + wait_for_tag() + + tag_exists = git.tag_exist("0.3.0-a0") + assert tag_exists is True + + testargs = ["cz", "changelog", "0.3.0-a0", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(DryRunExit): + cli.main() + + out, _ = capsys.readouterr() + + file_regression.check(out, extension=".md") + + testargs = ["cz", "bump", "--yes", "--prerelease", "alpha"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + capsys.readouterr() + wait_for_tag() + + tag_exists = git.tag_exist("0.3.0-a1") + assert tag_exists is True + + testargs = ["cz", "changelog", "0.3.0-a1", "--dry-run"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(DryRunExit): + cli.main() + + out, _ = capsys.readouterr() + + file_regression.check(out, extension=".second-prerelease.md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_uses_version_tags_for_header(mocker: MockFixture, config): + """Tests that changelog headers always use version tags even if there are non-version tags + + This tests a scenario fixed in this commit: + The first header was using a non-version tag and outputting "## 0-not-a-version" instead of "## 1.0.0 + """ + create_file_and_commit("feat: commit in 1.0.0") + create_tag("0-not-a-version") + create_tag("1.0.0") + create_tag("also-not-a-version") + + write_patch = mocker.patch("commitizen.commands.changelog.out.write") + + changelog = Changelog( + config, {"dry_run": True, "incremental": True, "unreleased_version": None} + ) + + with pytest.raises(DryRunExit): + changelog() + + changelog_output = write_patch.call_args[0][0] + + assert changelog_output.startswith("## 1.0.0") + assert "0-no-a-version" not in changelog_output + assert "also-not-a-version" not in changelog_output + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_from_current_version_tag_with_nonversion_tag( + mocker: MockFixture, config +): + """Tests that changelog generation for a single version works even if + there is a non-version tag in the list of tags + + This tests a scenario which is fixed in this commit: + You have a commit in between two versions (1.0.0..2.0.0) which is tagged with a non-version tag (not-a-version). + In this case commitizen should disregard the non-version tag when determining the rev-range & generating the changelog. + """ + create_file_and_commit( + "feat: initial commit", + committer_date=( + datetime.now() - relativedelta.relativedelta(seconds=3) + ).isoformat(), + ) + create_tag("1.0.0") + + create_file_and_commit( + "feat: commit 1", + committer_date=( + datetime.now() - relativedelta.relativedelta(seconds=2) + ).isoformat(), + ) + create_tag("1-not-a-version") + + create_file_and_commit( + "feat: commit 2", + committer_date=( + datetime.now() - relativedelta.relativedelta(seconds=1) + ).isoformat(), + ) + + create_file_and_commit("bump: version 1.0.0 โ†’ 2.0.0") + create_tag("2.0.0") + + mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + write_patch = mocker.patch("commitizen.commands.changelog.out.write") + + changelog = Changelog( + config, + { + "dry_run": True, + "incremental": False, + "unreleased_version": None, + "rev_range": "2.0.0", + }, + ) + + with pytest.raises(DryRunExit): + changelog() + + full_changelog = "\ +## 2.0.0 (2022-02-13)\n\n\ +### Feat\n\n\ +- commit 2\n\ +- commit 1\n" + + write_patch.assert_called_with(full_changelog) + + +@pytest.mark.parametrize( + "arg,cfg,expected", + ( + pytest.param("", "", "default", id="default"), + pytest.param("", "changelog.cfg", "from config", id="from-config"), + pytest.param( + "--template=changelog.cmd", "changelog.cfg", "from cmd", id="from-command" + ), + ), +) +def test_changelog_template_option_precedance( + mocker: MockFixture, + tmp_commitizen_project: Path, + any_changelog_format: ChangelogFormat, + arg: str, + cfg: str, + expected: str, +): + project_root = Path(tmp_commitizen_project) + cfg_template = project_root / "changelog.cfg" + cmd_template = project_root / "changelog.cmd" + default_template = project_root / any_changelog_format.template + changelog = project_root / any_changelog_format.default_changelog_file + + cfg_template.write_text("from config") + cmd_template.write_text("from cmd") + default_template.write_text("default") + + create_file_and_commit("feat: new file") + + if cfg: + pyproject = project_root / "pyproject.toml" + pyproject.write_text( + dedent( + f"""\ + [tool.commitizen] + version = "0.1.0" + template = "{cfg}" + """ + ) + ) + + testargs = ["cz", "changelog"] + if arg: + testargs.append(arg) + mocker.patch.object(sys, "argv", testargs) + cli.main() + + out = changelog.read_text() + assert out == expected + + +def test_changelog_template_extras_precedance( + mocker: MockFixture, + tmp_commitizen_project: Path, + mock_plugin: BaseCommitizen, + any_changelog_format: ChangelogFormat, +): + project_root = Path(tmp_commitizen_project) + changelog_tpl = project_root / any_changelog_format.template + changelog_tpl.write_text("{{first}} - {{second}} - {{third}}") + + mock_plugin.template_extras = dict( + first="from-plugin", second="from-plugin", third="from-plugin" + ) + + pyproject = project_root / "pyproject.toml" + pyproject.write_text( + dedent( + """\ + [tool.commitizen] + version = "0.1.0" + [tool.commitizen.extras] + first = "from-config" + second = "from-config" + """ + ) + ) + + create_file_and_commit("feat: new file") + + testargs = ["cz", "changelog", "--extra", "first=from-command"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + changelog = project_root / any_changelog_format.default_changelog_file + assert changelog.read_text() == "from-command - from-config - from-plugin" + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2021-06-11") +def test_changelog_only_tag_matching_tag_format_included_prefix( + mocker: MockFixture, + changelog_path: Path, + config_path: Path, +): + with open(config_path, "a", encoding="utf-8") as f: + f.write('\ntag_format = "custom${version}"\n') + create_file_and_commit("feat: new file") + git.tag("v0.2.0") + create_file_and_commit("feat: another new file") + git.tag("0.2.0") + git.tag("random0.2.0") + testargs = ["cz", "bump", "--changelog", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + create_file_and_commit("feat: another new file") + cli.main() + with open(changelog_path) as f: + out = f.read() + assert out.startswith("## custom0.3.0 (2021-06-11)") + assert "## v0.2.0 (2021-06-11)" not in out + assert "## 0.2.0 (2021-06-11)" not in out + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_only_tag_matching_tag_format_included_prefix_sep( + mocker: MockFixture, + changelog_path: Path, + config_path: Path, +): + with open(config_path, "a", encoding="utf-8") as f: + f.write('\ntag_format = "custom-${version}"\n') + create_file_and_commit("feat: new file") + git.tag("v0.2.0") + create_file_and_commit("feat: another new file") + git.tag("0.2.0") + git.tag("random0.2.0") + wait_for_tag() + testargs = ["cz", "bump", "--changelog", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + with open(changelog_path) as f: + out = f.read() + create_file_and_commit("feat: new version another new file") + create_file_and_commit("feat: new version some new file") + testargs = ["cz", "bump", "--changelog"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + with open(changelog_path) as f: + out = f.read() + assert out.startswith("## custom-0.3.0") + assert "## v0.2.0" not in out + assert "## 0.2.0" not in out + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2021-06-11") +def test_changelog_only_tag_matching_tag_format_included_suffix( + mocker: MockFixture, + changelog_path: Path, + config_path: Path, +): + with open(config_path, "a", encoding="utf-8") as f: + f.write('\ntag_format = "${version}custom"\n') + create_file_and_commit("feat: new file") + git.tag("v0.2.0") + create_file_and_commit("feat: another new file") + git.tag("0.2.0") + git.tag("random0.2.0") + testargs = ["cz", "bump", "--changelog", "--yes"] + mocker.patch.object(sys, "argv", testargs) + # bump to 0.2.0custom + cli.main() + wait_for_tag() + create_file_and_commit("feat: another new file") + # bump to 0.3.0custom + cli.main() + wait_for_tag() + with open(changelog_path) as f: + out = f.read() + assert out.startswith("## 0.3.0custom (2021-06-11)") + assert "## v0.2.0 (2021-06-11)" not in out + assert "## 0.2.0 (2021-06-11)" not in out + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2021-06-11") +def test_changelog_only_tag_matching_tag_format_included_suffix_sep( + mocker: MockFixture, + changelog_path: Path, + config_path: Path, +): + with open(config_path, "a", encoding="utf-8") as f: + f.write('\ntag_format = "${version}-custom"\n') + create_file_and_commit("feat: new file") + git.tag("v0.2.0") + create_file_and_commit("feat: another new file") + git.tag("0.2.0") + git.tag("random0.2.0") + testargs = ["cz", "bump", "--changelog", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + wait_for_tag() + create_file_and_commit("feat: another new file") + cli.main() + wait_for_tag() + with open(changelog_path) as f: + out = f.read() + assert out.startswith("## 0.3.0-custom (2021-06-11)") + assert "## v0.2.0 (2021-06-11)" not in out + assert "## 0.2.0 (2021-06-11)" not in out + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_legacy_tags( + mocker: MockFixture, + changelog_path: Path, + config_path: Path, +): + with open(config_path, "a", encoding="utf-8") as f: + f.writelines( + [ + 'tag_format = "v${version}"\n', + "legacy_tag_formats = [\n", + ' "older-${version}",\n', + ' "oldest-${version}",\n', + "]\n", + ] + ) + create_file_and_commit("feat: new file") + git.tag("oldest-0.1.0") + create_file_and_commit("feat: new file") + git.tag("older-0.2.0") + create_file_and_commit("feat: another new file") + git.tag("v0.3.0") + create_file_and_commit("feat: another new file") + git.tag("not-0.3.1") + testargs = ["cz", "bump", "--changelog", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + out = open(changelog_path).read() + assert "## v0.3.0" in out + assert "## older-0.2.0" in out + assert "## oldest-0.1.0" in out + assert "## v0.3.0" in out + assert "## not-0.3.1" not in out + + +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2024-11-18") +def test_changelog_incremental_change_tag_format( + mocker: MockFixture, + changelog_path: Path, + config_path: Path, + file_regression: FileRegressionFixture, +): + mocker.patch("commitizen.git.GitTag.date", "2024-11-18") + config = Path(config_path) + base_config = config.read_text() + config.write_text( + "\n".join( + ( + base_config, + 'tag_format = "older-${version}"', + ) + ) + ) + create_file_and_commit("feat: new file") + git.tag("older-0.1.0") + create_file_and_commit("feat: new file") + git.tag("older-0.2.0") + mocker.patch.object(sys, "argv", ["cz", "changelog"]) + cli.main() + + config.write_text( + "\n".join( + ( + base_config, + 'tag_format = "v${version}"', + 'legacy_tag_formats = ["older-${version}"]', + ) + ) + ) + create_file_and_commit("feat: another new file") + git.tag("v0.3.0") + mocker.patch.object(sys, "argv", ["cz", "changelog", "--incremental"]) + cli.main() + out = open(changelog_path).read() + assert "## v0.3.0" in out + assert "## older-0.2.0" in out + assert "## older-0.1.0" in out + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_ignored_tags( + mocker: MockFixture, + changelog_path: Path, + config_path: Path, + capsys: pytest.CaptureFixture, +): + with open(config_path, "a", encoding="utf-8") as f: + f.writelines( + [ + 'tag_format = "v${version}"\n', + "ignored_tag_formats = [\n", + ' "ignored",\n', + ' "ignore-${version}",\n', + "]\n", + ] + ) + create_file_and_commit("feat: new file") + git.tag("ignore-0.1.0") + create_file_and_commit("feat: new file") + git.tag("ignored") + create_file_and_commit("feat: another new file") + git.tag("v0.3.0") + create_file_and_commit("feat: another new file") + git.tag("not-ignored") + testargs = ["cz", "bump", "--changelog", "--yes"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + out = open(changelog_path).read() + _, err = capsys.readouterr() + assert "## ignore-0.1.0" not in out + assert "InvalidVersion ignore-0.1.0" not in err + assert "## ignored" not in out + assert "InvalidVersion ignored" not in err + assert "## not-ignored" not in out + assert "InvalidVersion not-ignored" in err + assert "## v0.3.0" in out + assert "InvalidVersion v0.3.0" not in err + + +def test_changelog_template_extra_quotes( + mocker: MockFixture, + tmp_commitizen_project: Path, + any_changelog_format: ChangelogFormat, +): + project_root = Path(tmp_commitizen_project) + changelog_tpl = project_root / any_changelog_format.template + changelog_tpl.write_text("{{first}} - {{second}} - {{third}}") + + create_file_and_commit("feat: new file") + + testargs = [ + "cz", + "changelog", + "-e", + "first=no-quote", + "-e", + "second='single quotes'", + "-e", + 'third="double quotes"', + ] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + changelog = project_root / any_changelog_format.default_changelog_file + assert changelog.read_text() == "no-quote - single quotes - double quotes" + + +@pytest.mark.parametrize( + "extra, expected", + ( + pytest.param("key=value=", "value=", id="2-equals"), + pytest.param("key==value", "=value", id="2-consecutives-equals"), + pytest.param("key==value==", "=value==", id="multiple-equals"), + ), +) +def test_changelog_template_extra_weird_but_valid( + mocker: MockFixture, + tmp_commitizen_project: Path, + any_changelog_format: ChangelogFormat, + extra: str, + expected, +): + project_root = Path(tmp_commitizen_project) + changelog_tpl = project_root / any_changelog_format.template + changelog_tpl.write_text("{{key}}") + + create_file_and_commit("feat: new file") + + testargs = ["cz", "changelog", "-e", extra] + mocker.patch.object(sys, "argv", testargs) + cli.main() + + changelog = project_root / any_changelog_format.default_changelog_file + assert changelog.read_text() == expected + + +@pytest.mark.parametrize("extra", ("no-equal", "", "=no-key")) +def test_changelog_template_extra_bad_format( + mocker: MockFixture, + tmp_commitizen_project: Path, + any_changelog_format: ChangelogFormat, + extra: str, +): + project_root = Path(tmp_commitizen_project) + changelog_tpl = project_root / any_changelog_format.template + changelog_tpl.write_text("") + + create_file_and_commit("feat: new file") + + testargs = ["cz", "changelog", "-e", extra] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(InvalidCommandArgumentError): + cli.main() + + +def test_export_changelog_template_from_default( + mocker: MockFixture, + tmp_commitizen_project: Path, + any_changelog_format: ChangelogFormat, +): + project_root = Path(tmp_commitizen_project) + target = project_root / "changelog.jinja" + src = Path(commitizen_init).parent / "templates" / any_changelog_format.template + + args = ["cz", "changelog", "--export-template", str(target)] + + mocker.patch.object(sys, "argv", args) + cli.main() + + assert target.exists() + assert target.read_text() == src.read_text() + + +def test_export_changelog_template_from_plugin( + mocker: MockFixture, + tmp_commitizen_project: Path, + mock_plugin: BaseCommitizen, + changelog_format: ChangelogFormat, + tmp_path: Path, +): + project_root = Path(tmp_commitizen_project) + target = project_root / "changelog.jinja" + src = tmp_path / changelog_format.template + tpl = "I am a custom template" + src.write_text(tpl) + mock_plugin.template_loader = FileSystemLoader(tmp_path) + + args = ["cz", "changelog", "--export-template", str(target)] + + mocker.patch.object(sys, "argv", args) + cli.main() + + assert target.exists() + assert target.read_text() == tpl + + +@skip_below_py_3_13 +def test_changelog_command_shows_description_when_use_help_option( + mocker: MockFixture, capsys, file_regression +): + testargs = ["cz", "changelog", "--help"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(SystemExit): + cli.main() + + out, _ = capsys.readouterr() + file_regression.check(out, extension=".txt") diff --git a/tests/commands/test_changelog_command/test_breaking_change_content_v1.md b/tests/commands/test_changelog_command/test_breaking_change_content_v1.md new file mode 100644 index 0000000..c480973 --- /dev/null +++ b/tests/commands/test_changelog_command/test_breaking_change_content_v1.md @@ -0,0 +1,10 @@ +## Unreleased + +### BREAKING CHANGE + +- migrate by renaming user to users + +### Feat + +- **users**: email pattern corrected + diff --git a/tests/commands/test_changelog_command/test_breaking_change_content_v1_beta.md b/tests/commands/test_changelog_command/test_breaking_change_content_v1_beta.md new file mode 100644 index 0000000..c480973 --- /dev/null +++ b/tests/commands/test_changelog_command/test_breaking_change_content_v1_beta.md @@ -0,0 +1,10 @@ +## Unreleased + +### BREAKING CHANGE + +- migrate by renaming user to users + +### Feat + +- **users**: email pattern corrected + diff --git a/tests/commands/test_changelog_command/test_breaking_change_content_v1_multiline.md b/tests/commands/test_changelog_command/test_breaking_change_content_v1_multiline.md new file mode 100644 index 0000000..7becef3 --- /dev/null +++ b/tests/commands/test_changelog_command/test_breaking_change_content_v1_multiline.md @@ -0,0 +1,11 @@ +## Unreleased + +### BREAKING CHANGE + +- migrate by renaming user to users. +and then connect the thingy with the other thingy + +### Feat + +- **users**: email pattern corrected + diff --git a/tests/commands/test_changelog_command/test_breaking_change_content_v1_with_exclamation_mark.md b/tests/commands/test_changelog_command/test_breaking_change_content_v1_with_exclamation_mark.md new file mode 100644 index 0000000..d12d780 --- /dev/null +++ b/tests/commands/test_changelog_command/test_breaking_change_content_v1_with_exclamation_mark.md @@ -0,0 +1,5 @@ +## Unreleased + + +- drop support for py36 + diff --git a/tests/commands/test_changelog_command/test_breaking_change_content_v1_with_exclamation_mark_feat.md b/tests/commands/test_changelog_command/test_breaking_change_content_v1_with_exclamation_mark_feat.md new file mode 100644 index 0000000..84c2368 --- /dev/null +++ b/tests/commands/test_changelog_command/test_breaking_change_content_v1_with_exclamation_mark_feat.md @@ -0,0 +1,6 @@ +## Unreleased + +### Feat + +- **pipeline**: some text with breaking change + diff --git a/tests/commands/test_changelog_command/test_changelog_command_shows_description_when_use_help_option.txt b/tests/commands/test_changelog_command/test_changelog_command_shows_description_when_use_help_option.txt new file mode 100644 index 0000000..461eb2e --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_command_shows_description_when_use_help_option.txt @@ -0,0 +1,39 @@ +usage: cz changelog [-h] [--dry-run] [--file-name FILE_NAME] + [--unreleased-version UNRELEASED_VERSION] [--incremental] + [--start-rev START_REV] [--merge-prerelease] + [--version-scheme {pep440,semver,semver2}] + [--export-template EXPORT_TEMPLATE] [--template TEMPLATE] + [--extra EXTRA] + [rev_range] + +generate changelog (note that it will overwrite existing file) + +positional arguments: + rev_range generates changelog for the given version (e.g: 1.5.3) + or version range (e.g: 1.5.3..1.7.9) + +options: + -h, --help show this help message and exit + --dry-run show changelog to stdout + --file-name FILE_NAME + file name of changelog (default: 'CHANGELOG.md') + --unreleased-version UNRELEASED_VERSION + set the value for the new version (use the tag value), + instead of using unreleased + --incremental generates changelog from last created version, useful + if the changelog has been manually modified + --start-rev START_REV + start rev of the changelog. If not set, it will + generate changelog from the start + --merge-prerelease collect all changes from prereleases into next non- + prerelease. If not set, it will include prereleases in + the changelog + --version-scheme {pep440,semver,semver2} + choose version scheme + --export-template EXPORT_TEMPLATE + Export the changelog template into this file instead + of rendering it + --template, -t TEMPLATE + changelog template file name (relative to the current + working directory) + --extra, -e EXTRA a changelog extra variable (in the form 'key=value') diff --git a/tests/commands/test_changelog_command/test_changelog_config_flag_increment.md b/tests/commands/test_changelog_command/test_changelog_config_flag_increment.md new file mode 100644 index 0000000..5bdace3 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_config_flag_increment.md @@ -0,0 +1,8 @@ + +note: this should be persisted using increment + +## Unreleased + +### Feat + +- add new output diff --git a/tests/commands/test_changelog_command/test_changelog_config_flag_merge_prerelease_alpha_.md b/tests/commands/test_changelog_command/test_changelog_config_flag_merge_prerelease_alpha_.md new file mode 100644 index 0000000..dca7824 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_config_flag_merge_prerelease_alpha_.md @@ -0,0 +1,13 @@ +## Unreleased + +### Feat + +- add more stuff +- add new output + +### Fix + +- mama gotta work +- output glitch + +## 1.0.0 (1970-01-01) diff --git a/tests/commands/test_changelog_command/test_changelog_config_flag_merge_prerelease_beta_.md b/tests/commands/test_changelog_command/test_changelog_config_flag_merge_prerelease_beta_.md new file mode 100644 index 0000000..dca7824 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_config_flag_merge_prerelease_beta_.md @@ -0,0 +1,13 @@ +## Unreleased + +### Feat + +- add more stuff +- add new output + +### Fix + +- mama gotta work +- output glitch + +## 1.0.0 (1970-01-01) diff --git a/tests/commands/test_changelog_command/test_changelog_config_flag_merge_prerelease_rc_.md b/tests/commands/test_changelog_command/test_changelog_config_flag_merge_prerelease_rc_.md new file mode 100644 index 0000000..dca7824 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_config_flag_merge_prerelease_rc_.md @@ -0,0 +1,13 @@ +## Unreleased + +### Feat + +- add more stuff +- add new output + +### Fix + +- mama gotta work +- output glitch + +## 1.0.0 (1970-01-01) diff --git a/tests/commands/test_changelog_command/test_changelog_config_start_rev_option.md b/tests/commands/test_changelog_command/test_changelog_config_start_rev_option.md new file mode 100644 index 0000000..d1baef2 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_config_start_rev_option.md @@ -0,0 +1,7 @@ +## Unreleased + +### Feat + +- after 0.2 +- after 0.2.0 + diff --git a/tests/commands/test_changelog_command/test_changelog_from_rev_first_version_from_arg.md b/tests/commands/test_changelog_command/test_changelog_from_rev_first_version_from_arg.md new file mode 100644 index 0000000..3519498 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_rev_first_version_from_arg.md @@ -0,0 +1,5 @@ +## 0.2.0 (2022-02-13) + +### Feat + +- new file diff --git a/tests/commands/test_changelog_command/test_changelog_from_rev_latest_version_dry_run.md b/tests/commands/test_changelog_command/test_changelog_from_rev_latest_version_dry_run.md new file mode 100644 index 0000000..e6531e6 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_rev_latest_version_dry_run.md @@ -0,0 +1,7 @@ +## 0.3.0 (2022-02-13) + +### Feat + +- another feature +- after 0.2.0 + diff --git a/tests/commands/test_changelog_command/test_changelog_from_rev_latest_version_from_arg.md b/tests/commands/test_changelog_command/test_changelog_from_rev_latest_version_from_arg.md new file mode 100644 index 0000000..9188064 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_rev_latest_version_from_arg.md @@ -0,0 +1,6 @@ +## 0.3.0 (2022-02-13) + +### Feat + +- another feature +- after 0.2.0 diff --git a/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_from_arg.md b/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_from_arg.md new file mode 100644 index 0000000..0c483c7 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_from_arg.md @@ -0,0 +1,12 @@ +## 0.4.0 (2022-02-13) + +### Feat + +- getting ready for this + +## 0.3.0 (2022-02-13) + +### Feat + +- another feature +- after 0.2.0 diff --git a/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_including_first_tag.md b/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_including_first_tag.md new file mode 100644 index 0000000..44bffb3 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_including_first_tag.md @@ -0,0 +1,12 @@ +## 0.3.0 (2022-02-13) + +### Feat + +- another feature +- after 0.2.0 + +## 0.2.0 (2022-02-13) + +### Feat + +- new file diff --git a/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_with_legacy_tags.md b/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_with_legacy_tags.md new file mode 100644 index 0000000..5d37333 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_rev_version_range_with_legacy_tags.md @@ -0,0 +1,17 @@ +## legacy-0.4.0 (2022-02-13) + +### Feat + +- new file + +## legacy-0.3.0 (2022-02-13) + +### Feat + +- new file + +## old-0.2.0 (2022-02-13) + +### Feat + +- new file diff --git a/tests/commands/test_changelog_command/test_changelog_from_rev_version_with_big_range_from_arg.md b/tests/commands/test_changelog_command/test_changelog_from_rev_version_with_big_range_from_arg.md new file mode 100644 index 0000000..376424d --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_rev_version_with_big_range_from_arg.md @@ -0,0 +1,24 @@ +## 0.5.0 (2022-02-13) + +### Feat + +- new shinny feature + +## 0.4.1 (2022-02-13) + +### Fix + +- small error + +## 0.4.0 (2022-02-13) + +### Feat + +- getting ready for this + +## 0.3.0 (2022-02-13) + +### Feat + +- another feature +- after 0.2.0 diff --git a/tests/commands/test_changelog_command/test_changelog_from_start_asciidoc_.adoc b/tests/commands/test_changelog_command/test_changelog_from_start_asciidoc_.adoc new file mode 100644 index 0000000..842e120 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_start_asciidoc_.adoc @@ -0,0 +1,9 @@ +== Unreleased + +=== Feat + +* new file + +=== Refactor + +* is in changelog diff --git a/tests/commands/test_changelog_command/test_changelog_from_start_markdown_.md b/tests/commands/test_changelog_command/test_changelog_from_start_markdown_.md new file mode 100644 index 0000000..bdc7866 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_start_markdown_.md @@ -0,0 +1,9 @@ +## Unreleased + +### Feat + +- new file + +### Refactor + +- is in changelog diff --git a/tests/commands/test_changelog_command/test_changelog_from_start_restructuredtext_.rst b/tests/commands/test_changelog_command/test_changelog_from_start_restructuredtext_.rst new file mode 100644 index 0000000..555f5bc --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_start_restructuredtext_.rst @@ -0,0 +1,12 @@ +Unreleased +========== + +Feat +---- + +- new file + +Refactor +-------- + +- is in changelog diff --git a/tests/commands/test_changelog_command/test_changelog_from_start_textile_.textile b/tests/commands/test_changelog_command/test_changelog_from_start_textile_.textile new file mode 100644 index 0000000..e71fb99 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_start_textile_.textile @@ -0,0 +1,9 @@ +h2. Unreleased + +h3. Feat + +- new file + +h3. Refactor + +- is in changelog diff --git a/tests/commands/test_changelog_command/test_changelog_from_version_zero_point_two.md b/tests/commands/test_changelog_command/test_changelog_from_version_zero_point_two.md new file mode 100644 index 0000000..d1baef2 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_from_version_zero_point_two.md @@ -0,0 +1,7 @@ +## Unreleased + +### Feat + +- after 0.2 +- after 0.2.0 + diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_angular_sample.md b/tests/commands/test_changelog_command/test_changelog_incremental_angular_sample.md new file mode 100644 index 0000000..83bfdfe --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_angular_sample.md @@ -0,0 +1,16 @@ +## Unreleased + +### Feat + +- add more stuff +- add new output + +### Fix + +- mama gotta work +- output glitch + +# [10.0.0-rc.3](https://github.com/angular/angular/compare/10.0.0-rc.2...10.0.0-rc.3) (2020-04-22) + +### Bug Fixes +* **common:** format day-periods that cross midnight ([#36611](https://github.com/angular/angular/issues/36611)) ([c6e5fc4](https://github.com/angular/angular/commit/c6e5fc4)), closes [#36566](https://github.com/angular/angular/issues/36566) diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_change_tag_format.md b/tests/commands/test_changelog_command/test_changelog_incremental_change_tag_format.md new file mode 100644 index 0000000..2f0cc29 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_change_tag_format.md @@ -0,0 +1,17 @@ +## v0.3.0 (2024-11-18) + +### Feat + +- another new file + +## older-0.2.0 (2024-11-18) + +### Feat + +- new file + +## older-0.1.0 (2024-11-18) + +### Feat + +- new file diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_keep_a_changelog_sample.md b/tests/commands/test_changelog_command/test_changelog_incremental_keep_a_changelog_sample.md new file mode 100644 index 0000000..56e2cf8 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_keep_a_changelog_sample.md @@ -0,0 +1,32 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Feat + +- add more stuff +- add new output + +### Fix + +- mama gotta work +- output glitch + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_keep_a_changelog_sample_with_annotated_tag.md b/tests/commands/test_changelog_command/test_changelog_incremental_keep_a_changelog_sample_with_annotated_tag.md new file mode 100644 index 0000000..56e2cf8 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_keep_a_changelog_sample_with_annotated_tag.md @@ -0,0 +1,32 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Feat + +- add more stuff +- add new output + +### Fix + +- mama gotta work +- output glitch + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_merge_prerelease_alpha_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_merge_prerelease_alpha_.md new file mode 100644 index 0000000..8e81f62 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_merge_prerelease_alpha_.md @@ -0,0 +1,37 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Feat + +- add more stuff + +### Fix + +- mama gotta work +- output glitch + +## 0.2.0a0 (2023-04-16) + +### Feat + +- add new output + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_merge_prerelease_beta_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_merge_prerelease_beta_.md new file mode 100644 index 0000000..65f14c0 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_merge_prerelease_beta_.md @@ -0,0 +1,37 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Feat + +- add more stuff + +### Fix + +- mama gotta work +- output glitch + +## 0.2.0b0 (2023-04-16) + +### Feat + +- add new output + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_merge_prerelease_rc_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_merge_prerelease_rc_.md new file mode 100644 index 0000000..0987e11 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_merge_prerelease_rc_.md @@ -0,0 +1,37 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Feat + +- add more stuff + +### Fix + +- mama gotta work +- output glitch + +## 0.2.0rc0 (2023-04-16) + +### Feat + +- add new output + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_alpha_alpha_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_alpha_alpha_.md new file mode 100644 index 0000000..cfaca36 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_alpha_alpha_.md @@ -0,0 +1,32 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.2.0a1 (2021-06-11) + +## 0.2.0a0 (2021-06-11) + +### Feat + +- add new output + +### Fix + +- output glitch + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_alpha_beta_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_alpha_beta_.md new file mode 100644 index 0000000..013e16f --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_alpha_beta_.md @@ -0,0 +1,32 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.2.0b0 (2021-06-11) + +## 0.2.0a0 (2021-06-11) + +### Feat + +- add new output + +### Fix + +- output glitch + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_alpha_rc_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_alpha_rc_.md new file mode 100644 index 0000000..25cc276 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_alpha_rc_.md @@ -0,0 +1,32 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.2.0rc0 (2021-06-11) + +## 0.2.0a0 (2021-06-11) + +### Feat + +- add new output + +### Fix + +- output glitch + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_beta_alpha_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_beta_alpha_.md new file mode 100644 index 0000000..ec9ab11 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_beta_alpha_.md @@ -0,0 +1,32 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.2.0b1 (2021-06-11) + +## 0.2.0b0 (2021-06-11) + +### Feat + +- add new output + +### Fix + +- output glitch + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_beta_beta_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_beta_beta_.md new file mode 100644 index 0000000..ec9ab11 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_beta_beta_.md @@ -0,0 +1,32 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.2.0b1 (2021-06-11) + +## 0.2.0b0 (2021-06-11) + +### Feat + +- add new output + +### Fix + +- output glitch + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_beta_rc_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_beta_rc_.md new file mode 100644 index 0000000..22b1e14 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_beta_rc_.md @@ -0,0 +1,32 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.2.0rc0 (2021-06-11) + +## 0.2.0b0 (2021-06-11) + +### Feat + +- add new output + +### Fix + +- output glitch + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_alpha_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_alpha_.md new file mode 100644 index 0000000..e1c20b7 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_alpha_.md @@ -0,0 +1,32 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.2.0rc1 (2021-06-11) + +## 0.2.0rc0 (2021-06-11) + +### Feat + +- add new output + +### Fix + +- output glitch + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_beta_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_beta_.md new file mode 100644 index 0000000..e1c20b7 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_beta_.md @@ -0,0 +1,32 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.2.0rc1 (2021-06-11) + +## 0.2.0rc0 (2021-06-11) + +### Feat + +- add new output + +### Fix + +- output glitch + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_rc_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_rc_.md new file mode 100644 index 0000000..e1c20b7 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_prerelease_version_to_prerelease_version_rc_rc_.md @@ -0,0 +1,32 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.2.0rc1 (2021-06-11) + +## 0.2.0rc0 (2021-06-11) + +### Feat + +- add new output + +### Fix + +- output glitch + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_release_candidate_version_alpha_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_release_candidate_version_alpha_.md new file mode 100644 index 0000000..9a598e6 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_release_candidate_version_alpha_.md @@ -0,0 +1,40 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Feat + +- add more stuff + +### Fix + +- mama gotta work + +## 0.2.0a0 (2021-06-11) + +### Feat + +- add new output + +### Fix + +- output glitch + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_release_candidate_version_beta_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_release_candidate_version_beta_.md new file mode 100644 index 0000000..1f8de4f --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_release_candidate_version_beta_.md @@ -0,0 +1,40 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Feat + +- add more stuff + +### Fix + +- mama gotta work + +## 0.2.0b0 (2021-06-11) + +### Feat + +- add new output + +### Fix + +- output glitch + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_incremental_with_release_candidate_version_rc_.md b/tests/commands/test_changelog_command/test_changelog_incremental_with_release_candidate_version_rc_.md new file mode 100644 index 0000000..3ba4eb2 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_with_release_candidate_version_rc_.md @@ -0,0 +1,40 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Feat + +- add more stuff + +### Fix + +- mama gotta work + +## 0.2.0rc0 (2021-06-11) + +### Feat + +- add new output + +### Fix + +- output glitch + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. + +### Changed +- Start using "changelog" over "change log" since it's the common usage. + +### Removed +- Section about "changelog" vs "CHANGELOG". + +## [0.3.0] - 2015-12-03 +### Added +- RU translation from [@aishek](https://github.com/aishek). diff --git a/tests/commands/test_changelog_command/test_changelog_is_persisted_using_incremental.md b/tests/commands/test_changelog_command/test_changelog_is_persisted_using_incremental.md new file mode 100644 index 0000000..721d11d --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_is_persisted_using_incremental.md @@ -0,0 +1,21 @@ +## Unreleased + +### Feat + +- add more stuff + +### Fix + +- mama gotta work + +## 0.2.0 (2022-08-14) + +### Feat + +- add new output + +### Fix + +- output glitch + +note: this should be persisted using increment diff --git a/tests/commands/test_changelog_command/test_changelog_multiple_incremental_do_not_add_new_lines.md b/tests/commands/test_changelog_command/test_changelog_multiple_incremental_do_not_add_new_lines.md new file mode 100644 index 0000000..85834c2 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_multiple_incremental_do_not_add_new_lines.md @@ -0,0 +1,11 @@ +## Unreleased + +### Feat + +- add more stuff +- add new output + +### Fix + +- no more explosions +- output glitch diff --git a/tests/commands/test_changelog_command/test_changelog_prerelease_rev_with_use_scheme_semver.md b/tests/commands/test_changelog_command/test_changelog_prerelease_rev_with_use_scheme_semver.md new file mode 100644 index 0000000..9a66210 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_prerelease_rev_with_use_scheme_semver.md @@ -0,0 +1,7 @@ +## 0.3.0-a0 (2022-02-13) + +### Feat + +- another feature +- after 0.2.0 + diff --git a/tests/commands/test_changelog_command/test_changelog_prerelease_rev_with_use_scheme_semver.second-prerelease.md b/tests/commands/test_changelog_command/test_changelog_prerelease_rev_with_use_scheme_semver.second-prerelease.md new file mode 100644 index 0000000..09fd10e --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_prerelease_rev_with_use_scheme_semver.second-prerelease.md @@ -0,0 +1,2 @@ +## 0.3.0-a1 (2022-02-13) + diff --git a/tests/commands/test_changelog_command/test_changelog_release_candidate_version_with_merge_prerelease_alpha_.md b/tests/commands/test_changelog_command/test_changelog_release_candidate_version_with_merge_prerelease_alpha_.md new file mode 100644 index 0000000..dca7824 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_release_candidate_version_with_merge_prerelease_alpha_.md @@ -0,0 +1,13 @@ +## Unreleased + +### Feat + +- add more stuff +- add new output + +### Fix + +- mama gotta work +- output glitch + +## 1.0.0 (1970-01-01) diff --git a/tests/commands/test_changelog_command/test_changelog_release_candidate_version_with_merge_prerelease_beta_.md b/tests/commands/test_changelog_command/test_changelog_release_candidate_version_with_merge_prerelease_beta_.md new file mode 100644 index 0000000..dca7824 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_release_candidate_version_with_merge_prerelease_beta_.md @@ -0,0 +1,13 @@ +## Unreleased + +### Feat + +- add more stuff +- add new output + +### Fix + +- mama gotta work +- output glitch + +## 1.0.0 (1970-01-01) diff --git a/tests/commands/test_changelog_command/test_changelog_release_candidate_version_with_merge_prerelease_rc_.md b/tests/commands/test_changelog_command/test_changelog_release_candidate_version_with_merge_prerelease_rc_.md new file mode 100644 index 0000000..dca7824 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_release_candidate_version_with_merge_prerelease_rc_.md @@ -0,0 +1,13 @@ +## Unreleased + +### Feat + +- add more stuff +- add new output + +### Fix + +- mama gotta work +- output glitch + +## 1.0.0 (1970-01-01) diff --git a/tests/commands/test_changelog_command/test_changelog_replacing_unreleased_using_incremental_asciidoc_.adoc b/tests/commands/test_changelog_command/test_changelog_replacing_unreleased_using_incremental_asciidoc_.adoc new file mode 100644 index 0000000..2e789bc --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_replacing_unreleased_using_incremental_asciidoc_.adoc @@ -0,0 +1,19 @@ +== Unreleased + +=== Feat + +* add more stuff + +=== Fix + +* mama gotta work + +== 0.2.0 (2022-08-14) + +=== Feat + +* add new output + +=== Fix + +* output glitch diff --git a/tests/commands/test_changelog_command/test_changelog_replacing_unreleased_using_incremental_markdown_.md b/tests/commands/test_changelog_command/test_changelog_replacing_unreleased_using_incremental_markdown_.md new file mode 100644 index 0000000..8fca3c6 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_replacing_unreleased_using_incremental_markdown_.md @@ -0,0 +1,19 @@ +## Unreleased + +### Feat + +- add more stuff + +### Fix + +- mama gotta work + +## 0.2.0 (2022-08-14) + +### Feat + +- add new output + +### Fix + +- output glitch diff --git a/tests/commands/test_changelog_command/test_changelog_replacing_unreleased_using_incremental_restructuredtext_.rst b/tests/commands/test_changelog_command/test_changelog_replacing_unreleased_using_incremental_restructuredtext_.rst new file mode 100644 index 0000000..ca0077a --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_replacing_unreleased_using_incremental_restructuredtext_.rst @@ -0,0 +1,25 @@ +Unreleased +========== + +Feat +---- + +- add more stuff + +Fix +--- + +- mama gotta work + +0.2.0 (2022-08-14) +================== + +Feat +---- + +- add new output + +Fix +--- + +- output glitch diff --git a/tests/commands/test_changelog_command/test_changelog_replacing_unreleased_using_incremental_textile_.textile b/tests/commands/test_changelog_command/test_changelog_replacing_unreleased_using_incremental_textile_.textile new file mode 100644 index 0000000..07f2ba5 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_replacing_unreleased_using_incremental_textile_.textile @@ -0,0 +1,19 @@ +h2. Unreleased + +h3. Feat + +- add more stuff + +h3. Fix + +- mama gotta work + +h2. 0.2.0 (2022-08-14) + +h3. Feat + +- add new output + +h3. Fix + +- output glitch diff --git a/tests/commands/test_changelog_command/test_changelog_with_customized_change_type_order.md b/tests/commands/test_changelog_command/test_changelog_with_customized_change_type_order.md new file mode 100644 index 0000000..358908e --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_with_customized_change_type_order.md @@ -0,0 +1,20 @@ +## 0.4.0 (2022-02-13) + +### Perf + +- perf improvement + +### Feat + +- getting ready for this + +## 0.3.0 (2022-02-13) + +### Fix + +- fix bug + +### Feat + +- another feature +- after 0.2.0 diff --git a/tests/commands/test_changelog_command/test_changelog_with_different_cz.md b/tests/commands/test_changelog_command/test_changelog_with_different_cz.md new file mode 100644 index 0000000..e9d3062 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_with_different_cz.md @@ -0,0 +1,6 @@ +## Unreleased + + +- JRA-35 #time 1w 2d 4h 30m Total work logged +- JRA-34 #comment corrected indent issue + diff --git a/tests/commands/test_check_command.py b/tests/commands/test_check_command.py new file mode 100644 index 0000000..f1db446 --- /dev/null +++ b/tests/commands/test_check_command.py @@ -0,0 +1,454 @@ +from __future__ import annotations + +import sys +from io import StringIO + +import pytest +from pytest_mock import MockFixture + +from commitizen import cli, commands, git +from commitizen.exceptions import ( + InvalidCommandArgumentError, + InvalidCommitMessageError, + NoCommitsFoundError, +) +from tests.utils import create_file_and_commit, skip_below_py_3_13 + +COMMIT_LOG = [ + "refactor: A code change that neither fixes a bug nor adds a feature", + r"refactor(cz/connventional_commit): use \S to check scope", + "refactor(git): remove unnecessary dot between git range", + "bump: version 1.16.3 โ†’ 1.16.4", + ( + "Merge pull request #139 from Lee-W/fix-init-clean-config-file\n\n" + "Fix init clean config file" + ), + "ci(pyproject.toml): add configuration for coverage", + "fix(commands/init): fix clean up file when initialize commitizen config\n\n#138", + "refactor(defaults): split config files into long term support and deprecated ones", + "bump: version 1.16.2 โ†’ 1.16.3", + ( + "Merge pull request #136 from Lee-W/remove-redundant-readme\n\n" + "Remove redundant readme" + ), + "fix: replace README.rst with docs/README.md in config files", + ( + "refactor(docs): remove README.rst and use docs/README.md\n\n" + "By removing README.rst, we no longer need to maintain " + "two document with almost the same content\n" + "Github can read docs/README.md as README for the project." + ), + "docs(check): pin pre-commit to v1.16.2", + "docs(check): fix pre-commit setup", + "bump: version 1.16.1 โ†’ 1.16.2", + "Merge pull request #135 from Lee-W/fix-pre-commit-hook\n\nFix pre commit hook", + "docs(check): enforce cz check only when committing", + ( + 'Revert "fix(pre-commit): set pre-commit check stage to commit-msg"\n\n' + "This reverts commit afc70133e4a81344928561fbf3bb20738dfc8a0b." + ), + "feat!: add user stuff", + "fixup! test(commands): ignore fixup! prefix", + "fixup! test(commands): ignore squash! prefix", +] + + +def _build_fake_git_commits(commit_msgs: list[str]) -> list[git.GitCommit]: + return [git.GitCommit("test_rev", commit_msg) for commit_msg in commit_msgs] + + +def test_check_jira_fails(mocker: MockFixture): + testargs = ["cz", "-n", "cz_jira", "check", "--commit-msg-file", "some_file"] + mocker.patch.object(sys, "argv", testargs) + mocker.patch( + "commitizen.commands.check.open", + mocker.mock_open(read_data="random message for J-2 #fake_command blah"), + ) + with pytest.raises(InvalidCommitMessageError) as excinfo: + cli.main() + assert "commit validation: failed!" in str(excinfo.value) + + +def test_check_jira_command_after_issue_one_space(mocker: MockFixture, capsys): + testargs = ["cz", "-n", "cz_jira", "check", "--commit-msg-file", "some_file"] + mocker.patch.object(sys, "argv", testargs) + mocker.patch( + "commitizen.commands.check.open", + mocker.mock_open(read_data="JR-23 #command some arguments etc"), + ) + cli.main() + out, _ = capsys.readouterr() + assert "Commit validation: successful!" in out + + +def test_check_jira_command_after_issue_two_spaces(mocker: MockFixture, capsys): + testargs = ["cz", "-n", "cz_jira", "check", "--commit-msg-file", "some_file"] + mocker.patch.object(sys, "argv", testargs) + mocker.patch( + "commitizen.commands.check.open", + mocker.mock_open(read_data="JR-2 #command some arguments etc"), + ) + cli.main() + out, _ = capsys.readouterr() + assert "Commit validation: successful!" in out + + +def test_check_jira_text_between_issue_and_command(mocker: MockFixture, capsys): + testargs = ["cz", "-n", "cz_jira", "check", "--commit-msg-file", "some_file"] + mocker.patch.object(sys, "argv", testargs) + mocker.patch( + "commitizen.commands.check.open", + mocker.mock_open(read_data="JR-234 some text #command some arguments etc"), + ) + cli.main() + out, _ = capsys.readouterr() + assert "Commit validation: successful!" in out + + +def test_check_jira_multiple_commands(mocker: MockFixture, capsys): + testargs = ["cz", "-n", "cz_jira", "check", "--commit-msg-file", "some_file"] + mocker.patch.object(sys, "argv", testargs) + mocker.patch( + "commitizen.commands.check.open", + mocker.mock_open(read_data="JRA-23 some text #command1 args #command2 args"), + ) + cli.main() + out, _ = capsys.readouterr() + assert "Commit validation: successful!" in out + + +def test_check_conventional_commit_succeeds(mocker: MockFixture, capsys): + testargs = ["cz", "check", "--commit-msg-file", "some_file"] + mocker.patch.object(sys, "argv", testargs) + mocker.patch( + "commitizen.commands.check.open", + mocker.mock_open(read_data="fix(scope): some commit message"), + ) + cli.main() + out, _ = capsys.readouterr() + assert "Commit validation: successful!" in out + + +@pytest.mark.parametrize( + "commit_msg", + ( + "feat!(lang): removed polish language", + "no conventional commit", + ( + "ci: check commit message on merge\n" + "testing with more complex commit mes\n\n" + "age with error" + ), + ), +) +def test_check_no_conventional_commit(commit_msg, config, mocker: MockFixture, tmpdir): + with pytest.raises(InvalidCommitMessageError): + error_mock = mocker.patch("commitizen.out.error") + + tempfile = tmpdir.join("temp_commit_file") + tempfile.write(commit_msg) + + check_cmd = commands.Check( + config=config, arguments={"commit_msg_file": tempfile} + ) + check_cmd() + error_mock.assert_called_once() + + +@pytest.mark.parametrize( + "commit_msg", + ( + "feat(lang)!: removed polish language", + "feat(lang): added polish language", + "feat: add polish language", + "bump: 0.0.1 -> 1.0.0", + ), +) +def test_check_conventional_commit(commit_msg, config, mocker: MockFixture, tmpdir): + success_mock = mocker.patch("commitizen.out.success") + + tempfile = tmpdir.join("temp_commit_file") + tempfile.write(commit_msg) + + check_cmd = commands.Check(config=config, arguments={"commit_msg_file": tempfile}) + + check_cmd() + success_mock.assert_called_once() + + +def test_check_command_when_commit_file_not_found(config): + with pytest.raises(FileNotFoundError): + commands.Check(config=config, arguments={"commit_msg_file": "no_such_file"})() + + +def test_check_a_range_of_git_commits(config, mocker: MockFixture): + success_mock = mocker.patch("commitizen.out.success") + mocker.patch( + "commitizen.git.get_commits", return_value=_build_fake_git_commits(COMMIT_LOG) + ) + + check_cmd = commands.Check( + config=config, arguments={"rev_range": "HEAD~10..master"} + ) + + check_cmd() + success_mock.assert_called_once() + + +def test_check_a_range_of_git_commits_and_failed(config, mocker: MockFixture): + error_mock = mocker.patch("commitizen.out.error") + mocker.patch( + "commitizen.git.get_commits", + return_value=_build_fake_git_commits(["This commit does not follow rule"]), + ) + check_cmd = commands.Check( + config=config, arguments={"rev_range": "HEAD~10..master"} + ) + + with pytest.raises(InvalidCommitMessageError): + check_cmd() + error_mock.assert_called_once() + + +def test_check_command_with_invalid_argument(config): + with pytest.raises(InvalidCommandArgumentError) as excinfo: + commands.Check( + config=config, + arguments={"commit_msg_file": "some_file", "rev_range": "HEAD~10..master"}, + ) + assert ( + "Only one of --rev-range, --message, and --commit-msg-file is permitted by check command!" + in str(excinfo.value) + ) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_check_command_with_empty_range(config, mocker: MockFixture): + # must initialize git with a commit + create_file_and_commit("feat: initial") + + check_cmd = commands.Check(config=config, arguments={"rev_range": "master..master"}) + with pytest.raises(NoCommitsFoundError) as excinfo: + check_cmd() + + assert "No commit found with range: 'master..master'" in str(excinfo) + + +def test_check_a_range_of_failed_git_commits(config, mocker: MockFixture): + ill_formated_commits_msgs = [ + "First commit does not follow rule", + "Second commit does not follow rule", + ("Third commit does not follow rule\nIll-formatted commit with body"), + ] + mocker.patch( + "commitizen.git.get_commits", + return_value=_build_fake_git_commits(ill_formated_commits_msgs), + ) + check_cmd = commands.Check( + config=config, arguments={"rev_range": "HEAD~10..master"} + ) + + with pytest.raises(InvalidCommitMessageError) as excinfo: + check_cmd() + assert all([msg in str(excinfo.value) for msg in ill_formated_commits_msgs]) + + +def test_check_command_with_valid_message(config, mocker: MockFixture): + success_mock = mocker.patch("commitizen.out.success") + check_cmd = commands.Check( + config=config, arguments={"message": "fix(scope): some commit message"} + ) + + check_cmd() + success_mock.assert_called_once() + + +def test_check_command_with_invalid_message(config, mocker: MockFixture): + error_mock = mocker.patch("commitizen.out.error") + check_cmd = commands.Check(config=config, arguments={"message": "bad commit"}) + + with pytest.raises(InvalidCommitMessageError): + check_cmd() + error_mock.assert_called_once() + + +def test_check_command_with_empty_message(config, mocker: MockFixture): + error_mock = mocker.patch("commitizen.out.error") + check_cmd = commands.Check(config=config, arguments={"message": ""}) + + with pytest.raises(InvalidCommitMessageError): + check_cmd() + error_mock.assert_called_once() + + +def test_check_command_with_allow_abort_arg(config, mocker: MockFixture): + success_mock = mocker.patch("commitizen.out.success") + check_cmd = commands.Check( + config=config, arguments={"message": "", "allow_abort": True} + ) + + check_cmd() + success_mock.assert_called_once() + + +def test_check_command_with_allow_abort_config(config, mocker: MockFixture): + success_mock = mocker.patch("commitizen.out.success") + config.settings["allow_abort"] = True + check_cmd = commands.Check(config=config, arguments={"message": ""}) + + check_cmd() + success_mock.assert_called_once() + + +def test_check_command_override_allow_abort_config(config, mocker: MockFixture): + error_mock = mocker.patch("commitizen.out.error") + config.settings["allow_abort"] = True + check_cmd = commands.Check( + config=config, arguments={"message": "", "allow_abort": False} + ) + + with pytest.raises(InvalidCommitMessageError): + check_cmd() + error_mock.assert_called_once() + + +def test_check_command_with_allowed_prefixes_arg(config, mocker: MockFixture): + success_mock = mocker.patch("commitizen.out.success") + check_cmd = commands.Check( + config=config, + arguments={"message": "custom! test", "allowed_prefixes": ["custom!"]}, + ) + + check_cmd() + success_mock.assert_called_once() + + +def test_check_command_with_allowed_prefixes_config(config, mocker: MockFixture): + success_mock = mocker.patch("commitizen.out.success") + config.settings["allowed_prefixes"] = ["custom!"] + check_cmd = commands.Check(config=config, arguments={"message": "custom! test"}) + + check_cmd() + success_mock.assert_called_once() + + +def test_check_command_override_allowed_prefixes_config(config, mocker: MockFixture): + error_mock = mocker.patch("commitizen.out.error") + config.settings["allow_abort"] = ["fixup!"] + check_cmd = commands.Check( + config=config, + arguments={"message": "fixup! test", "allowed_prefixes": ["custom!"]}, + ) + + with pytest.raises(InvalidCommitMessageError): + check_cmd() + error_mock.assert_called_once() + + +def test_check_command_with_pipe_message(mocker: MockFixture, capsys): + testargs = ["cz", "check"] + mocker.patch.object(sys, "argv", testargs) + mocker.patch("sys.stdin", StringIO("fix(scope): some commit message")) + + cli.main() + out, _ = capsys.readouterr() + assert "Commit validation: successful!" in out + + +def test_check_command_with_pipe_message_and_failed(mocker: MockFixture): + testargs = ["cz", "check"] + mocker.patch.object(sys, "argv", testargs) + mocker.patch("sys.stdin", StringIO("bad commit message")) + + with pytest.raises(InvalidCommitMessageError) as excinfo: + cli.main() + assert "commit validation: failed!" in str(excinfo.value) + + +def test_check_command_with_comment_in_messege_file(mocker: MockFixture, capsys): + testargs = ["cz", "check", "--commit-msg-file", "some_file"] + mocker.patch.object(sys, "argv", testargs) + mocker.patch( + "commitizen.commands.check.open", + mocker.mock_open( + read_data="# <type>: (If applied, this commit will...) <subject>\n" + "# |<---- Try to Limit to a Max of 50 char ---->|\n" + "ci: add commitizen pre-commit hook\n" + "\n" + "# Explain why this change is being made\n" + "# |<---- Try To Limit Each Line to a Max Of 72 Char ---->|\n" + "This pre-commit hook will check our commits automatically." + ), + ) + cli.main() + out, _ = capsys.readouterr() + assert "Commit validation: successful!" in out + + +def test_check_conventional_commit_succeed_with_git_diff(mocker, capsys): + commit_msg = ( + "feat: This is a test commit\n" + "# Please enter the commit message for your changes. Lines starting\n" + "# with '#' will be ignored, and an empty message aborts the commit.\n" + "#\n" + "# On branch ...\n" + "# Changes to be committed:\n" + "# modified: ...\n" + "#\n" + "# ------------------------ >8 ------------------------\n" + "# Do not modify or remove the line above.\n" + "# Everything below it will be ignored.\n" + "diff --git a/... b/...\n" + "index f1234c..1c5678 1234\n" + "--- a/...\n" + "+++ b/...\n" + "@@ -92,3 +92,4 @@ class Command(BaseCommand):\n" + '+ "this is a test"\n' + ) + testargs = ["cz", "check", "--commit-msg-file", "some_file"] + mocker.patch.object(sys, "argv", testargs) + mocker.patch( + "commitizen.commands.check.open", + mocker.mock_open(read_data=commit_msg), + ) + cli.main() + out, _ = capsys.readouterr() + assert "Commit validation: successful!" in out + + +@skip_below_py_3_13 +def test_check_command_shows_description_when_use_help_option( + mocker: MockFixture, capsys, file_regression +): + testargs = ["cz", "check", "--help"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(SystemExit): + cli.main() + + out, _ = capsys.readouterr() + file_regression.check(out, extension=".txt") + + +def test_check_command_with_message_length_limit(config, mocker: MockFixture): + success_mock = mocker.patch("commitizen.out.success") + message = "fix(scope): some commit message" + check_cmd = commands.Check( + config=config, + arguments={"message": message, "message_length_limit": len(message) + 1}, + ) + + check_cmd() + success_mock.assert_called_once() + + +def test_check_command_with_message_length_limit_exceeded(config, mocker: MockFixture): + error_mock = mocker.patch("commitizen.out.error") + message = "fix(scope): some commit message" + check_cmd = commands.Check( + config=config, + arguments={"message": message, "message_length_limit": len(message) - 1}, + ) + + with pytest.raises(InvalidCommitMessageError): + check_cmd() + error_mock.assert_called_once() diff --git a/tests/commands/test_check_command/test_check_command_shows_description_when_use_help_option.txt b/tests/commands/test_check_command/test_check_command_shows_description_when_use_help_option.txt new file mode 100644 index 0000000..85f42f6 --- /dev/null +++ b/tests/commands/test_check_command/test_check_command_shows_description_when_use_help_option.txt @@ -0,0 +1,25 @@ +usage: cz check [-h] [--commit-msg-file COMMIT_MSG_FILE | + --rev-range REV_RANGE | -m MESSAGE] [--allow-abort] + [--allowed-prefixes [ALLOWED_PREFIXES ...]] + [-l MESSAGE_LENGTH_LIMIT] + +validates that a commit message matches the commitizen schema + +options: + -h, --help show this help message and exit + --commit-msg-file COMMIT_MSG_FILE + ask for the name of the temporal file that contains + the commit message. Using it in a git hook script: + MSG_FILE=$1 + --rev-range REV_RANGE + a range of git rev to check. e.g, master..HEAD + -m, --message MESSAGE + commit message that needs to be checked + --allow-abort allow empty commit messages, which typically abort a + commit + --allowed-prefixes [ALLOWED_PREFIXES ...] + allowed commit message prefixes. If the message starts + by one of these prefixes, the message won't be checked + against the regex + -l, --message-length-limit MESSAGE_LENGTH_LIMIT + length limit of the commit message; 0 for no limit diff --git a/tests/commands/test_commit_command.py b/tests/commands/test_commit_command.py new file mode 100644 index 0000000..55751f6 --- /dev/null +++ b/tests/commands/test_commit_command.py @@ -0,0 +1,527 @@ +import os +import sys +from unittest.mock import ANY + +import pytest +from pytest_mock import MockFixture + +from commitizen import cli, cmd, commands +from commitizen.cz.exceptions import CzException +from commitizen.cz.utils import get_backup_file_path +from commitizen.exceptions import ( + CommitError, + CommitMessageLengthExceededError, + CustomError, + DryRunExit, + NoAnswersError, + NoCommitBackupError, + NotAGitProjectError, + NotAllowed, + NothingToCommitError, +) +from tests.utils import skip_below_py_3_13 + + +@pytest.fixture +def staging_is_clean(mocker: MockFixture, tmp_git_project): + is_staging_clean_mock = mocker.patch("commitizen.git.is_staging_clean") + is_staging_clean_mock.return_value = False + return tmp_git_project + + +@pytest.fixture +def backup_file(tmp_git_project): + with open(get_backup_file_path(), "w") as backup_file: + backup_file.write("backup commit") + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit(config, mocker: MockFixture): + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.return_value = { + "prefix": "feat", + "subject": "user created", + "scope": "", + "is_breaking_change": False, + "body": "", + "footer": "", + } + + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("success", "", b"", b"", 0) + success_mock = mocker.patch("commitizen.out.success") + + commands.Commit(config, {})() + success_mock.assert_called_once() + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_backup_on_failure(config, mocker: MockFixture): + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.return_value = { + "prefix": "feat", + "subject": "user created", + "scope": "", + "is_breaking_change": False, + "body": "closes #21", + "footer": "", + } + + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("", "error", b"", b"", 9) + error_mock = mocker.patch("commitizen.out.error") + + with pytest.raises(CommitError): + commit_cmd = commands.Commit(config, {}) + temp_file = commit_cmd.temp_file + commit_cmd() + + prompt_mock.assert_called_once() + error_mock.assert_called_once() + assert os.path.isfile(temp_file) + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_retry_fails_no_backup(config, mocker: MockFixture): + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("success", "", b"", b"", 0) + + with pytest.raises(NoCommitBackupError) as excinfo: + commands.Commit(config, {"retry": True})() + + assert NoCommitBackupError.message in str(excinfo.value) + + +@pytest.mark.usefixtures("staging_is_clean", "backup_file") +def test_commit_retry_works(config, mocker: MockFixture): + prompt_mock = mocker.patch("questionary.prompt") + + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("success", "", b"", b"", 0) + success_mock = mocker.patch("commitizen.out.success") + + commit_cmd = commands.Commit(config, {"retry": True}) + temp_file = commit_cmd.temp_file + commit_cmd() + + commit_mock.assert_called_with("backup commit", args="") + prompt_mock.assert_not_called() + success_mock.assert_called_once() + assert not os.path.isfile(temp_file) + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_retry_after_failure_no_backup(config, mocker: MockFixture): + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.return_value = { + "prefix": "feat", + "subject": "user created", + "scope": "", + "is_breaking_change": False, + "body": "closes #21", + "footer": "", + } + + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("success", "", b"", b"", 0) + success_mock = mocker.patch("commitizen.out.success") + + config.settings["retry_after_failure"] = True + commands.Commit(config, {})() + + commit_mock.assert_called_with("feat: user created\n\ncloses #21", args="") + prompt_mock.assert_called_once() + success_mock.assert_called_once() + + +@pytest.mark.usefixtures("staging_is_clean", "backup_file") +def test_commit_retry_after_failure_works(config, mocker: MockFixture): + prompt_mock = mocker.patch("questionary.prompt") + + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("success", "", b"", b"", 0) + success_mock = mocker.patch("commitizen.out.success") + + config.settings["retry_after_failure"] = True + commit_cmd = commands.Commit(config, {}) + temp_file = commit_cmd.temp_file + commit_cmd() + + commit_mock.assert_called_with("backup commit", args="") + prompt_mock.assert_not_called() + success_mock.assert_called_once() + assert not os.path.isfile(temp_file) + + +@pytest.mark.usefixtures("staging_is_clean", "backup_file") +def test_commit_retry_after_failure_with_no_retry_works(config, mocker: MockFixture): + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.return_value = { + "prefix": "feat", + "subject": "user created", + "scope": "", + "is_breaking_change": False, + "body": "closes #21", + "footer": "", + } + + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("success", "", b"", b"", 0) + success_mock = mocker.patch("commitizen.out.success") + + config.settings["retry_after_failure"] = True + commit_cmd = commands.Commit(config, {"no_retry": True}) + temp_file = commit_cmd.temp_file + commit_cmd() + + commit_mock.assert_called_with("feat: user created\n\ncloses #21", args="") + prompt_mock.assert_called_once() + success_mock.assert_called_once() + assert not os.path.isfile(temp_file) + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_command_with_dry_run_option(config, mocker: MockFixture): + prompt_mock = mocker = mocker.patch("questionary.prompt") + prompt_mock.return_value = { + "prefix": "feat", + "subject": "user created", + "scope": "", + "is_breaking_change": False, + "body": "closes #57", + "footer": "", + } + + with pytest.raises(DryRunExit): + commit_cmd = commands.Commit(config, {"dry_run": True}) + commit_cmd() + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_command_with_write_message_to_file_option( + config, tmp_path, mocker: MockFixture +): + tmp_file = tmp_path / "message" + + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.return_value = { + "prefix": "feat", + "subject": "user created", + "scope": "", + "is_breaking_change": False, + "body": "", + "footer": "", + } + + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("success", "", b"", b"", 0) + success_mock = mocker.patch("commitizen.out.success") + + commands.Commit(config, {"write_message_to_file": tmp_file})() + success_mock.assert_called_once() + assert tmp_file.exists() + assert tmp_file.read_text() == "feat: user created" + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_command_with_invalid_write_message_to_file_option( + config, tmp_path, mocker: MockFixture +): + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.return_value = { + "prefix": "feat", + "subject": "user created", + "scope": "", + "is_breaking_change": False, + "body": "", + "footer": "", + } + + with pytest.raises(NotAllowed): + commit_cmd = commands.Commit(config, {"write_message_to_file": tmp_path}) + commit_cmd() + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_command_with_signoff_option(config, mocker: MockFixture): + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.return_value = { + "prefix": "feat", + "subject": "user created", + "scope": "", + "is_breaking_change": False, + "body": "", + "footer": "", + } + + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("success", "", b"", b"", 0) + success_mock = mocker.patch("commitizen.out.success") + + commands.Commit(config, {"signoff": True})() + + commit_mock.assert_called_once_with(ANY, args="-s") + success_mock.assert_called_once() + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_command_with_always_signoff_enabled(config, mocker: MockFixture): + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.return_value = { + "prefix": "feat", + "subject": "user created", + "scope": "", + "is_breaking_change": False, + "body": "", + "footer": "", + } + + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("success", "", b"", b"", 0) + success_mock = mocker.patch("commitizen.out.success") + + config.settings["always_signoff"] = True + commands.Commit(config, {})() + + commit_mock.assert_called_once_with(ANY, args="-s") + success_mock.assert_called_once() + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_command_with_gpgsign_and_always_signoff_enabled( + config, mocker: MockFixture +): + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.return_value = { + "prefix": "feat", + "subject": "user created", + "scope": "", + "is_breaking_change": False, + "body": "", + "footer": "", + } + + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("success", "", b"", b"", 0) + success_mock = mocker.patch("commitizen.out.success") + + config.settings["always_signoff"] = True + commands.Commit(config, {"extra_cli_args": "-S"})() + + commit_mock.assert_called_once_with(ANY, args="-S -s") + success_mock.assert_called_once() + + +@pytest.mark.usefixtures("tmp_git_project") +def test_commit_when_nothing_to_commit(config, mocker: MockFixture): + is_staging_clean_mock = mocker.patch("commitizen.git.is_staging_clean") + is_staging_clean_mock.return_value = True + + with pytest.raises(NothingToCommitError) as excinfo: + commit_cmd = commands.Commit(config, {}) + commit_cmd() + + assert "No files added to staging!" in str(excinfo.value) + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_with_allow_empty(config, mocker: MockFixture): + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.return_value = { + "prefix": "feat", + "subject": "user created", + "scope": "", + "is_breaking_change": False, + "body": "closes #21", + "footer": "", + } + + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("success", "", b"", b"", 0) + success_mock = mocker.patch("commitizen.out.success") + + commands.Commit(config, {"extra_cli_args": "--allow-empty"})() + + commit_mock.assert_called_with( + "feat: user created\n\ncloses #21", args="--allow-empty" + ) + success_mock.assert_called_once() + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_with_signoff_and_allow_empty(config, mocker: MockFixture): + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.return_value = { + "prefix": "feat", + "subject": "user created", + "scope": "", + "is_breaking_change": False, + "body": "closes #21", + "footer": "", + } + + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("success", "", b"", b"", 0) + success_mock = mocker.patch("commitizen.out.success") + + config.settings["always_signoff"] = True + commands.Commit(config, {"extra_cli_args": "--allow-empty"})() + + commit_mock.assert_called_with( + "feat: user created\n\ncloses #21", args="--allow-empty -s" + ) + success_mock.assert_called_once() + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_when_customized_expected_raised(config, mocker: MockFixture, capsys): + _err = ValueError() + _err.__context__ = CzException("This is the root custom err") + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.side_effect = _err + + with pytest.raises(CustomError) as excinfo: + commit_cmd = commands.Commit(config, {}) + commit_cmd() + + # Assert only the content in the formatted text + assert "This is the root custom err" in str(excinfo.value) + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_when_non_customized_expected_raised( + config, mocker: MockFixture, capsys +): + _err = ValueError() + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.side_effect = _err + + with pytest.raises(ValueError): + commit_cmd = commands.Commit(config, {}) + commit_cmd() + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_when_no_user_answer(config, mocker: MockFixture, capsys): + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.return_value = None + + with pytest.raises(NoAnswersError): + commit_cmd = commands.Commit(config, {}) + commit_cmd() + + +def test_commit_in_non_git_project(tmpdir, config): + with tmpdir.as_cwd(): + with pytest.raises(NotAGitProjectError): + commands.Commit(config, {}) + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_command_with_all_option(config, mocker: MockFixture): + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.return_value = { + "prefix": "feat", + "subject": "user created", + "scope": "", + "is_breaking_change": False, + "body": "", + "footer": "", + } + + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("success", "", b"", b"", 0) + success_mock = mocker.patch("commitizen.out.success") + add_mock = mocker.patch("commitizen.git.add") + commands.Commit(config, {"all": True})() + add_mock.assert_called() + success_mock.assert_called_once() + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_command_with_extra_args(config, mocker: MockFixture): + prompt_mock = mocker.patch("questionary.prompt") + prompt_mock.return_value = { + "prefix": "feat", + "subject": "user created", + "scope": "", + "is_breaking_change": False, + "body": "", + "footer": "", + } + + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("success", "", b"", b"", 0) + success_mock = mocker.patch("commitizen.out.success") + commands.Commit(config, {"extra_cli_args": "-- -extra-args1 -extra-arg2"})() + commit_mock.assert_called_once_with(ANY, args="-- -extra-args1 -extra-arg2") + success_mock.assert_called_once() + + +@pytest.mark.usefixtures("staging_is_clean") +def test_commit_command_with_message_length_limit(config, mocker: MockFixture): + prompt_mock = mocker.patch("questionary.prompt") + prefix = "feat" + subject = "random subject" + message_length = len(prefix) + len(": ") + len(subject) + prompt_mock.return_value = { + "prefix": prefix, + "subject": subject, + "scope": "", + "is_breaking_change": False, + "body": "random body", + "footer": "random footer", + } + + commit_mock = mocker.patch("commitizen.git.commit") + commit_mock.return_value = cmd.Command("success", "", b"", b"", 0) + success_mock = mocker.patch("commitizen.out.success") + + commands.Commit(config, {"message_length_limit": message_length})() + success_mock.assert_called_once() + + with pytest.raises(CommitMessageLengthExceededError): + commands.Commit(config, {"message_length_limit": message_length - 1})() + + +@pytest.mark.usefixtures("staging_is_clean") +@pytest.mark.parametrize("editor", ["vim", None]) +def test_manual_edit(editor, config, mocker: MockFixture, tmp_path): + mocker.patch("commitizen.git.get_core_editor", return_value=editor) + subprocess_mock = mocker.patch("subprocess.call") + + mocker.patch("shutil.which", return_value=editor) + + test_message = "Initial commit message" + temp_file = tmp_path / "temp_commit_message" + temp_file.write_text(test_message) + + mock_temp_file = mocker.patch("tempfile.NamedTemporaryFile") + mock_temp_file.return_value.__enter__.return_value.name = str(temp_file) + + commit_cmd = commands.Commit(config, {"edit": True}) + + if editor is None: + with pytest.raises(RuntimeError): + commit_cmd.manual_edit(test_message) + else: + edited_message = commit_cmd.manual_edit(test_message) + + subprocess_mock.assert_called_once_with(["vim", str(temp_file)]) + + assert edited_message == test_message.strip() + + temp_file.unlink() + + +@skip_below_py_3_13 +def test_commit_command_shows_description_when_use_help_option( + mocker: MockFixture, capsys, file_regression +): + testargs = ["cz", "commit", "--help"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(SystemExit): + cli.main() + + out, _ = capsys.readouterr() + file_regression.check(out, extension=".txt") diff --git a/tests/commands/test_commit_command/test_commit_command_shows_description_when_use_help_option.txt b/tests/commands/test_commit_command/test_commit_command_shows_description_when_use_help_option.txt new file mode 100644 index 0000000..dd1f53f --- /dev/null +++ b/tests/commands/test_commit_command/test_commit_command_shows_description_when_use_help_option.txt @@ -0,0 +1,22 @@ +usage: cz commit [-h] [--retry] [--no-retry] [--dry-run] + [--write-message-to-file FILE_PATH] [-s] [-a] [-e] + [-l MESSAGE_LENGTH_LIMIT] [--] + +create new commit + +options: + -h, --help show this help message and exit + --retry retry last commit + --no-retry skip retry if retry_after_failure is set to true + --dry-run show output to stdout, no commit, no modified files + --write-message-to-file FILE_PATH + write message to file before committing (can be + combined with --dry-run) + -s, --signoff sign off the commit + -a, --all Tell the command to automatically stage files that + have been modified and deleted, but new files you have + not told Git about are not affected. + -e, --edit edit the commit message before committing + -l, --message-length-limit MESSAGE_LENGTH_LIMIT + length limit of the commit message; 0 for no limit + -- Positional arguments separator (recommended) diff --git a/tests/commands/test_example_command.py b/tests/commands/test_example_command.py new file mode 100644 index 0000000..0521679 --- /dev/null +++ b/tests/commands/test_example_command.py @@ -0,0 +1,26 @@ +import sys + +import pytest +from pytest_mock import MockerFixture + +from commitizen import cli, commands +from tests.utils import skip_below_py_3_10 + + +def test_example(config, mocker: MockerFixture): + write_mock = mocker.patch("commitizen.out.write") + commands.Example(config)() + write_mock.assert_called_once() + + +@skip_below_py_3_10 +def test_example_command_shows_description_when_use_help_option( + mocker: MockerFixture, capsys, file_regression +): + testargs = ["cz", "example", "--help"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(SystemExit): + cli.main() + + out, _ = capsys.readouterr() + file_regression.check(out, extension=".txt") diff --git a/tests/commands/test_example_command/test_example_command_shows_description_when_use_help_option.txt b/tests/commands/test_example_command/test_example_command_shows_description_when_use_help_option.txt new file mode 100644 index 0000000..b9bf7f8 --- /dev/null +++ b/tests/commands/test_example_command/test_example_command_shows_description_when_use_help_option.txt @@ -0,0 +1,6 @@ +usage: cz example [-h] + +show commit example + +options: + -h, --help show this help message and exit diff --git a/tests/commands/test_info_command.py b/tests/commands/test_info_command.py new file mode 100644 index 0000000..2bd1553 --- /dev/null +++ b/tests/commands/test_info_command.py @@ -0,0 +1,26 @@ +import sys + +import pytest +from pytest_mock import MockerFixture + +from commitizen import cli, commands +from tests.utils import skip_below_py_3_10 + + +def test_info(config, mocker: MockerFixture): + write_mock = mocker.patch("commitizen.out.write") + commands.Info(config)() + write_mock.assert_called_once() + + +@skip_below_py_3_10 +def test_info_command_shows_description_when_use_help_option( + mocker: MockerFixture, capsys, file_regression +): + testargs = ["cz", "info", "--help"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(SystemExit): + cli.main() + + out, _ = capsys.readouterr() + file_regression.check(out, extension=".txt") diff --git a/tests/commands/test_info_command/test_info_command_shows_description_when_use_help_option.txt b/tests/commands/test_info_command/test_info_command_shows_description_when_use_help_option.txt new file mode 100644 index 0000000..99b1ba8 --- /dev/null +++ b/tests/commands/test_info_command/test_info_command_shows_description_when_use_help_option.txt @@ -0,0 +1,6 @@ +usage: cz info [-h] + +show information about the cz + +options: + -h, --help show this help message and exit diff --git a/tests/commands/test_init_command.py b/tests/commands/test_init_command.py new file mode 100644 index 0000000..ea18e89 --- /dev/null +++ b/tests/commands/test_init_command.py @@ -0,0 +1,268 @@ +from __future__ import annotations + +import json +import os +import sys +from typing import Any + +import pytest +import yaml +from pytest_mock import MockFixture + +from commitizen import cli, commands +from commitizen.__version__ import __version__ +from commitizen.exceptions import InitFailedError, NoAnswersError +from tests.utils import skip_below_py_3_10 + + +class FakeQuestion: + def __init__(self, expected_return): + self.expected_return = expected_return + + def ask(self): + return self.expected_return + + def unsafe_ask(self): + return self.expected_return + + +pre_commit_config_filename = ".pre-commit-config.yaml" +cz_hook_config = { + "repo": "https://github.com/commitizen-tools/commitizen", + "rev": f"v{__version__}", + "hooks": [ + {"id": "commitizen"}, + {"id": "commitizen-branch", "stages": ["push"]}, + ], +} + +expected_config = ( + "[tool.commitizen]\n" + 'name = "cz_conventional_commits"\n' + 'tag_format = "$version"\n' + 'version_scheme = "semver"\n' + 'version = "0.0.1"\n' + "update_changelog_on_bump = true\n" + "major_version_zero = true\n" +) + +EXPECTED_DICT_CONFIG = { + "commitizen": { + "name": "cz_conventional_commits", + "tag_format": "$version", + "version_scheme": "semver", + "version": "0.0.1", + "update_changelog_on_bump": True, + "major_version_zero": True, + } +} + + +def test_init_without_setup_pre_commit_hook(tmpdir, mocker: MockFixture, config): + mocker.patch( + "questionary.select", + side_effect=[ + FakeQuestion("pyproject.toml"), + FakeQuestion("cz_conventional_commits"), + FakeQuestion("commitizen"), + FakeQuestion("semver"), + ], + ) + mocker.patch("questionary.confirm", return_value=FakeQuestion(True)) + mocker.patch("questionary.text", return_value=FakeQuestion("$version")) + # Return None to skip hook installation + mocker.patch("questionary.checkbox", return_value=FakeQuestion(None)) + + with tmpdir.as_cwd(): + commands.Init(config)() + + with open("pyproject.toml", encoding="utf-8") as toml_file: + config_data = toml_file.read() + assert config_data == expected_config + + assert not os.path.isfile(pre_commit_config_filename) + + +def test_init_when_config_already_exists(config, capsys): + # Set config path + path = os.sep.join(["tests", "pyproject.toml"]) + config.add_path(path) + + commands.Init(config)() + captured = capsys.readouterr() + assert captured.out == f"Config file {path} already exists\n" + + +def test_init_without_choosing_tag(config, mocker: MockFixture, tmpdir): + mocker.patch( + "commitizen.commands.init.get_tag_names", return_value=["0.0.2", "0.0.1"] + ) + mocker.patch("commitizen.commands.init.get_latest_tag_name", return_value="0.0.2") + mocker.patch( + "questionary.select", + side_effect=[ + FakeQuestion("pyproject.toml"), + FakeQuestion("cz_conventional_commits"), + FakeQuestion("commitizen"), + FakeQuestion(""), + ], + ) + mocker.patch("questionary.confirm", return_value=FakeQuestion(False)) + mocker.patch("questionary.text", return_value=FakeQuestion("y")) + + with tmpdir.as_cwd(): + with pytest.raises(NoAnswersError): + commands.Init(config)() + + +def test_executed_pre_commit_command(config): + init = commands.Init(config) + expected_cmd = "pre-commit install --hook-type commit-msg --hook-type pre-push" + assert init._gen_pre_commit_cmd(["commit-msg", "pre-push"]) == expected_cmd + + +@pytest.fixture(scope="function") +def pre_commit_installed(mocker: MockFixture): + # Assume the `pre-commit` is installed + mocker.patch( + "commitizen.commands.init.ProjectInfo.is_pre_commit_installed", + return_value=True, + ) + # And installation success (i.e. no exception raised) + mocker.patch( + "commitizen.commands.init.Init._exec_install_pre_commit_hook", + return_value=None, + ) + + +@pytest.fixture(scope="function", params=["pyproject.toml", ".cz.json", ".cz.yaml"]) +def default_choice(request, mocker: MockFixture): + mocker.patch( + "questionary.select", + side_effect=[ + FakeQuestion(request.param), + FakeQuestion("cz_conventional_commits"), + FakeQuestion("commitizen"), + FakeQuestion("semver"), + ], + ) + mocker.patch("questionary.confirm", return_value=FakeQuestion(True)) + mocker.patch("questionary.text", return_value=FakeQuestion("$version")) + mocker.patch( + "questionary.checkbox", + return_value=FakeQuestion(["commit-msg", "pre-push"]), + ) + yield request.param + + +def check_cz_config(config: str): + """ + Check the content of commitizen config is as expected + + Args: + config: The config path + """ + with open(config) as file: + if "json" in config: + assert json.load(file) == EXPECTED_DICT_CONFIG + elif "yaml" in config: + assert yaml.load(file, Loader=yaml.FullLoader) == EXPECTED_DICT_CONFIG + else: + config_data = file.read() + assert config_data == expected_config + + +def check_pre_commit_config(expected: list[dict[str, Any]]): + """ + Check the content of pre-commit config is as expected + """ + with open(pre_commit_config_filename) as pre_commit_file: + pre_commit_config_data = yaml.safe_load(pre_commit_file.read()) + assert pre_commit_config_data == {"repos": expected} + + +@pytest.mark.usefixtures("pre_commit_installed") +class TestPreCommitCases: + def test_no_existing_pre_commit_conifg(_, default_choice, tmpdir, config): + with tmpdir.as_cwd(): + commands.Init(config)() + check_cz_config(default_choice) + check_pre_commit_config([cz_hook_config]) + + def test_empty_pre_commit_config(_, default_choice, tmpdir, config): + with tmpdir.as_cwd(): + p = tmpdir.join(pre_commit_config_filename) + p.write("") + + commands.Init(config)() + check_cz_config(default_choice) + check_pre_commit_config([cz_hook_config]) + + def test_pre_commit_config_without_cz_hook(_, default_choice, tmpdir, config): + existing_hook_config = { + "repo": "https://github.com/pre-commit/pre-commit-hooks", + "rev": "v1.2.3", + "hooks": [{"id", "trailing-whitespace"}], + } + + with tmpdir.as_cwd(): + p = tmpdir.join(pre_commit_config_filename) + p.write(yaml.safe_dump({"repos": [existing_hook_config]})) + + commands.Init(config)() + check_cz_config(default_choice) + check_pre_commit_config([existing_hook_config, cz_hook_config]) + + def test_cz_hook_exists_in_pre_commit_config(_, default_choice, tmpdir, config): + with tmpdir.as_cwd(): + p = tmpdir.join(pre_commit_config_filename) + p.write(yaml.safe_dump({"repos": [cz_hook_config]})) + + commands.Init(config)() + check_cz_config(default_choice) + # check that config is not duplicated + check_pre_commit_config([cz_hook_config]) + + +class TestNoPreCommitInstalled: + def test_pre_commit_not_installed( + _, mocker: MockFixture, config, default_choice, tmpdir + ): + # Assume `pre-commit` is not installed + mocker.patch( + "commitizen.commands.init.ProjectInfo.is_pre_commit_installed", + return_value=False, + ) + with tmpdir.as_cwd(): + with pytest.raises(InitFailedError): + commands.Init(config)() + + def test_pre_commit_exec_failed( + _, mocker: MockFixture, config, default_choice, tmpdir + ): + # Assume `pre-commit` is installed + mocker.patch( + "commitizen.commands.init.ProjectInfo.is_pre_commit_installed", + return_value=True, + ) + # But pre-commit installation will fail + mocker.patch( + "commitizen.commands.init.Init._exec_install_pre_commit_hook", + side_effect=InitFailedError("Mock init failed error."), + ) + with tmpdir.as_cwd(): + with pytest.raises(InitFailedError): + commands.Init(config)() + + +@skip_below_py_3_10 +def test_init_command_shows_description_when_use_help_option( + mocker: MockFixture, capsys, file_regression +): + testargs = ["cz", "init", "--help"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(SystemExit): + cli.main() + + out, _ = capsys.readouterr() + file_regression.check(out, extension=".txt") diff --git a/tests/commands/test_init_command/test_init_command_shows_description_when_use_help_option.txt b/tests/commands/test_init_command/test_init_command_shows_description_when_use_help_option.txt new file mode 100644 index 0000000..0f72042 --- /dev/null +++ b/tests/commands/test_init_command/test_init_command_shows_description_when_use_help_option.txt @@ -0,0 +1,6 @@ +usage: cz init [-h] + +init commitizen configuration + +options: + -h, --help show this help message and exit diff --git a/tests/commands/test_ls_command.py b/tests/commands/test_ls_command.py new file mode 100644 index 0000000..7225d2a --- /dev/null +++ b/tests/commands/test_ls_command.py @@ -0,0 +1,26 @@ +import sys + +import pytest +from pytest_mock import MockerFixture + +from commitizen import cli, commands +from tests.utils import skip_below_py_3_10 + + +def test_list_cz(config, mocker: MockerFixture): + write_mock = mocker.patch("commitizen.out.write") + commands.ListCz(config)() + write_mock.assert_called_once() + + +@skip_below_py_3_10 +def test_ls_command_shows_description_when_use_help_option( + mocker: MockerFixture, capsys, file_regression +): + testargs = ["cz", "ls", "--help"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(SystemExit): + cli.main() + + out, _ = capsys.readouterr() + file_regression.check(out, extension=".txt") diff --git a/tests/commands/test_ls_command/test_ls_command_shows_description_when_use_help_option.txt b/tests/commands/test_ls_command/test_ls_command_shows_description_when_use_help_option.txt new file mode 100644 index 0000000..5fa8fe1 --- /dev/null +++ b/tests/commands/test_ls_command/test_ls_command_shows_description_when_use_help_option.txt @@ -0,0 +1,6 @@ +usage: cz ls [-h] + +show available commitizens + +options: + -h, --help show this help message and exit diff --git a/tests/commands/test_schema_command.py b/tests/commands/test_schema_command.py new file mode 100644 index 0000000..5e57172 --- /dev/null +++ b/tests/commands/test_schema_command.py @@ -0,0 +1,26 @@ +import sys + +import pytest +from pytest_mock import MockerFixture + +from commitizen import cli, commands +from tests.utils import skip_below_py_3_10 + + +def test_schema(config, mocker: MockerFixture): + write_mock = mocker.patch("commitizen.out.write") + commands.Schema(config)() + write_mock.assert_called_once() + + +@skip_below_py_3_10 +def test_schema_command_shows_description_when_use_help_option( + mocker: MockerFixture, capsys, file_regression +): + testargs = ["cz", "schema", "--help"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(SystemExit): + cli.main() + + out, _ = capsys.readouterr() + file_regression.check(out, extension=".txt") diff --git a/tests/commands/test_schema_command/test_schema_command_shows_description_when_use_help_option.txt b/tests/commands/test_schema_command/test_schema_command_shows_description_when_use_help_option.txt new file mode 100644 index 0000000..6666db4 --- /dev/null +++ b/tests/commands/test_schema_command/test_schema_command_shows_description_when_use_help_option.txt @@ -0,0 +1,6 @@ +usage: cz schema [-h] + +show commit schema + +options: + -h, --help show this help message and exit diff --git a/tests/commands/test_version_command.py b/tests/commands/test_version_command.py new file mode 100644 index 0000000..927cf55 --- /dev/null +++ b/tests/commands/test_version_command.py @@ -0,0 +1,122 @@ +import platform +import sys + +import pytest +from pytest_mock import MockerFixture + +from commitizen import cli, commands +from commitizen.__version__ import __version__ +from commitizen.config.base_config import BaseConfig +from tests.utils import skip_below_py_3_10 + + +def test_version_for_showing_project_version(config, capsys): + # No version exist + commands.Version( + config, + {"report": False, "project": True, "commitizen": False, "verbose": False}, + )() + captured = capsys.readouterr() + assert "No project information in this project." in captured.err + + config.settings["version"] = "v0.0.1" + commands.Version( + config, + {"report": False, "project": True, "commitizen": False, "verbose": False}, + )() + captured = capsys.readouterr() + assert "v0.0.1" in captured.out + + +def test_version_for_showing_commitizen_version(config, capsys): + commands.Version( + config, + {"report": False, "project": False, "commitizen": True, "verbose": False}, + )() + captured = capsys.readouterr() + assert f"{__version__}" in captured.out + + # default showing commitizen version + commands.Version( + config, + {"report": False, "project": False, "commitizen": False, "verbose": False}, + )() + captured = capsys.readouterr() + assert f"{__version__}" in captured.out + + +def test_version_for_showing_both_versions(config, capsys): + commands.Version( + config, + {"report": False, "project": False, "commitizen": False, "verbose": True}, + )() + captured = capsys.readouterr() + assert f"Installed Commitizen Version: {__version__}" in captured.out + assert "No project information in this project." in captured.err + + config.settings["version"] = "v0.0.1" + commands.Version( + config, + {"report": False, "project": False, "commitizen": False, "verbose": True}, + )() + captured = capsys.readouterr() + expected_out = ( + f"Installed Commitizen Version: {__version__}\nProject Version: v0.0.1" + ) + assert expected_out in captured.out + + +def test_version_for_showing_commitizen_system_info(config, capsys): + commands.Version( + config, + {"report": True, "project": False, "commitizen": False, "verbose": False}, + )() + captured = capsys.readouterr() + assert f"Commitizen Version: {__version__}" in captured.out + assert f"Python Version: {sys.version}" in captured.out + assert f"Operating System: {platform.system()}" in captured.out + + +@pytest.mark.parametrize("project", (True, False)) +@pytest.mark.usefixtures("tmp_git_project") +def test_version_use_version_provider( + mocker: MockerFixture, + config: BaseConfig, + capsys: pytest.CaptureFixture, + project: bool, +): + version = "0.0.0" + mock = mocker.MagicMock(name="provider") + mock.get_version.return_value = version + get_provider = mocker.patch( + "commitizen.commands.version.get_provider", return_value=mock + ) + + commands.Version( + config, + { + "report": False, + "project": project, + "commitizen": False, + "verbose": not project, + }, + )() + captured = capsys.readouterr() + + assert version in captured.out + get_provider.assert_called_once() + mock.get_version.assert_called_once() + mock.set_version.assert_not_called() + + +@skip_below_py_3_10 +def test_version_command_shows_description_when_use_help_option( + mocker: MockerFixture, capsys, file_regression +): + testargs = ["cz", "version", "--help"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(SystemExit): + cli.main() + + out, _ = capsys.readouterr() + file_regression.check(out, extension=".txt") diff --git a/tests/commands/test_version_command/test_version_command_shows_description_when_use_help_option.txt b/tests/commands/test_version_command/test_version_command_shows_description_when_use_help_option.txt new file mode 100644 index 0000000..c461b10 --- /dev/null +++ b/tests/commands/test_version_command/test_version_command_shows_description_when_use_help_option.txt @@ -0,0 +1,12 @@ +usage: cz version [-h] [-r | -p | -c | -v] + +get the version of the installed commitizen or the current project (default: +installed commitizen) + +options: + -h, --help show this help message and exit + -r, --report get system information for reporting bugs + -p, --project get the version of the current project + -c, --commitizen get the version of the installed commitizen + -v, --verbose get the version of both the installed commitizen and the + current project diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..3d88f19 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,260 @@ +from __future__ import annotations + +import os +import re +import tempfile +from collections.abc import Iterator +from pathlib import Path + +import pytest +from pytest_mock import MockerFixture + +from commitizen import cmd, defaults +from commitizen.changelog_formats import ( + ChangelogFormat, + get_changelog_format, +) +from commitizen.config import BaseConfig +from commitizen.cz import registry +from commitizen.cz.base import BaseCommitizen +from tests.utils import create_file_and_commit + +SIGNER = "GitHub Action" +SIGNER_MAIL = "action@github.com" + + +@pytest.fixture(autouse=True) +def git_sandbox(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + """Ensure git commands are executed without the current user settings""" + # Clear any GIT_ prefixed environment variable + for var in os.environ: + if var.startswith("GIT_"): + monkeypatch.delenv(var) + + # Define a dedicated temporary git config + gitconfig = tmp_path / ".git" / "config" + if not gitconfig.parent.exists(): + gitconfig.parent.mkdir() + + monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(gitconfig)) + + r = cmd.run(f"git config --file {gitconfig} user.name {SIGNER}") + assert r.return_code == 0, r.err + r = cmd.run(f"git config --file {gitconfig} user.email {SIGNER_MAIL}") + assert r.return_code == 0, r.err + + r = cmd.run(f"git config --file {gitconfig} safe.directory '*'") + assert r.return_code == 0, r.err + + r = cmd.run("git config --global init.defaultBranch master") + assert r.return_code == 0, r.err + + +@pytest.fixture +def chdir(tmp_path: Path) -> Iterator[Path]: + cwd = os.getcwd() + os.chdir(tmp_path) + yield tmp_path + os.chdir(cwd) + + +@pytest.fixture(scope="function") +def tmp_git_project(tmpdir): + with tmpdir.as_cwd(): + cmd.run("git init") + + yield tmpdir + + +@pytest.fixture(scope="function") +def tmp_commitizen_project(tmp_git_project): + tmp_commitizen_cfg_file = tmp_git_project.join("pyproject.toml") + tmp_commitizen_cfg_file.write('[tool.commitizen]\nversion="0.1.0"\n') + + yield tmp_git_project + + +@pytest.fixture(scope="function") +def tmp_commitizen_project_initial(tmp_git_project): + def _initial( + config_extra: str | None = None, + version="0.1.0", + initial_commit="feat: new user interface", + ): + with tmp_git_project.as_cwd(): + tmp_commitizen_cfg_file = tmp_git_project.join("pyproject.toml") + tmp_commitizen_cfg_file.write(f'[tool.commitizen]\nversion="{version}"\n') + tmp_version_file = tmp_git_project.join("__version__.py") + tmp_version_file.write(version) + tmp_commitizen_cfg_file = tmp_git_project.join("pyproject.toml") + tmp_version_file_string = str(tmp_version_file).replace("\\", "/") + tmp_commitizen_cfg_file.write( + f"{tmp_commitizen_cfg_file.read()}\n" + f'version_files = ["{tmp_version_file_string}"]\n' + ) + if config_extra: + tmp_commitizen_cfg_file.write(config_extra, mode="a") + create_file_and_commit(initial_commit) + + return tmp_git_project + + yield _initial + + +def _get_gpg_keyid(signer_mail): + _new_key = cmd.run(f"gpg --list-secret-keys {signer_mail}") + _m = re.search( + r"[a-zA-Z0-9 \[\]-_]*\n[ ]*([0-9A-Za-z]*)\n[\na-zA-Z0-9 \[\]-_<>@]*", + _new_key.out, + ) + return _m.group(1) if _m else None + + +@pytest.fixture(scope="function") +def tmp_commitizen_project_with_gpg(tmp_commitizen_project): + # create a temporary GPGHOME to store a temporary keyring. + # Home path must be less than 104 characters + gpg_home = tempfile.TemporaryDirectory(suffix="_cz") + if os.name != "nt": + os.environ["GNUPGHOME"] = gpg_home.name # tempdir = temp keyring + + # create a key (a keyring will be generated within GPUPGHOME) + c = cmd.run( + f"gpg --batch --yes --debug-quick-random --passphrase '' --quick-gen-key '{SIGNER} {SIGNER_MAIL}'" + ) + if c.return_code != 0: + raise Exception(f"gpg keygen failed with err: '{c.err}'") + key_id = _get_gpg_keyid(SIGNER_MAIL) + assert key_id + + # configure git to use gpg signing + cmd.run("git config commit.gpgsign true") + cmd.run(f"git config user.signingkey {key_id}") + + yield tmp_commitizen_project + + +@pytest.fixture() +def config(): + _config = BaseConfig() + _config.settings.update({"name": defaults.DEFAULT_SETTINGS["name"]}) + return _config + + +@pytest.fixture() +def config_path() -> str: + return os.path.join(os.getcwd(), "pyproject.toml") + + +class SemverCommitizen(BaseCommitizen): + """A minimal cz rules used to test changelog and bump. + + Samples: + ``` + minor(users): add email to user + major: removed user profile + patch(deps): updated dependency for security + ``` + """ + + bump_pattern = r"^(patch|minor|major)" + bump_map = { + "major": "MAJOR", + "minor": "MINOR", + "patch": "PATCH", + } + bump_map_major_version_zero = { + "major": "MINOR", + "minor": "MINOR", + "patch": "PATCH", + } + changelog_pattern = r"^(patch|minor|major)" + commit_parser = r"^(?P<change_type>patch|minor|major)(?:\((?P<scope>[^()\r\n]*)\)|\()?:?\s(?P<message>.+)" # noqa + change_type_map = { + "major": "Breaking Changes", + "minor": "Features", + "patch": "Bugs", + } + + def questions(self) -> list: + return [ + { + "type": "list", + "name": "prefix", + "message": "Select the type of change you are committing", + "choices": [ + { + "value": "patch", + "name": "patch: a bug fix", + "key": "p", + }, + { + "value": "minor", + "name": "minor: a new feature, non-breaking", + "key": "m", + }, + { + "value": "major", + "name": "major: a breaking change", + "key": "b", + }, + ], + }, + { + "type": "input", + "name": "subject", + "message": ( + "Write a short and imperative summary of the code changes: (lower case and no period)\n" + ), + }, + ] + + def message(self, answers: dict) -> str: + prefix = answers["prefix"] + subject = answers.get("subject", "default message").trim() + return f"{prefix}: {subject}" + + +@pytest.fixture() +def use_cz_semver(mocker): + new_cz = {**registry, "cz_semver": SemverCommitizen} + mocker.patch.dict("commitizen.cz.registry", new_cz) + + +class MockPlugin(BaseCommitizen): + def questions(self) -> defaults.Questions: + return [] + + def message(self, answers: dict) -> str: + return "" + + +@pytest.fixture +def mock_plugin(mocker: MockerFixture, config: BaseConfig) -> BaseCommitizen: + mock = MockPlugin(config) + mocker.patch("commitizen.factory.commiter_factory", return_value=mock) + return mock + + +SUPPORTED_FORMATS = ("markdown", "textile", "asciidoc", "restructuredtext") + + +@pytest.fixture(params=SUPPORTED_FORMATS) +def changelog_format( + config: BaseConfig, request: pytest.FixtureRequest +) -> ChangelogFormat: + """For tests relying on formats specifics""" + format: str = request.param + config.settings["changelog_format"] = format + if "tmp_commitizen_project" in request.fixturenames: + tmp_commitizen_project = request.getfixturevalue("tmp_commitizen_project") + pyproject = tmp_commitizen_project / "pyproject.toml" + pyproject.write(f'{pyproject.read()}\nchangelog_format = "{format}"\n') + return get_changelog_format(config) + + +@pytest.fixture +def any_changelog_format(config: BaseConfig) -> ChangelogFormat: + """For test not relying on formats specifics, use the default""" + config.settings["changelog_format"] = defaults.CHANGELOG_FORMAT + return get_changelog_format(config) diff --git a/tests/data/inconsistent_version.py b/tests/data/inconsistent_version.py new file mode 100644 index 0000000..4764676 --- /dev/null +++ b/tests/data/inconsistent_version.py @@ -0,0 +1,4 @@ +__title__ = "requests" +__description__ = "Python HTTP for Humans." +__url__ = "http://python-requests.org" +__version__ = "2.10.3" diff --git a/tests/data/multiple_versions_to_update_pyproject.toml b/tests/data/multiple_versions_to_update_pyproject.toml new file mode 100644 index 0000000..de4ead0 --- /dev/null +++ b/tests/data/multiple_versions_to_update_pyproject.toml @@ -0,0 +1,27 @@ +[[package]] +name = "to-update-1" +version = "1.2.9" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "to-update-2" +version = "1.2.9" diff --git a/tests/data/multiple_versions_to_update_pyproject_wo_eol.toml b/tests/data/multiple_versions_to_update_pyproject_wo_eol.toml new file mode 100644 index 0000000..e2746fa --- /dev/null +++ b/tests/data/multiple_versions_to_update_pyproject_wo_eol.toml @@ -0,0 +1,27 @@ +[[package]] +name = "to-update-1" +version = "1.2.9" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "to-update-2" +version = "1.2.9" \ No newline at end of file diff --git a/tests/data/repeated_version_number.json b/tests/data/repeated_version_number.json new file mode 100644 index 0000000..8421026 --- /dev/null +++ b/tests/data/repeated_version_number.json @@ -0,0 +1,7 @@ +{ + "name": "magictool", + "version": "1.2.3", + "dependencies": { + "lodash": "1.2.3" + } +} diff --git a/tests/data/sample_cargo.lock b/tests/data/sample_cargo.lock new file mode 100644 index 0000000..d9dbd79 --- /dev/null +++ b/tests/data/sample_cargo.lock @@ -0,0 +1,11 @@ +[[package]] +name = "textwrap" +version = "1.2.3" + +[[package]] +name = "there-i-fixed-it" +version = "1.2.3" # automatically bumped by Commitizen + +[[package]] +name = "other-project" +version = "1.2.3" diff --git a/tests/data/sample_docker_compose.yaml b/tests/data/sample_docker_compose.yaml new file mode 100644 index 0000000..9da8caf --- /dev/null +++ b/tests/data/sample_docker_compose.yaml @@ -0,0 +1,6 @@ +version: "3.3" + +services: + app: + image: my-repo/my-container:v1.2.3 + command: my-command diff --git a/tests/data/sample_pyproject.toml b/tests/data/sample_pyproject.toml new file mode 100644 index 0000000..9f50155 --- /dev/null +++ b/tests/data/sample_pyproject.toml @@ -0,0 +1,3 @@ +[tool.poetry] +name = "commitizen" +version = "1.2.3" diff --git a/tests/data/sample_version.py b/tests/data/sample_version.py new file mode 100644 index 0000000..4dd4512 --- /dev/null +++ b/tests/data/sample_version.py @@ -0,0 +1,4 @@ +__title__ = "requests" +__description__ = "Python HTTP for Humans." +__url__ = "http://python-requests.org" +__version__ = "1.2.3" diff --git a/tests/providers/conftest.py b/tests/providers/conftest.py new file mode 100644 index 0000000..f73cdb7 --- /dev/null +++ b/tests/providers/conftest.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +import os +from collections.abc import Iterator +from pathlib import Path + +import pytest + + +@pytest.fixture +def chdir(tmp_path: Path) -> Iterator[Path]: + cwd = Path() + os.chdir(tmp_path) + yield tmp_path + os.chdir(cwd) diff --git a/tests/providers/test_base_provider.py b/tests/providers/test_base_provider.py new file mode 100644 index 0000000..482bd69 --- /dev/null +++ b/tests/providers/test_base_provider.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import pytest + +from commitizen.config.base_config import BaseConfig +from commitizen.exceptions import VersionProviderUnknown +from commitizen.providers import get_provider +from commitizen.providers.commitizen_provider import CommitizenProvider + + +def test_default_version_provider_is_commitizen_config(config: BaseConfig): + provider = get_provider(config) + + assert isinstance(provider, CommitizenProvider) + + +def test_raise_for_unknown_provider(config: BaseConfig): + config.settings["version_provider"] = "unknown" + with pytest.raises(VersionProviderUnknown): + get_provider(config) diff --git a/tests/providers/test_cargo_provider.py b/tests/providers/test_cargo_provider.py new file mode 100644 index 0000000..646ef3a --- /dev/null +++ b/tests/providers/test_cargo_provider.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from pathlib import Path +from textwrap import dedent + +import pytest + +from commitizen.config.base_config import BaseConfig +from commitizen.providers import get_provider +from commitizen.providers.cargo_provider import CargoProvider + +CARGO_TOML = """\ +[package] +name = "whatever" +version = "0.1.0" +""" + +CARGO_EXPECTED = """\ +[package] +name = "whatever" +version = "42.1" +""" + +CARGO_WORKSPACE_TOML = """\ +[workspace.package] +name = "whatever" +version = "0.1.0" +""" + +CARGO_WORKSPACE_EXPECTED = """\ +[workspace.package] +name = "whatever" +version = "42.1" +""" + + +@pytest.mark.parametrize( + "content, expected", + ( + (CARGO_TOML, CARGO_EXPECTED), + (CARGO_WORKSPACE_TOML, CARGO_WORKSPACE_EXPECTED), + ), +) +def test_cargo_provider( + config: BaseConfig, + chdir: Path, + content: str, + expected: str, +): + filename = CargoProvider.filename + file = chdir / filename + file.write_text(dedent(content)) + config.settings["version_provider"] = "cargo" + + provider = get_provider(config) + assert isinstance(provider, CargoProvider) + assert provider.get_version() == "0.1.0" + + provider.set_version("42.1") + assert file.read_text() == dedent(expected) diff --git a/tests/providers/test_commitizen_provider.py b/tests/providers/test_commitizen_provider.py new file mode 100644 index 0000000..b8df60d --- /dev/null +++ b/tests/providers/test_commitizen_provider.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from commitizen.config.base_config import BaseConfig +from commitizen.providers.commitizen_provider import CommitizenProvider + +if TYPE_CHECKING: + from pytest_mock import MockerFixture + + +def test_commitizen_provider(config: BaseConfig, mocker: MockerFixture): + config.settings["version"] = "42" + mock = mocker.patch.object(config, "set_key") + + provider = CommitizenProvider(config) + assert provider.get_version() == "42" + + provider.set_version("43.1") + mock.assert_called_once_with("version", "43.1") diff --git a/tests/providers/test_composer_provider.py b/tests/providers/test_composer_provider.py new file mode 100644 index 0000000..45cbc8a --- /dev/null +++ b/tests/providers/test_composer_provider.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from pathlib import Path +from textwrap import dedent + +import pytest + +from commitizen.config.base_config import BaseConfig +from commitizen.providers import get_provider +from commitizen.providers.composer_provider import ComposerProvider + +COMPOSER_JSON = """\ +{ + "name": "whatever", + "version": "0.1.0" +} +""" + +COMPOSER_EXPECTED = """\ +{ + "name": "whatever", + "version": "42.1" +} +""" + + +@pytest.mark.parametrize( + "content, expected", + ((COMPOSER_JSON, COMPOSER_EXPECTED),), +) +def test_composer_provider( + config: BaseConfig, + chdir: Path, + content: str, + expected: str, +): + filename = ComposerProvider.filename + file = chdir / filename + file.write_text(dedent(content)) + config.settings["version_provider"] = "composer" + + provider = get_provider(config) + assert isinstance(provider, ComposerProvider) + assert provider.get_version() == "0.1.0" + + provider.set_version("42.1") + assert file.read_text() == dedent(expected) diff --git a/tests/providers/test_npm_provider.py b/tests/providers/test_npm_provider.py new file mode 100644 index 0000000..bc93999 --- /dev/null +++ b/tests/providers/test_npm_provider.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from pathlib import Path +from textwrap import dedent + +import pytest + +from commitizen.config.base_config import BaseConfig +from commitizen.providers import get_provider +from commitizen.providers.npm_provider import NpmProvider + +NPM_PACKAGE_JSON = """\ +{ + "name": "whatever", + "version": "0.1.0" +} +""" + +NPM_PACKAGE_EXPECTED = """\ +{ + "name": "whatever", + "version": "42.1" +} +""" + +NPM_LOCKFILE_JSON = """\ +{ + "name": "whatever", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "whatever", + "version": "0.1.0" + }, + "someotherpackage": { + "version": "0.1.0" + } + } +} +""" + +NPM_LOCKFILE_EXPECTED = """\ +{ + "name": "whatever", + "version": "42.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "whatever", + "version": "42.1" + }, + "someotherpackage": { + "version": "0.1.0" + } + } +} +""" + + +@pytest.mark.parametrize( + "pkg_shrinkwrap_content, pkg_shrinkwrap_expected", + ((NPM_LOCKFILE_JSON, NPM_LOCKFILE_EXPECTED), (None, None)), +) +@pytest.mark.parametrize( + "pkg_lock_content, pkg_lock_expected", + ((NPM_LOCKFILE_JSON, NPM_LOCKFILE_EXPECTED), (None, None)), +) +def test_npm_provider( + config: BaseConfig, + chdir: Path, + pkg_lock_content: str, + pkg_lock_expected: str, + pkg_shrinkwrap_content: str, + pkg_shrinkwrap_expected: str, +): + pkg = chdir / NpmProvider.package_filename + pkg.write_text(dedent(NPM_PACKAGE_JSON)) + if pkg_lock_content: + pkg_lock = chdir / NpmProvider.lock_filename + pkg_lock.write_text(dedent(pkg_lock_content)) + if pkg_shrinkwrap_content: + pkg_shrinkwrap = chdir / NpmProvider.shrinkwrap_filename + pkg_shrinkwrap.write_text(dedent(pkg_shrinkwrap_content)) + config.settings["version_provider"] = "npm" + + provider = get_provider(config) + assert isinstance(provider, NpmProvider) + assert provider.get_version() == "0.1.0" + + provider.set_version("42.1") + assert pkg.read_text() == dedent(NPM_PACKAGE_EXPECTED) + if pkg_lock_content: + assert pkg_lock.read_text() == dedent(pkg_lock_expected) + if pkg_shrinkwrap_content: + assert pkg_shrinkwrap.read_text() == dedent(pkg_shrinkwrap_expected) diff --git a/tests/providers/test_pep621_provider.py b/tests/providers/test_pep621_provider.py new file mode 100644 index 0000000..16bb479 --- /dev/null +++ b/tests/providers/test_pep621_provider.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from pathlib import Path +from textwrap import dedent + +import pytest + +from commitizen.config.base_config import BaseConfig +from commitizen.providers import get_provider +from commitizen.providers.pep621_provider import Pep621Provider + +PEP621_TOML = """\ +[project] +version = "0.1.0" +""" + +PEP621_EXPECTED = """\ +[project] +version = "42.1" +""" + + +@pytest.mark.parametrize( + "content, expected", + ((PEP621_TOML, PEP621_EXPECTED),), +) +def test_cargo_provider( + config: BaseConfig, + chdir: Path, + content: str, + expected: str, +): + filename = Pep621Provider.filename + file = chdir / filename + file.write_text(dedent(content)) + config.settings["version_provider"] = "pep621" + + provider = get_provider(config) + assert isinstance(provider, Pep621Provider) + assert provider.get_version() == "0.1.0" + + provider.set_version("42.1") + assert file.read_text() == dedent(expected) diff --git a/tests/providers/test_poetry_provider.py b/tests/providers/test_poetry_provider.py new file mode 100644 index 0000000..e26e2a4 --- /dev/null +++ b/tests/providers/test_poetry_provider.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from pathlib import Path +from textwrap import dedent + +import pytest + +from commitizen.config.base_config import BaseConfig +from commitizen.providers import get_provider +from commitizen.providers.poetry_provider import PoetryProvider + +POETRY_TOML = """\ +[tool.poetry] +version = "0.1.0" +""" + +POETRY_EXPECTED = """\ +[tool.poetry] +version = "42.1" +""" + + +@pytest.mark.parametrize( + "content, expected", + ((POETRY_TOML, POETRY_EXPECTED),), +) +def test_cargo_provider( + config: BaseConfig, + chdir: Path, + content: str, + expected: str, +): + filename = PoetryProvider.filename + file = chdir / filename + file.write_text(dedent(content)) + config.settings["version_provider"] = "poetry" + + provider = get_provider(config) + assert isinstance(provider, PoetryProvider) + assert provider.get_version() == "0.1.0" + + provider.set_version("42.1") + assert file.read_text() == dedent(expected) diff --git a/tests/providers/test_scm_provider.py b/tests/providers/test_scm_provider.py new file mode 100644 index 0000000..9d955b2 --- /dev/null +++ b/tests/providers/test_scm_provider.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +import pytest + +from commitizen.config.base_config import BaseConfig +from commitizen.providers import get_provider +from commitizen.providers.scm_provider import ScmProvider +from tests.utils import ( + create_branch, + create_file_and_commit, + create_tag, + merge_branch, + switch_branch, +) + + +@pytest.mark.parametrize( + "tag_format,tag,expected_version", + ( + # If tag_format is $version (the default), version_scheme.parser is used. + # Its DEFAULT_VERSION_PARSER allows a v prefix, but matches PEP440 otherwise. + ("$version", "no-match-because-version-scheme-is-strict", "0.0.0"), + ("$version", "0.1.0", "0.1.0"), + ("$version", "v0.1.0", "0.1.0"), + ("$version", "v-0.1.0", "0.0.0"), + # If tag_format is not None or $version, TAG_FORMAT_REGEXS are used, which are + # much more lenient but require a v prefix. + ("v$version", "v0.1.0", "0.1.0"), + ("v$version", "no-match-because-no-v-prefix", "0.0.0"), + # no match because not a valid version + ("v$version", "v-match-TAG_FORMAT_REGEXS", "0.0.0"), + ("version-$version", "version-0.1.0", "0.1.0"), + ("version-$version", "version-0.1", "0.1"), + ("version-$version", "version-0.1.0rc1", "0.1.0rc1"), + ("v$minor.$major.$patch", "v1.0.0", "0.1.0"), + ("version-$major.$minor.$patch", "version-0.1.0", "0.1.0"), + ("v$major.$minor$prerelease$devrelease", "v1.0rc1", "1.0rc1"), + ("v$major.$minor.$patch$prerelease$devrelease", "v0.1.0", "0.1.0"), + ("v$major.$minor.$patch$prerelease$devrelease", "v0.1.0rc1", "0.1.0rc1"), + ("v$major.$minor.$patch$prerelease$devrelease", "v1.0.0.dev0", "1.0.0.dev0"), + ), +) +@pytest.mark.usefixtures("tmp_git_project") +def test_scm_provider( + config: BaseConfig, tag_format: str, tag: str, expected_version: str +): + create_file_and_commit("test: fake commit") + create_tag(tag) + create_file_and_commit("test: fake commit") + create_tag("should-not-match") + + config.settings["version_provider"] = "scm" + config.settings["tag_format"] = tag_format + + provider = get_provider(config) + assert isinstance(provider, ScmProvider) + actual_version = provider.get_version() + assert actual_version == expected_version + + # Should not fail on set_version() + provider.set_version("43.1") + + +@pytest.mark.usefixtures("tmp_git_project") +def test_scm_provider_default_without_commits_and_tags(config: BaseConfig): + config.settings["version_provider"] = "scm" + + provider = get_provider(config) + assert isinstance(provider, ScmProvider) + assert provider.get_version() == "0.0.0" + + +@pytest.mark.usefixtures("tmp_git_project") +def test_scm_provider_default_with_commits_and_tags(config: BaseConfig): + config.settings["version_provider"] = "scm" + + provider = get_provider(config) + assert isinstance(provider, ScmProvider) + assert provider.get_version() == "0.0.0" + + create_file_and_commit("Initial state") + create_tag("1.0.0") + # create develop + create_branch("develop") + switch_branch("develop") + + # add a feature to develop + create_file_and_commit("develop: add beta feature1") + assert provider.get_version() == "1.0.0" + create_tag("1.1.0b0") + + # create staging + create_branch("staging") + switch_branch("staging") + create_file_and_commit("staging: Starting release candidate") + assert provider.get_version() == "1.1.0b0" + create_tag("1.1.0rc0") + + # add another feature to develop + switch_branch("develop") + create_file_and_commit("develop: add beta feature2") + assert provider.get_version() == "1.1.0b0" + create_tag("1.2.0b0") + + # add a hotfix to master + switch_branch("master") + create_file_and_commit("master: add hotfix") + assert provider.get_version() == "1.0.0" + create_tag("1.0.1") + + # merge the hotfix to staging + switch_branch("staging") + merge_branch("master") + + assert provider.get_version() == "1.1.0rc0" + + +@pytest.mark.usefixtures("tmp_git_project") +def test_scm_provider_detect_legacy_tags(config: BaseConfig): + config.settings["version_provider"] = "scm" + config.settings["tag_format"] = "v${version}" + config.settings["legacy_tag_formats"] = [ + "legacy-${version}", + "old-${version}", + ] + provider = get_provider(config) + + create_file_and_commit("test: fake commit") + create_tag("old-0.4.1") + assert provider.get_version() == "0.4.1" + + create_file_and_commit("test: fake commit") + create_tag("legacy-0.4.2") + assert provider.get_version() == "0.4.2" + + create_file_and_commit("test: fake commit") + create_tag("v0.5.0") + assert provider.get_version() == "0.5.0" diff --git a/tests/providers/test_uv_provider.py b/tests/providers/test_uv_provider.py new file mode 100644 index 0000000..4093709 --- /dev/null +++ b/tests/providers/test_uv_provider.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from commitizen.config.base_config import BaseConfig +from commitizen.providers import get_provider +from commitizen.providers.uv_provider import UvProvider + +if TYPE_CHECKING: + from pytest_regressions.file_regression import FileRegressionFixture + + +PYPROJECT_TOML = """ +[project] +name = "test-uv" +version = "4.2.1" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = ["commitizen==4.2.1"] +""" + +UV_LOCK_SIMPLIFIED = """ +version = 1 +revision = 1 +requires-python = ">=3.13" + +[[package]] +name = "commitizen" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "charset-normalizer" }, + { name = "colorama" }, + { name = "decli" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "questionary" }, + { name = "termcolor" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/a3/77ffc9aee014cbf46c84c9f156a1ddef2d4c7cfb87d567decf2541464245/commitizen-4.2.1.tar.gz", hash = "sha256:5255416f6d6071068159f0b97605777f3e25d00927ff157b7a8d01efeda7b952", size = 50645 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/ce/2f5d8ebe8376991b5f805e9f33d20c7f4c9ca6155bdbda761117dc41dff1/commitizen-4.2.1-py3-none-any.whl", hash = "sha256:a347889e0fe408c3b920a34130d8f35616be3ea8ac6b7b20c5b9aac19762661b", size = 72646 }, +] + +[[package]] +name = "decli" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/a0/a4658f93ecb589f479037b164dc13c68d108b50bf6594e54c820749f97ac/decli-0.6.2.tar.gz", hash = "sha256:36f71eb55fd0093895efb4f416ec32b7f6e00147dda448e3365cf73ceab42d6f", size = 7424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/70/3ea48dc9e958d7d66c44c9944809181f1ca79aaef25703c023b5092d34ff/decli-0.6.2-py3-none-any.whl", hash = "sha256:2fc84106ce9a8f523ed501ca543bdb7e416c064917c12a59ebdc7f311a97b7ed", size = 7854 }, +] + +[[package]] +name = "test-uv" +version = "4.2.1" +source = { virtual = "." } +dependencies = [ + { name = "commitizen" }, +] +""" + + +def test_uv_provider( + config: BaseConfig, tmpdir, file_regression: FileRegressionFixture +): + with tmpdir.as_cwd(): + pyproject_toml_file = tmpdir / UvProvider.filename + pyproject_toml_file.write_text(PYPROJECT_TOML, encoding="utf-8") + + uv_lock_file = tmpdir / UvProvider.lock_filename + uv_lock_file.write_text(UV_LOCK_SIMPLIFIED, encoding="utf-8") + + config.settings["version_provider"] = "uv" + + provider = get_provider(config) + assert isinstance(provider, UvProvider) + assert provider.get_version() == "4.2.1" + + provider.set_version("100.100.100") + assert provider.get_version() == "100.100.100" + + updated_pyproject_toml_content = pyproject_toml_file.read_text(encoding="utf-8") + updated_uv_lock_content = uv_lock_file.read_text(encoding="utf-8") + + for content in (updated_pyproject_toml_content, updated_uv_lock_content): + # updated project version + assert "100.100.100" in content + # commitizen version which was the same as project version and should not be affected + assert "4.2.1" in content + + file_regression.check(updated_pyproject_toml_content, extension=".toml") + file_regression.check(updated_uv_lock_content, extension=".lock") diff --git a/tests/providers/test_uv_provider/test_uv_provider.lock b/tests/providers/test_uv_provider/test_uv_provider.lock new file mode 100644 index 0000000..d353763 --- /dev/null +++ b/tests/providers/test_uv_provider/test_uv_provider.lock @@ -0,0 +1,42 @@ + +version = 1 +revision = 1 +requires-python = ">=3.13" + +[[package]] +name = "commitizen" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "charset-normalizer" }, + { name = "colorama" }, + { name = "decli" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "questionary" }, + { name = "termcolor" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/a3/77ffc9aee014cbf46c84c9f156a1ddef2d4c7cfb87d567decf2541464245/commitizen-4.2.1.tar.gz", hash = "sha256:5255416f6d6071068159f0b97605777f3e25d00927ff157b7a8d01efeda7b952", size = 50645 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/ce/2f5d8ebe8376991b5f805e9f33d20c7f4c9ca6155bdbda761117dc41dff1/commitizen-4.2.1-py3-none-any.whl", hash = "sha256:a347889e0fe408c3b920a34130d8f35616be3ea8ac6b7b20c5b9aac19762661b", size = 72646 }, +] + +[[package]] +name = "decli" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/a0/a4658f93ecb589f479037b164dc13c68d108b50bf6594e54c820749f97ac/decli-0.6.2.tar.gz", hash = "sha256:36f71eb55fd0093895efb4f416ec32b7f6e00147dda448e3365cf73ceab42d6f", size = 7424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/70/3ea48dc9e958d7d66c44c9944809181f1ca79aaef25703c023b5092d34ff/decli-0.6.2-py3-none-any.whl", hash = "sha256:2fc84106ce9a8f523ed501ca543bdb7e416c064917c12a59ebdc7f311a97b7ed", size = 7854 }, +] + +[[package]] +name = "test-uv" +version = "100.100.100" +source = { virtual = "." } +dependencies = [ + { name = "commitizen" }, +] diff --git a/tests/providers/test_uv_provider/test_uv_provider.toml b/tests/providers/test_uv_provider/test_uv_provider.toml new file mode 100644 index 0000000..9fdb6eb --- /dev/null +++ b/tests/providers/test_uv_provider/test_uv_provider.toml @@ -0,0 +1,8 @@ + +[project] +name = "test-uv" +version = "100.100.100" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = ["commitizen==4.2.1"] diff --git a/tests/test_bump_create_commit_message.py b/tests/test_bump_create_commit_message.py new file mode 100644 index 0000000..0002659 --- /dev/null +++ b/tests/test_bump_create_commit_message.py @@ -0,0 +1,160 @@ +import sys +from pathlib import Path +from textwrap import dedent + +import pytest +from pytest_mock import MockFixture + +from commitizen import bump, cli, cmd, exceptions + +conversion = [ + ( + ("1.2.3", "1.3.0", "bump: $current_version -> $new_version [skip ci]"), + "bump: 1.2.3 -> 1.3.0 [skip ci]", + ), + (("1.2.3", "1.3.0", None), "bump: version 1.2.3 โ†’ 1.3.0"), + (("1.2.3", "1.3.0", "release $new_version"), "release 1.3.0"), +] + + +@pytest.mark.parametrize("test_input,expected", conversion) +def test_create_tag(test_input, expected): + current_version, new_version, message_template = test_input + new_tag = bump.create_commit_message(current_version, new_version, message_template) + assert new_tag == expected + + +@pytest.mark.parametrize( + "retry", + ( + pytest.param( + True, + marks=pytest.mark.skipif( + sys.version_info >= (3, 13), + reason="mirrors-prettier is not supported with Python 3.13 or higher", + ), + ), + False, + ), +) +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_pre_commit_changelog(mocker: MockFixture, freezer, retry): + freezer.move_to("2022-04-01") + testargs = ["cz", "bump", "--changelog", "--yes"] + if retry: + testargs.append("--retry") + else: + pytest.xfail("it will fail because pre-commit will reformat CHANGELOG.md") + mocker.patch.object(sys, "argv", testargs) + # Configure prettier as a pre-commit hook + Path(".pre-commit-config.yaml").write_text( + dedent( + """\ + repos: + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.0.3 + hooks: + - id: prettier + stages: [commit] + """ + ) + ) + # Prettier inherits editorconfig + Path(".editorconfig").write_text( + dedent( + """\ + [*] + indent_size = 4 + """ + ) + ) + cmd.run("git add -A") + cmd.run('git commit -m "fix: _test"') + cmd.run("pre-commit install") + cli.main() + # Pre-commit fixed last line adding extra indent and "\" char + assert Path("CHANGELOG.md").read_text() == dedent( + """\ + ## 0.1.1 (2022-04-01) + + ### Fix + + - \\_test + """ + ) + + +@pytest.mark.parametrize("retry", (True, False)) +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_pre_commit_changelog_fails_always(mocker: MockFixture, freezer, retry): + freezer.move_to("2022-04-01") + testargs = ["cz", "bump", "--changelog", "--yes"] + if retry: + testargs.append("--retry") + mocker.patch.object(sys, "argv", testargs) + Path(".pre-commit-config.yaml").write_text( + dedent( + """\ + repos: + - repo: local + hooks: + - id: forbid-changelog + name: changelogs are forbidden + entry: changelogs are forbidden + language: fail + files: CHANGELOG.md + """ + ) + ) + cmd.run("git add -A") + cmd.run('git commit -m "feat: forbid changelogs"') + cmd.run("pre-commit install") + with pytest.raises(exceptions.BumpCommitFailedError): + cli.main() + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_with_build_metadata(mocker: MockFixture, freezer): + def _add_entry(test_str: str, args: list): + Path(test_str).write_text("") + cmd.run("git add -A") + cmd.run(f'git commit -m "fix: test-{test_str}"') + cz_args = ["cz", "bump", "--changelog", "--yes"] + args + mocker.patch.object(sys, "argv", cz_args) + cli.main() + + freezer.move_to("2024-01-01") + + _add_entry("a", ["--build-metadata", "a.b.c"]) + _add_entry("b", []) + _add_entry("c", ["--build-metadata", "alongmetadatastring"]) + _add_entry("d", []) + + # Pre-commit fixed last line adding extra indent and "\" char + assert Path("CHANGELOG.md").read_text() == dedent( + """\ + ## 0.1.4 (2024-01-01) + + ### Fix + + - test-d + + ## 0.1.3+alongmetadatastring (2024-01-01) + + ### Fix + + - test-c + + ## 0.1.2 (2024-01-01) + + ### Fix + + - test-b + + ## 0.1.1+a.b.c (2024-01-01) + + ### Fix + + - test-a + """ + ) diff --git a/tests/test_bump_find_increment.py b/tests/test_bump_find_increment.py new file mode 100644 index 0000000..ff24ff1 --- /dev/null +++ b/tests/test_bump_find_increment.py @@ -0,0 +1,124 @@ +""" +CC: Conventional commits +SVE: Semantic version at the end +""" + +import pytest + +from commitizen import bump +from commitizen.cz.conventional_commits import ConventionalCommitsCz +from commitizen.git import GitCommit + +NONE_INCREMENT_CC = [ + "docs(README): motivation", + "ci: added travis", + "performance. Remove or disable the reimplemented linters", + "refactor that how this line starts", +] + +PATCH_INCREMENTS_CC = [ + "fix(setup.py): future is now required for every python version", + "docs(README): motivation", +] + +MINOR_INCREMENTS_CC = [ + "feat(cli): added version", + "docs(README): motivation", + "fix(setup.py): future is now required for every python version", + "perf: app is much faster", + "refactor: app is much faster", +] + +MAJOR_INCREMENTS_BREAKING_CHANGE_CC = [ + "feat(cli): added version", + "docs(README): motivation", + "BREAKING CHANGE: `extends` key in config file is now used for extending other config files", # noqa + "fix(setup.py): future is now required for every python version", +] + +MAJOR_INCREMENTS_BREAKING_CHANGE_ALT_CC = [ + "feat(cli): added version", + "docs(README): motivation", + "BREAKING-CHANGE: `extends` key in config file is now used for extending other config files", # noqa + "fix(setup.py): future is now required for every python version", +] + +MAJOR_INCREMENTS_EXCLAMATION_CC = [ + "feat(cli)!: added version", + "docs(README): motivation", + "fix(setup.py): future is now required for every python version", +] + +MAJOR_INCREMENTS_EXCLAMATION_CC_SAMPLE_2 = [ + "feat(pipeline)!: some text with breaking change" +] + +MAJOR_INCREMENTS_EXCLAMATION_OTHER_TYPE_CC = [ + "chore!: drop support for Python 3.9", + "docs(README): motivation", + "fix(setup.py): future is now required for every python version", +] + +MAJOR_INCREMENTS_EXCLAMATION_OTHER_TYPE_WITH_SCOPE_CC = [ + "chore(deps)!: drop support for Python 3.9", + "docs(README): motivation", + "fix(setup.py): future is now required for every python version", +] + +PATCH_INCREMENTS_SVE = ["readme motivation PATCH", "fix setup.py PATCH"] + +MINOR_INCREMENTS_SVE = [ + "readme motivation PATCH", + "fix setup.py PATCH", + "added version to cli MINOR", +] + +MAJOR_INCREMENTS_SVE = [ + "readme motivation PATCH", + "fix setup.py PATCH", + "added version to cli MINOR", + "extends key is used for other config files MAJOR", +] + +semantic_version_pattern = r"(MAJOR|MINOR|PATCH)" +semantic_version_map = {"MAJOR": "MAJOR", "MINOR": "MINOR", "PATCH": "PATCH"} + + +@pytest.mark.parametrize( + "messages, expected_type", + ( + (PATCH_INCREMENTS_CC, "PATCH"), + (MINOR_INCREMENTS_CC, "MINOR"), + (MAJOR_INCREMENTS_BREAKING_CHANGE_CC, "MAJOR"), + (MAJOR_INCREMENTS_BREAKING_CHANGE_ALT_CC, "MAJOR"), + (MAJOR_INCREMENTS_EXCLAMATION_OTHER_TYPE_CC, "MAJOR"), + (MAJOR_INCREMENTS_EXCLAMATION_OTHER_TYPE_WITH_SCOPE_CC, "MAJOR"), + (MAJOR_INCREMENTS_EXCLAMATION_CC, "MAJOR"), + (MAJOR_INCREMENTS_EXCLAMATION_CC_SAMPLE_2, "MAJOR"), + (NONE_INCREMENT_CC, None), + ), +) +def test_find_increment(messages, expected_type): + commits = [GitCommit(rev="test", title=message) for message in messages] + increment_type = bump.find_increment( + commits, + regex=ConventionalCommitsCz.bump_pattern, + increments_map=ConventionalCommitsCz.bump_map, + ) + assert increment_type == expected_type + + +@pytest.mark.parametrize( + "messages, expected_type", + ( + (PATCH_INCREMENTS_SVE, "PATCH"), + (MINOR_INCREMENTS_SVE, "MINOR"), + (MAJOR_INCREMENTS_SVE, "MAJOR"), + ), +) +def test_find_increment_sve(messages, expected_type): + commits = [GitCommit(rev="test", title=message) for message in messages] + increment_type = bump.find_increment( + commits, regex=semantic_version_pattern, increments_map=semantic_version_map + ) + assert increment_type == expected_type diff --git a/tests/test_bump_hooks.py b/tests/test_bump_hooks.py new file mode 100644 index 0000000..70ed7fe --- /dev/null +++ b/tests/test_bump_hooks.py @@ -0,0 +1,42 @@ +import os +from unittest.mock import call + +import pytest +from pytest_mock import MockFixture + +from commitizen import cmd, hooks +from commitizen.exceptions import RunHookError + + +def test_run(mocker: MockFixture): + bump_hooks = ["pre_bump_hook", "pre_bump_hook_1"] + + cmd_run_mock = mocker.Mock() + cmd_run_mock.return_value.return_code = 0 + mocker.patch.object(cmd, "run", cmd_run_mock) + + hooks.run(bump_hooks) + + cmd_run_mock.assert_has_calls( + [ + call("pre_bump_hook", env=dict(os.environ)), + call("pre_bump_hook_1", env=dict(os.environ)), + ] + ) + + +def test_run_error(mocker: MockFixture): + bump_hooks = ["pre_bump_hook", "pre_bump_hook_1"] + + cmd_run_mock = mocker.Mock() + cmd_run_mock.return_value.return_code = 1 + mocker.patch.object(cmd, "run", cmd_run_mock) + + with pytest.raises(RunHookError): + hooks.run(bump_hooks) + + +def test_format_env(): + result = hooks._format_env("TEST_", {"foo": "bar", "bar": "baz"}) + assert "TEST_FOO" in result and result["TEST_FOO"] == "bar" + assert "TEST_BAR" in result and result["TEST_BAR"] == "baz" diff --git a/tests/test_bump_normalize_tag.py b/tests/test_bump_normalize_tag.py new file mode 100644 index 0000000..895acbd --- /dev/null +++ b/tests/test_bump_normalize_tag.py @@ -0,0 +1,23 @@ +import pytest + +from commitizen.tags import TagRules + +conversion = [ + (("1.2.3", "v$version"), "v1.2.3"), + (("1.2.3a2", "v$version"), "v1.2.3a2"), + (("1.2.3b2", "v$version"), "v1.2.3b2"), + (("1.2.3", "ver$major.$minor.$patch"), "ver1.2.3"), + (("1.2.3a0", "ver$major.$minor.$patch.$prerelease"), "ver1.2.3.a0"), + (("1.2.3rc2", "$major.$minor.$patch.$prerelease-majestic"), "1.2.3.rc2-majestic"), + (("1.2.3+1.0.0", "v$version"), "v1.2.3+1.0.0"), + (("1.2.3+1.0.0", "v$version-local"), "v1.2.3+1.0.0-local"), + (("1.2.3+1.0.0", "ver$major.$minor.$patch"), "ver1.2.3"), +] + + +@pytest.mark.parametrize("test_input,expected", conversion) +def test_create_tag(test_input, expected): + version, format = test_input + rules = TagRules() + new_tag = rules.normalize_tag(version, format) + assert new_tag == expected diff --git a/tests/test_bump_update_version_in_files.py b/tests/test_bump_update_version_in_files.py new file mode 100644 index 0000000..850b59c --- /dev/null +++ b/tests/test_bump_update_version_in_files.py @@ -0,0 +1,227 @@ +from pathlib import Path +from shutil import copyfile + +import pytest + +from commitizen import bump +from commitizen.exceptions import CurrentVersionNotFoundError + +MULTIPLE_VERSIONS_INCREASE_STRING = 'version = "1.2.9"\n' * 30 +MULTIPLE_VERSIONS_REDUCE_STRING = 'version = "1.2.10"\n' * 30 + +TESTING_FILE_PREFIX = "tests/data" + + +def _copy_sample_file_to_tmpdir( + tmp_path: Path, source_filename: str, dest_filename: str +) -> Path: + tmp_file = tmp_path / dest_filename + copyfile(f"{TESTING_FILE_PREFIX}/{source_filename}", tmp_file) + return tmp_file + + +@pytest.fixture(scope="function") +def commitizen_config_file(tmpdir): + return _copy_sample_file_to_tmpdir( + tmpdir, "sample_pyproject.toml", "pyproject.toml" + ) + + +@pytest.fixture(scope="function") +def python_version_file(tmpdir, request): + return _copy_sample_file_to_tmpdir(tmpdir, "sample_version.py", "__version__.py") + + +@pytest.fixture(scope="function") +def inconsistent_python_version_file(tmpdir): + return _copy_sample_file_to_tmpdir( + tmpdir, "inconsistent_version.py", "__version__.py" + ) + + +@pytest.fixture(scope="function") +def random_location_version_file(tmpdir): + return _copy_sample_file_to_tmpdir(tmpdir, "sample_cargo.lock", "Cargo.lock") + + +@pytest.fixture(scope="function") +def version_repeated_file(tmpdir): + return _copy_sample_file_to_tmpdir( + tmpdir, "repeated_version_number.json", "package.json" + ) + + +@pytest.fixture(scope="function") +def docker_compose_file(tmpdir): + return _copy_sample_file_to_tmpdir( + tmpdir, "sample_docker_compose.yaml", "docker-compose.yaml" + ) + + +@pytest.fixture( + scope="function", + params=( + "multiple_versions_to_update_pyproject.toml", + "multiple_versions_to_update_pyproject_wo_eol.toml", + ), + ids=("with_eol", "without_eol"), +) +def multiple_versions_to_update_poetry_lock(tmpdir, request): + return _copy_sample_file_to_tmpdir(tmpdir, request.param, "pyproject.toml") + + +@pytest.fixture(scope="function") +def multiple_versions_increase_string(tmpdir): + tmp_file = tmpdir.join("anyfile") + tmp_file.write(MULTIPLE_VERSIONS_INCREASE_STRING) + return str(tmp_file) + + +@pytest.fixture(scope="function") +def multiple_versions_reduce_string(tmpdir): + tmp_file = tmpdir.join("anyfile") + tmp_file.write(MULTIPLE_VERSIONS_REDUCE_STRING) + return str(tmp_file) + + +@pytest.fixture(scope="function") +def version_files( + commitizen_config_file, + python_version_file, + version_repeated_file, + docker_compose_file, +): + return ( + commitizen_config_file, + python_version_file, + version_repeated_file, + docker_compose_file, + ) + + +def test_update_version_in_files(version_files, file_regression): + old_version = "1.2.3" + new_version = "2.0.0" + bump.update_version_in_files( + old_version, new_version, version_files, encoding="utf-8" + ) + + file_contents = "" + for filepath in version_files: + with open(filepath, encoding="utf-8") as f: + file_contents += f.read() + file_regression.check(file_contents, extension=".txt") + + +def test_partial_update_of_file(version_repeated_file, file_regression): + old_version = "1.2.3" + new_version = "2.0.0" + regex = "version" + location = f"{version_repeated_file}:{regex}" + + bump.update_version_in_files(old_version, new_version, [location], encoding="utf-8") + with open(version_repeated_file, encoding="utf-8") as f: + file_regression.check(f.read(), extension=".json") + + +def test_random_location(random_location_version_file, file_regression): + old_version = "1.2.3" + new_version = "2.0.0" + location = f"{random_location_version_file}:version.+Commitizen" + + bump.update_version_in_files(old_version, new_version, [location], encoding="utf-8") + with open(random_location_version_file, encoding="utf-8") as f: + file_regression.check(f.read(), extension=".lock") + + +def test_duplicates_are_change_with_no_regex( + random_location_version_file, file_regression +): + old_version = "1.2.3" + new_version = "2.0.0" + location = f"{random_location_version_file}:version" + + bump.update_version_in_files(old_version, new_version, [location], encoding="utf-8") + with open(random_location_version_file, encoding="utf-8") as f: + file_regression.check(f.read(), extension=".lock") + + +def test_version_bump_increase_string_length( + multiple_versions_increase_string, file_regression +): + old_version = "1.2.9" + new_version = "1.2.10" + location = f"{multiple_versions_increase_string}:version" + + bump.update_version_in_files(old_version, new_version, [location], encoding="utf-8") + with open(multiple_versions_increase_string, encoding="utf-8") as f: + file_regression.check(f.read(), extension=".txt") + + +def test_version_bump_reduce_string_length( + multiple_versions_reduce_string, file_regression +): + old_version = "1.2.10" + new_version = "2.0.0" + location = f"{multiple_versions_reduce_string}:version" + + bump.update_version_in_files(old_version, new_version, [location], encoding="utf-8") + with open(multiple_versions_reduce_string, encoding="utf-8") as f: + file_regression.check(f.read(), extension=".txt") + + +def test_file_version_inconsistent_error( + commitizen_config_file, inconsistent_python_version_file, version_repeated_file +): + version_files = [ + commitizen_config_file, + inconsistent_python_version_file, + version_repeated_file, + ] + old_version = "1.2.3" + new_version = "2.0.0" + with pytest.raises(CurrentVersionNotFoundError) as excinfo: + bump.update_version_in_files( + old_version, + new_version, + version_files, + check_consistency=True, + encoding="utf-8", + ) + + expected_msg = ( + f"Current version 1.2.3 is not found in {inconsistent_python_version_file}.\n" + "The version defined in commitizen configuration and the ones in " + "version_files are possibly inconsistent." + ) + assert expected_msg in str(excinfo.value) + + +def test_multiplt_versions_to_bump( + multiple_versions_to_update_poetry_lock, file_regression +): + old_version = "1.2.9" + new_version = "1.2.10" + location = f"{multiple_versions_to_update_poetry_lock}:version" + + bump.update_version_in_files(old_version, new_version, [location], encoding="utf-8") + with open(multiple_versions_to_update_poetry_lock, encoding="utf-8") as f: + file_regression.check(f.read(), extension=".toml") + + +def test_update_version_in_globbed_files(commitizen_config_file, file_regression): + old_version = "1.2.3" + new_version = "2.0.0" + other = commitizen_config_file.dirpath("other.toml") + print(commitizen_config_file, other) + copyfile(commitizen_config_file, other) + + # Prepend full ppath as test assume absolute paths or cwd-relative + version_files = [commitizen_config_file.dirpath("*.toml")] + + bump.update_version_in_files( + old_version, new_version, version_files, encoding="utf-8" + ) + + for file in commitizen_config_file, other: + file_regression.check(file.read_text("utf-8"), extension=".toml") diff --git a/tests/test_bump_update_version_in_files/test_duplicates_are_change_with_no_regex.lock b/tests/test_bump_update_version_in_files/test_duplicates_are_change_with_no_regex.lock new file mode 100644 index 0000000..8fe75b8 --- /dev/null +++ b/tests/test_bump_update_version_in_files/test_duplicates_are_change_with_no_regex.lock @@ -0,0 +1,11 @@ +[[package]] +name = "textwrap" +version = "2.0.0" + +[[package]] +name = "there-i-fixed-it" +version = "2.0.0" # automatically bumped by Commitizen + +[[package]] +name = "other-project" +version = "2.0.0" diff --git a/tests/test_bump_update_version_in_files/test_multiple_versions_to_bump_with_eol_.toml b/tests/test_bump_update_version_in_files/test_multiple_versions_to_bump_with_eol_.toml new file mode 100644 index 0000000..f279eb4 --- /dev/null +++ b/tests/test_bump_update_version_in_files/test_multiple_versions_to_bump_with_eol_.toml @@ -0,0 +1,27 @@ +[[package]] +name = "to-update-1" +version = "1.2.10" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "to-update-2" +version = "1.2.10" diff --git a/tests/test_bump_update_version_in_files/test_multiple_versions_to_bump_without_eol_.toml b/tests/test_bump_update_version_in_files/test_multiple_versions_to_bump_without_eol_.toml new file mode 100644 index 0000000..47092b9 --- /dev/null +++ b/tests/test_bump_update_version_in_files/test_multiple_versions_to_bump_without_eol_.toml @@ -0,0 +1,27 @@ +[[package]] +name = "to-update-1" +version = "1.2.10" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "to-update-2" +version = "1.2.10" \ No newline at end of file diff --git a/tests/test_bump_update_version_in_files/test_multiplt_versions_to_bump_with_eol_.toml b/tests/test_bump_update_version_in_files/test_multiplt_versions_to_bump_with_eol_.toml new file mode 100644 index 0000000..f279eb4 --- /dev/null +++ b/tests/test_bump_update_version_in_files/test_multiplt_versions_to_bump_with_eol_.toml @@ -0,0 +1,27 @@ +[[package]] +name = "to-update-1" +version = "1.2.10" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "to-update-2" +version = "1.2.10" diff --git a/tests/test_bump_update_version_in_files/test_multiplt_versions_to_bump_without_eol_.toml b/tests/test_bump_update_version_in_files/test_multiplt_versions_to_bump_without_eol_.toml new file mode 100644 index 0000000..47092b9 --- /dev/null +++ b/tests/test_bump_update_version_in_files/test_multiplt_versions_to_bump_without_eol_.toml @@ -0,0 +1,27 @@ +[[package]] +name = "to-update-1" +version = "1.2.10" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "not-to-update" +version = "1.3.3" + +[[package]] +name = "to-update-2" +version = "1.2.10" \ No newline at end of file diff --git a/tests/test_bump_update_version_in_files/test_partial_update_of_file.json b/tests/test_bump_update_version_in_files/test_partial_update_of_file.json new file mode 100644 index 0000000..59224bc --- /dev/null +++ b/tests/test_bump_update_version_in_files/test_partial_update_of_file.json @@ -0,0 +1,7 @@ +{ + "name": "magictool", + "version": "2.0.0", + "dependencies": { + "lodash": "1.2.3" + } +} diff --git a/tests/test_bump_update_version_in_files/test_random_location.lock b/tests/test_bump_update_version_in_files/test_random_location.lock new file mode 100644 index 0000000..20dfe7f --- /dev/null +++ b/tests/test_bump_update_version_in_files/test_random_location.lock @@ -0,0 +1,11 @@ +[[package]] +name = "textwrap" +version = "1.2.3" + +[[package]] +name = "there-i-fixed-it" +version = "2.0.0" # automatically bumped by Commitizen + +[[package]] +name = "other-project" +version = "1.2.3" diff --git a/tests/test_bump_update_version_in_files/test_update_version_in_files.txt b/tests/test_bump_update_version_in_files/test_update_version_in_files.txt new file mode 100644 index 0000000..c4e527a --- /dev/null +++ b/tests/test_bump_update_version_in_files/test_update_version_in_files.txt @@ -0,0 +1,20 @@ +[tool.poetry] +name = "commitizen" +version = "2.0.0" +__title__ = "requests" +__description__ = "Python HTTP for Humans." +__url__ = "http://python-requests.org" +__version__ = "2.0.0" +{ + "name": "magictool", + "version": "2.0.0", + "dependencies": { + "lodash": "2.0.0" + } +} +version: "3.3" + +services: + app: + image: my-repo/my-container:v2.0.0 + command: my-command diff --git a/tests/test_bump_update_version_in_files/test_update_version_in_globbed_files.toml b/tests/test_bump_update_version_in_files/test_update_version_in_globbed_files.toml new file mode 100644 index 0000000..bf82cfe --- /dev/null +++ b/tests/test_bump_update_version_in_files/test_update_version_in_globbed_files.toml @@ -0,0 +1,3 @@ +[tool.poetry] +name = "commitizen" +version = "2.0.0" diff --git a/tests/test_bump_update_version_in_files/test_version_bump_increase_string_length.txt b/tests/test_bump_update_version_in_files/test_version_bump_increase_string_length.txt new file mode 100644 index 0000000..4b6d6d6 --- /dev/null +++ b/tests/test_bump_update_version_in_files/test_version_bump_increase_string_length.txt @@ -0,0 +1,30 @@ +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" +version = "1.2.10" diff --git a/tests/test_bump_update_version_in_files/test_version_bump_reduce_string_length.txt b/tests/test_bump_update_version_in_files/test_version_bump_reduce_string_length.txt new file mode 100644 index 0000000..8e619de --- /dev/null +++ b/tests/test_bump_update_version_in_files/test_version_bump_reduce_string_length.txt @@ -0,0 +1,30 @@ +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" +version = "2.0.0" diff --git a/tests/test_changelog.py b/tests/test_changelog.py new file mode 100644 index 0000000..df42b82 --- /dev/null +++ b/tests/test_changelog.py @@ -0,0 +1,1633 @@ +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Optional + +import pytest +from jinja2 import FileSystemLoader + +from commitizen import changelog, git +from commitizen.changelog_formats import ChangelogFormat +from commitizen.cz.conventional_commits.conventional_commits import ( + ConventionalCommitsCz, +) +from commitizen.exceptions import InvalidConfigurationError +from commitizen.version_schemes import Pep440 + +COMMITS_DATA: list[dict[str, Any]] = [ + { + "rev": "141ee441c9c9da0809c554103a558eb17c30ed17", + "parents": ["6c4948501031b7d6405b54b21d3d635827f9421b"], + "title": "bump: version 1.1.1 โ†’ 1.2.0", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "6c4948501031b7d6405b54b21d3d635827f9421b", + "parents": ["ddd220ad515502200fe2dde443614c1075d26238"], + "title": "docs: how to create custom bumps", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "ddd220ad515502200fe2dde443614c1075d26238", + "parents": ["ad17acff2e3a2e141cbc3c6efd7705e4e6de9bfc"], + "title": "feat: custom cz plugins now support bumping version", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "ad17acff2e3a2e141cbc3c6efd7705e4e6de9bfc", + "parents": ["56c8a8da84e42b526bcbe130bd194306f7c7e813"], + "title": "docs: added bump gif", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "56c8a8da84e42b526bcbe130bd194306f7c7e813", + "parents": ["74c6134b1b2e6bb8b07ed53410faabe99b204f36"], + "title": "bump: version 1.1.0 โ†’ 1.1.1", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "74c6134b1b2e6bb8b07ed53410faabe99b204f36", + "parents": ["cbc7b5f22c4e74deff4bc92d14e19bd93524711e"], + "title": "refactor: changed stdout statements", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "cbc7b5f22c4e74deff4bc92d14e19bd93524711e", + "parents": ["1ba46f2a63cb9d6e7472eaece21528c8cd28b118"], + "title": "fix(bump): commit message now fits better with semver", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "1ba46f2a63cb9d6e7472eaece21528c8cd28b118", + "parents": ["c35dbffd1bb98bb0b3d1593797e79d1c3366af8f"], + "title": "fix: conventional commit 'breaking change' in body instead of title", + "body": "closes #16", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "c35dbffd1bb98bb0b3d1593797e79d1c3366af8f", + "parents": ["25313397a4ac3dc5b5c986017bee2a614399509d"], + "title": "refactor(schema): command logic removed from commitizen base", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "25313397a4ac3dc5b5c986017bee2a614399509d", + "parents": ["d2f13ac41b4e48995b3b619d931c82451886e6ff"], + "title": "refactor(info): command logic removed from commitizen base", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "d2f13ac41b4e48995b3b619d931c82451886e6ff", + "parents": ["d839e317e5b26671b010584ad8cc6bf362400fa1"], + "title": "refactor(example): command logic removed from commitizen base", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "d839e317e5b26671b010584ad8cc6bf362400fa1", + "parents": ["12d0e65beda969f7983c444ceedc2a01584f4e08"], + "title": "refactor(commit): moved most of the commit logic to the commit command", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "12d0e65beda969f7983c444ceedc2a01584f4e08", + "parents": ["fb4c85abe51c228e50773e424cbd885a8b6c610d"], + "title": "docs(README): updated documentation url)", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "fb4c85abe51c228e50773e424cbd885a8b6c610d", + "parents": ["17efb44d2cd16f6621413691a543e467c7d2dda6"], + "title": "docs: mkdocs documentation", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "17efb44d2cd16f6621413691a543e467c7d2dda6", + "parents": ["6012d9eecfce8163d75c8fff179788e9ad5347da"], + "title": "Bump version 1.0.0 โ†’ 1.1.0", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "6012d9eecfce8163d75c8fff179788e9ad5347da", + "parents": ["0c7fb0ca0168864dfc55d83c210da57771a18319"], + "title": "test: fixed issues with conf", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "0c7fb0ca0168864dfc55d83c210da57771a18319", + "parents": ["cb1dd2019d522644da5bdc2594dd6dee17122d7f"], + "title": "docs(README): some new information about bump", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "cb1dd2019d522644da5bdc2594dd6dee17122d7f", + "parents": ["9c7450f85df6bf6be508e79abf00855a30c3c73c"], + "title": "feat: new working bump command", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "9c7450f85df6bf6be508e79abf00855a30c3c73c", + "parents": ["9f3af3772baab167e3fd8775d37f041440184251"], + "title": "feat: create version tag", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "9f3af3772baab167e3fd8775d37f041440184251", + "parents": ["b0d6a3defbfde14e676e7eb34946409297d0221b"], + "title": "docs: added new changelog", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "b0d6a3defbfde14e676e7eb34946409297d0221b", + "parents": ["d630d07d912e420f0880551f3ac94e933f9d3beb"], + "title": "feat: update given files with new version", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "d630d07d912e420f0880551f3ac94e933f9d3beb", + "parents": ["1792b8980c58787906dbe6836f93f31971b1ec2d"], + "title": "fix: removed all from commit", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "1792b8980c58787906dbe6836f93f31971b1ec2d", + "parents": ["52def1ea3555185ba4b936b463311949907e31ec"], + "title": "feat(config): new set key, used to set version to cfg", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "52def1ea3555185ba4b936b463311949907e31ec", + "parents": ["3127e05077288a5e2b62893345590bf1096141b7"], + "title": "feat: support for pyproject.toml", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "3127e05077288a5e2b62893345590bf1096141b7", + "parents": ["fd480ed90a80a6ffa540549408403d5b60d0e90c"], + "title": "feat: first semantic version bump implementation", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "fd480ed90a80a6ffa540549408403d5b60d0e90c", + "parents": ["e4840a059731c0bf488381ffc77e989e85dd81ad"], + "title": "fix: fix config file not working", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "e4840a059731c0bf488381ffc77e989e85dd81ad", + "parents": ["aa44a92d68014d0da98965c0c2cb8c07957d4362"], + "title": "refactor: added commands folder, better integration with decli", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "aa44a92d68014d0da98965c0c2cb8c07957d4362", + "parents": ["58bb709765380dbd46b74ce6e8978515764eb955"], + "title": "Bump version: 1.0.0b2 โ†’ 1.0.0", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "58bb709765380dbd46b74ce6e8978515764eb955", + "parents": ["97afb0bb48e72b6feca793091a8a23c706693257"], + "title": "docs(README): new badges", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "97afb0bb48e72b6feca793091a8a23c706693257", + "parents": [ + "9cecb9224aa7fa68d4afeac37eba2a25770ef251", + "e004a90b81ea5b374f118759bce5951202d03d69", + ], + "title": "Merge pull request #10 from Woile/feat/decli", + "body": "Feat/decli", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "9cecb9224aa7fa68d4afeac37eba2a25770ef251", + "parents": ["f5781d1a2954d71c14ade2a6a1a95b91310b2577"], + "title": "style: black to files", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "f5781d1a2954d71c14ade2a6a1a95b91310b2577", + "parents": ["80105fb3c6d45369bc0cbf787bd329fba603864c"], + "title": "ci: added travis", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "80105fb3c6d45369bc0cbf787bd329fba603864c", + "parents": ["a96008496ffefb6b1dd9b251cb479eac6a0487f7"], + "title": "refactor: removed delegator, added decli and many tests", + "body": "BREAKING CHANGE: API is stable", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "a96008496ffefb6b1dd9b251cb479eac6a0487f7", + "parents": ["aab33d13110f26604fb786878856ec0b9e5fc32b"], + "title": "docs: updated test command", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "aab33d13110f26604fb786878856ec0b9e5fc32b", + "parents": ["b73791563d2f218806786090fb49ef70faa51a3a"], + "title": "Bump version: 1.0.0b1 โ†’ 1.0.0b2", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "b73791563d2f218806786090fb49ef70faa51a3a", + "parents": ["7aa06a454fb717408b3657faa590731fb4ab3719"], + "title": "docs(README): updated to reflect current state", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "7aa06a454fb717408b3657faa590731fb4ab3719", + "parents": [ + "7c7e96b723c2aaa1aec3a52561f680adf0b60e97", + "9589a65880016996cff156b920472b9d28d771ca", + ], + "title": "Merge pull request #9 from Woile/dev", + "body": "feat: py3 only, tests and conventional commits 1.0", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "7c7e96b723c2aaa1aec3a52561f680adf0b60e97", + "parents": ["ed830019581c83ba633bfd734720e6758eca6061"], + "title": "Bump version: 0.9.11 โ†’ 1.0.0b1", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "ed830019581c83ba633bfd734720e6758eca6061", + "parents": ["c52eca6f74f844ab3ffbde61d98ef96071e132b7"], + "title": "feat: py3 only, tests and conventional commits 1.0", + "body": "more tests\npyproject instead of Pipfile\nquestionary instead of whaaaaat (promptkit 2.0.0 support)", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "c52eca6f74f844ab3ffbde61d98ef96071e132b7", + "parents": ["0326652b2657083929507ee66d4d1a0899e861ba"], + "title": "Bump version: 0.9.10 โ†’ 0.9.11", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "0326652b2657083929507ee66d4d1a0899e861ba", + "parents": ["b3f89892222340150e32631ae6b7aab65230036f"], + "title": "fix(config): load config reads in order without failing if there is no commitizen section", + "body": "Closes #8", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "b3f89892222340150e32631ae6b7aab65230036f", + "parents": ["5e837bf8ef0735193597372cd2d85e31a8f715b9"], + "title": "Bump version: 0.9.9 โ†’ 0.9.10", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "5e837bf8ef0735193597372cd2d85e31a8f715b9", + "parents": ["684e0259cc95c7c5e94854608cd3dcebbd53219e"], + "title": "fix: parse scope (this is my punishment for not having tests)", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "684e0259cc95c7c5e94854608cd3dcebbd53219e", + "parents": ["ca38eac6ff09870851b5c76a6ff0a2a8e5ecda15"], + "title": "Bump version: 0.9.8 โ†’ 0.9.9", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "ca38eac6ff09870851b5c76a6ff0a2a8e5ecda15", + "parents": ["64168f18d4628718c49689ee16430549e96c5d4b"], + "title": "fix: parse scope empty", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "64168f18d4628718c49689ee16430549e96c5d4b", + "parents": ["9d4def716ef235a1fa5ae61614366423fbc8256f"], + "title": "Bump version: 0.9.7 โ†’ 0.9.8", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "9d4def716ef235a1fa5ae61614366423fbc8256f", + "parents": ["33b0bf1a0a4dc60aac45ed47476d2e5473add09e"], + "title": "fix(scope): parse correctly again", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "33b0bf1a0a4dc60aac45ed47476d2e5473add09e", + "parents": ["696885e891ec35775daeb5fec3ba2ab92c2629e1"], + "title": "Bump version: 0.9.6 โ†’ 0.9.7", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "696885e891ec35775daeb5fec3ba2ab92c2629e1", + "parents": ["bef4a86761a3bda309c962bae5d22ce9b57119e4"], + "title": "fix(scope): parse correctly", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "bef4a86761a3bda309c962bae5d22ce9b57119e4", + "parents": ["72472efb80f08ee3fd844660afa012c8cb256e4b"], + "title": "Bump version: 0.9.5 โ†’ 0.9.6", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "72472efb80f08ee3fd844660afa012c8cb256e4b", + "parents": ["b5561ce0ab3b56bb87712c8f90bcf37cf2474f1b"], + "title": "refactor(conventionalCommit): moved filters to questions instead of message", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "b5561ce0ab3b56bb87712c8f90bcf37cf2474f1b", + "parents": ["3e31714dc737029d96898f412e4ecd2be1bcd0ce"], + "title": "fix(manifest): included missing files", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "3e31714dc737029d96898f412e4ecd2be1bcd0ce", + "parents": ["9df721e06595fdd216884c36a28770438b4f4a39"], + "title": "Bump version: 0.9.4 โ†’ 0.9.5", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "9df721e06595fdd216884c36a28770438b4f4a39", + "parents": ["0cf6ada372470c8d09e6c9e68ebf94bbd5a1656f"], + "title": "fix(config): home path for python versions between 3.0 and 3.5", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "0cf6ada372470c8d09e6c9e68ebf94bbd5a1656f", + "parents": ["973c6b3e100f6f69a3fe48bd8ee55c135b96c318"], + "title": "Bump version: 0.9.3 โ†’ 0.9.4", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "973c6b3e100f6f69a3fe48bd8ee55c135b96c318", + "parents": ["dacc86159b260ee98eb5f57941c99ba731a01399"], + "title": "feat(cli): added version", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "dacc86159b260ee98eb5f57941c99ba731a01399", + "parents": ["4368f3c3cbfd4a1ced339212230d854bc5bab496"], + "title": "Bump version: 0.9.2 โ†’ 0.9.3", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "4368f3c3cbfd4a1ced339212230d854bc5bab496", + "parents": ["da94133288727d35dae9b91866a25045038f2d38"], + "title": "feat(committer): conventional commit is a bit more intelligent now", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "da94133288727d35dae9b91866a25045038f2d38", + "parents": ["1541f54503d2e1cf39bd777c0ca5ab5eb78772ba"], + "title": "docs(README): motivation", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "1541f54503d2e1cf39bd777c0ca5ab5eb78772ba", + "parents": ["ddc855a637b7879108308b8dbd85a0fd27c7e0e7"], + "title": "Bump version: 0.9.1 โ†’ 0.9.2", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "ddc855a637b7879108308b8dbd85a0fd27c7e0e7", + "parents": ["46e9032e18a819e466618c7a014bcb0e9981af9e"], + "title": "refactor: renamed conventional_changelog to conventional_commits, not backward compatible", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "46e9032e18a819e466618c7a014bcb0e9981af9e", + "parents": ["0fef73cd7dc77a25b82e197e7c1d3144a58c1350"], + "title": "Bump version: 0.9.0 โ†’ 0.9.1", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, + { + "rev": "0fef73cd7dc77a25b82e197e7c1d3144a58c1350", + "parents": [], + "title": "fix(setup.py): future is now required for every python version", + "body": "", + "author": "Commitizen", + "author_email": "author@cz.dev", + }, +] + + +TAGS = [ + ("v1.2.0", "141ee441c9c9da0809c554103a558eb17c30ed17", "2019-04-19"), + ("v1.1.1", "56c8a8da84e42b526bcbe130bd194306f7c7e813", "2019-04-18"), + ("v1.1.0", "17efb44d2cd16f6621413691a543e467c7d2dda6", "2019-04-14"), + ("v1.0.0", "aa44a92d68014d0da98965c0c2cb8c07957d4362", "2019-03-01"), + ("1.0.0b2", "aab33d13110f26604fb786878856ec0b9e5fc32b", "2019-01-18"), + ("v1.0.0b1", "7c7e96b723c2aaa1aec3a52561f680adf0b60e97", "2019-01-17"), + ("v0.9.11", "c52eca6f74f844ab3ffbde61d98ef96071e132b7", "2018-12-17"), + ("v0.9.10", "b3f89892222340150e32631ae6b7aab65230036f", "2018-09-22"), + ("v0.9.9", "684e0259cc95c7c5e94854608cd3dcebbd53219e", "2018-09-22"), + ("v0.9.8", "64168f18d4628718c49689ee16430549e96c5d4b", "2018-09-22"), + ("v0.9.7", "33b0bf1a0a4dc60aac45ed47476d2e5473add09e", "2018-09-22"), + ("v0.9.6", "bef4a86761a3bda309c962bae5d22ce9b57119e4", "2018-09-19"), + ("v0.9.5", "3e31714dc737029d96898f412e4ecd2be1bcd0ce", "2018-08-24"), + ("v0.9.4", "0cf6ada372470c8d09e6c9e68ebf94bbd5a1656f", "2018-08-02"), + ("v0.9.3", "dacc86159b260ee98eb5f57941c99ba731a01399", "2018-07-28"), + ("v0.9.2", "1541f54503d2e1cf39bd777c0ca5ab5eb78772ba", "2017-11-11"), + ("v0.9.1", "46e9032e18a819e466618c7a014bcb0e9981af9e", "2017-11-11"), +] + + +@pytest.fixture +def gitcommits() -> list: + commits = [ + git.GitCommit( + commit["rev"], + commit["title"], + commit["body"], + commit["author"], + commit["author_email"], + commit["parents"], + ) + for commit in COMMITS_DATA + ] + return commits + + +@pytest.fixture +def tags() -> list: + tags = [git.GitTag(*tag) for tag in TAGS] + return tags + + +@pytest.fixture +def changelog_content() -> str: + changelog_path = "tests/CHANGELOG_FOR_TEST.md" + with open(changelog_path, encoding="utf-8") as f: + return f.read() + + +def test_get_commit_tag_is_a_version(gitcommits, tags): + commit = gitcommits[0] + tag = git.GitTag(*TAGS[0]) + current_key = changelog.get_commit_tag(commit, tags) + assert current_key == tag + + +def test_get_commit_tag_is_None(gitcommits, tags): + commit = gitcommits[1] + current_key = changelog.get_commit_tag(commit, tags) + assert current_key is None + + +@pytest.mark.parametrize("test_input", TAGS) +def test_valid_tag_included_in_changelog(test_input): + tag = git.GitTag(*test_input) + rules = changelog.TagRules() + assert rules.include_in_changelog(tag) + + +def test_invalid_tag_included_in_changelog(): + tag = git.GitTag("not_a_version", "rev", "date") + rules = changelog.TagRules() + assert not rules.include_in_changelog(tag) + + +COMMITS_TREE = ( + { + "version": "v1.2.0", + "date": "2019-04-19", + "changes": { + "feat": [ + { + "scope": None, + "breaking": None, + "message": "custom cz plugins now support bumping version", + } + ] + }, + }, + { + "version": "v1.1.1", + "date": "2019-04-18", + "changes": { + "refactor": [ + { + "scope": None, + "breaking": None, + "message": "changed stdout statements", + }, + { + "scope": "schema", + "breaking": None, + "message": "command logic removed from commitizen base", + }, + { + "scope": "info", + "breaking": None, + "message": "command logic removed from commitizen base", + }, + { + "scope": "example", + "breaking": None, + "message": "command logic removed from commitizen base", + }, + { + "scope": "commit", + "breaking": None, + "message": "moved most of the commit logic to the commit command", + }, + ], + "fix": [ + { + "scope": "bump", + "breaking": None, + "message": "commit message now fits better with semver", + }, + { + "scope": None, + "breaking": None, + "message": "conventional commit 'breaking change' in body instead of title", + }, + ], + }, + }, + { + "version": "v1.1.0", + "date": "2019-04-14", + "changes": { + "feat": [ + { + "scope": None, + "breaking": None, + "message": "new working bump command", + }, + {"scope": None, "breaking": None, "message": "create version tag"}, + { + "scope": None, + "breaking": None, + "message": "update given files with new version", + }, + { + "scope": "config", + "breaking": None, + "message": "new set key, used to set version to cfg", + }, + { + "scope": None, + "breaking": None, + "message": "support for pyproject.toml", + }, + { + "scope": None, + "breaking": None, + "message": "first semantic version bump implementation", + }, + ], + "fix": [ + { + "scope": None, + "breaking": None, + "message": "removed all from commit", + }, + { + "scope": None, + "breaking": None, + "message": "fix config file not working", + }, + ], + "refactor": [ + { + "scope": None, + "breaking": None, + "message": "added commands folder, better integration with decli", + } + ], + }, + }, + { + "version": "v1.0.0", + "date": "2019-03-01", + "changes": { + "refactor": [ + { + "scope": None, + "breaking": None, + "message": "removed delegator, added decli and many tests", + } + ], + "BREAKING CHANGE": [ + {"scope": None, "breaking": None, "message": "API is stable"} + ], + }, + }, + {"version": "1.0.0b2", "date": "2019-01-18", "changes": {}}, + { + "version": "v1.0.0b1", + "date": "2019-01-17", + "changes": { + "feat": [ + { + "scope": None, + "breaking": None, + "message": "py3 only, tests and conventional commits 1.0", + } + ] + }, + }, + { + "version": "v0.9.11", + "date": "2018-12-17", + "changes": { + "fix": [ + { + "scope": "config", + "breaking": None, + "message": "load config reads in order without failing if there is no commitizen section", + } + ] + }, + }, + { + "version": "v0.9.10", + "date": "2018-09-22", + "changes": { + "fix": [ + { + "scope": None, + "breaking": None, + "message": "parse scope (this is my punishment for not having tests)", + } + ] + }, + }, + { + "version": "v0.9.9", + "date": "2018-09-22", + "changes": { + "fix": [{"scope": None, "breaking": None, "message": "parse scope empty"}] + }, + }, + { + "version": "v0.9.8", + "date": "2018-09-22", + "changes": { + "fix": [ + { + "scope": "scope", + "breaking": None, + "message": "parse correctly again", + } + ] + }, + }, + { + "version": "v0.9.7", + "date": "2018-09-22", + "changes": { + "fix": [{"scope": "scope", "breaking": None, "message": "parse correctly"}] + }, + }, + { + "version": "v0.9.6", + "date": "2018-09-19", + "changes": { + "refactor": [ + { + "scope": "conventionalCommit", + "breaking": None, + "message": "moved filters to questions instead of message", + } + ], + "fix": [ + { + "scope": "manifest", + "breaking": None, + "message": "included missing files", + } + ], + }, + }, + { + "version": "v0.9.5", + "date": "2018-08-24", + "changes": { + "fix": [ + { + "scope": "config", + "breaking": None, + "message": "home path for python versions between 3.0 and 3.5", + } + ] + }, + }, + { + "version": "v0.9.4", + "date": "2018-08-02", + "changes": { + "feat": [{"scope": "cli", "breaking": None, "message": "added version"}] + }, + }, + { + "version": "v0.9.3", + "date": "2018-07-28", + "changes": { + "feat": [ + { + "scope": "committer", + "breaking": None, + "message": "conventional commit is a bit more intelligent now", + } + ] + }, + }, + { + "version": "v0.9.2", + "date": "2017-11-11", + "changes": { + "refactor": [ + { + "scope": None, + "breaking": None, + "message": "renamed conventional_changelog to conventional_commits, not backward compatible", + } + ] + }, + }, + { + "version": "v0.9.1", + "date": "2017-11-11", + "changes": { + "fix": [ + { + "scope": "setup.py", + "breaking": None, + "message": "future is now required for every python version", + } + ] + }, + }, +) + +COMMITS_TREE_AFTER_MERGED_PRERELEASES = ( + { + "version": "v1.2.0", + "date": "2019-04-19", + "changes": { + "feat": [ + { + "scope": None, + "breaking": None, + "message": "custom cz plugins now support bumping version", + } + ] + }, + }, + { + "version": "v1.1.1", + "date": "2019-04-18", + "changes": { + "refactor": [ + { + "scope": None, + "breaking": None, + "message": "changed stdout statements", + }, + { + "scope": "schema", + "breaking": None, + "message": "command logic removed from commitizen base", + }, + { + "scope": "info", + "breaking": None, + "message": "command logic removed from commitizen base", + }, + { + "scope": "example", + "breaking": None, + "message": "command logic removed from commitizen base", + }, + { + "scope": "commit", + "breaking": None, + "message": "moved most of the commit logic to the commit command", + }, + ], + "fix": [ + { + "scope": "bump", + "breaking": None, + "message": "commit message now fits better with semver", + }, + { + "scope": None, + "breaking": None, + "message": "conventional commit 'breaking change' in body instead of title", + }, + ], + }, + }, + { + "version": "v1.1.0", + "date": "2019-04-14", + "changes": { + "feat": [ + { + "scope": None, + "breaking": None, + "message": "new working bump command", + }, + {"scope": None, "breaking": None, "message": "create version tag"}, + { + "scope": None, + "breaking": None, + "message": "update given files with new version", + }, + { + "scope": "config", + "breaking": None, + "message": "new set key, used to set version to cfg", + }, + { + "scope": None, + "breaking": None, + "message": "support for pyproject.toml", + }, + { + "scope": None, + "breaking": None, + "message": "first semantic version bump implementation", + }, + ], + "fix": [ + { + "scope": None, + "breaking": None, + "message": "removed all from commit", + }, + { + "scope": None, + "breaking": None, + "message": "fix config file not working", + }, + ], + "refactor": [ + { + "scope": None, + "breaking": None, + "message": "added commands folder, better integration with decli", + } + ], + }, + }, + { + "version": "v1.0.0", + "date": "2019-03-01", + "changes": { + "refactor": [ + { + "scope": None, + "breaking": None, + "message": "removed delegator, added decli and many tests", + } + ], + "feat": [ + { + "scope": None, + "breaking": None, + "message": "py3 only, tests and conventional commits 1.0", + } + ], + "BREAKING CHANGE": [ + {"scope": None, "breaking": None, "message": "API is stable"} + ], + }, + }, + { + "version": "v0.9.11", + "date": "2018-12-17", + "changes": { + "fix": [ + { + "scope": "config", + "breaking": None, + "message": "load config reads in order without failing if there is no commitizen section", + } + ] + }, + }, + { + "version": "v0.9.10", + "date": "2018-09-22", + "changes": { + "fix": [ + { + "scope": None, + "breaking": None, + "message": "parse scope (this is my punishment for not having tests)", + } + ] + }, + }, + { + "version": "v0.9.9", + "date": "2018-09-22", + "changes": { + "fix": [{"scope": None, "breaking": None, "message": "parse scope empty"}] + }, + }, + { + "version": "v0.9.8", + "date": "2018-09-22", + "changes": { + "fix": [ + { + "scope": "scope", + "breaking": None, + "message": "parse correctly again", + } + ] + }, + }, + { + "version": "v0.9.7", + "date": "2018-09-22", + "changes": { + "fix": [{"scope": "scope", "breaking": None, "message": "parse correctly"}] + }, + }, + { + "version": "v0.9.6", + "date": "2018-09-19", + "changes": { + "refactor": [ + { + "scope": "conventionalCommit", + "breaking": None, + "message": "moved filters to questions instead of message", + } + ], + "fix": [ + { + "scope": "manifest", + "breaking": None, + "message": "included missing files", + } + ], + }, + }, + { + "version": "v0.9.5", + "date": "2018-08-24", + "changes": { + "fix": [ + { + "scope": "config", + "breaking": None, + "message": "home path for python versions between 3.0 and 3.5", + } + ] + }, + }, + { + "version": "v0.9.4", + "date": "2018-08-02", + "changes": { + "feat": [{"scope": "cli", "breaking": None, "message": "added version"}] + }, + }, + { + "version": "v0.9.3", + "date": "2018-07-28", + "changes": { + "feat": [ + { + "scope": "committer", + "breaking": None, + "message": "conventional commit is a bit more intelligent now", + } + ] + }, + }, + { + "version": "v0.9.2", + "date": "2017-11-11", + "changes": { + "refactor": [ + { + "scope": None, + "breaking": None, + "message": "renamed conventional_changelog to conventional_commits, not backward compatible", + } + ] + }, + }, + { + "version": "v0.9.1", + "date": "2017-11-11", + "changes": { + "fix": [ + { + "scope": "setup.py", + "breaking": None, + "message": "future is now required for every python version", + } + ] + }, + }, +) + + +@pytest.mark.parametrize("merge_prereleases", (True, False)) +def test_generate_tree_from_commits(gitcommits, tags, merge_prereleases): + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.bump_pattern + rules = changelog.TagRules( + merge_prereleases=merge_prereleases, + ) + tree = changelog.generate_tree_from_commits( + gitcommits, tags, parser, changelog_pattern, rules=rules + ) + expected = ( + COMMITS_TREE_AFTER_MERGED_PRERELEASES if merge_prereleases else COMMITS_TREE + ) + + for release, expected_release in zip(tree, expected): + assert release["version"] == expected_release["version"] + assert release["date"] == expected_release["date"] + assert release["changes"].keys() == expected_release["changes"].keys() + for change_type in release["changes"]: + changes = release["changes"][change_type] + expected_changes = expected_release["changes"][change_type] + for change, expected_change in zip(changes, expected_changes): + assert change["scope"] == expected_change["scope"] + assert change["breaking"] == expected_change["breaking"] + assert change["message"] == expected_change["message"] + assert change["author"] == "Commitizen" + assert change["author_email"] in "author@cz.dev" + assert "sha1" in change + assert "parents" in change + + +def test_generate_tree_from_commits_with_no_commits(tags): + gitcommits = [] + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.bump_pattern + tree = changelog.generate_tree_from_commits( + gitcommits, tags, parser, changelog_pattern + ) + + assert tuple(tree) == ({"changes": {}, "date": "", "version": "Unreleased"},) + + +@pytest.mark.parametrize( + "change_type_order, expected_reordering", + ( + ([], {}), + ( + ["BREAKING CHANGE", "refactor"], + { + "1.1.0": { + "original": ["feat", "fix", "refactor"], + "sorted": ["refactor", "feat", "fix"], + }, + "1.0.0": { + "original": ["refactor", "BREAKING CHANGE"], + "sorted": ["BREAKING CHANGE", "refactor"], + }, + }, + ), + ), +) +def test_order_changelog_tree(change_type_order, expected_reordering): + tree = changelog.order_changelog_tree(COMMITS_TREE, change_type_order) + + for index, entry in enumerate(tuple(tree)): + version = tree[index]["version"] + if version in expected_reordering: + # Verify that all keys are present + assert [*tree[index].keys()] == [*COMMITS_TREE[index].keys()] + # Verify that the reorder only impacted the returned dict and not the original + expected = expected_reordering[version] + assert [*tree[index]["changes"].keys()] == expected["sorted"] + assert [*COMMITS_TREE[index]["changes"].keys()] == expected["original"] + else: + assert [*entry["changes"].keys()] == [*tree[index]["changes"].keys()] + + +def test_order_changelog_tree_raises(): + change_type_order = ["BREAKING CHANGE", "feat", "refactor", "feat"] + with pytest.raises(InvalidConfigurationError) as excinfo: + changelog.order_changelog_tree(COMMITS_TREE, change_type_order) + + assert "Change types contain duplicates types" in str(excinfo) + + +def test_render_changelog( + gitcommits, tags, changelog_content, any_changelog_format: ChangelogFormat +): + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.changelog_pattern + loader = ConventionalCommitsCz.template_loader + template = any_changelog_format.template + tree = changelog.generate_tree_from_commits( + gitcommits, tags, parser, changelog_pattern + ) + result = changelog.render_changelog(tree, loader, template) + assert result == changelog_content + + +def test_render_changelog_from_default_plugin_values( + gitcommits, tags, changelog_content, any_changelog_format: ChangelogFormat +): + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.changelog_pattern + loader = ConventionalCommitsCz.template_loader + template = any_changelog_format.template + tree = changelog.generate_tree_from_commits( + gitcommits, tags, parser, changelog_pattern + ) + result = changelog.render_changelog(tree, loader, template) + assert result == changelog_content + + +def test_render_changelog_override_loader(gitcommits, tags, tmp_path: Path): + loader = FileSystemLoader(tmp_path) + template = "tpl.j2" + tpl = "loader overridden" + (tmp_path / template).write_text(tpl) + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.changelog_pattern + tree = changelog.generate_tree_from_commits( + gitcommits, tags, parser, changelog_pattern + ) + result = changelog.render_changelog(tree, loader, template) + assert result == tpl + + +def test_render_changelog_override_template_from_cwd( + gitcommits, tags, chdir: Path, any_changelog_format: ChangelogFormat +): + tpl = "overridden from cwd" + template = any_changelog_format.template + (chdir / template).write_text(tpl) + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.changelog_pattern + loader = ConventionalCommitsCz.template_loader + tree = changelog.generate_tree_from_commits( + gitcommits, tags, parser, changelog_pattern + ) + result = changelog.render_changelog(tree, loader, template) + assert result == tpl + + +def test_render_changelog_override_template_from_cwd_with_custom_name( + gitcommits, tags, chdir: Path +): + tpl = "template overridden from cwd" + tpl_name = "tpl.j2" + (chdir / tpl_name).write_text(tpl) + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.changelog_pattern + loader = ConventionalCommitsCz.template_loader + tree = changelog.generate_tree_from_commits( + gitcommits, tags, parser, changelog_pattern + ) + result = changelog.render_changelog(tree, loader, tpl_name) + assert result == tpl + + +def test_render_changelog_override_loader_and_template( + gitcommits, tags, tmp_path: Path +): + loader = FileSystemLoader(tmp_path) + tpl = "loader and template overridden" + tpl_name = "tpl.j2" + (tmp_path / tpl_name).write_text(tpl) + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.bump_pattern + tree = changelog.generate_tree_from_commits( + gitcommits, tags, parser, changelog_pattern + ) + result = changelog.render_changelog(tree, loader, tpl_name) + assert result == tpl + + +def test_render_changelog_support_arbitrary_kwargs(gitcommits, tags, tmp_path: Path): + loader = FileSystemLoader(tmp_path) + tpl_name = "tpl.j2" + (tmp_path / tpl_name).write_text("{{ key }}") + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.changelog_pattern + tree = changelog.generate_tree_from_commits( + gitcommits, tags, parser, changelog_pattern + ) + result = changelog.render_changelog(tree, loader, tpl_name, key="value") + assert result == "value" + + +def test_render_changelog_unreleased(gitcommits, any_changelog_format: ChangelogFormat): + some_commits = gitcommits[:7] + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.changelog_pattern + loader = ConventionalCommitsCz.template_loader + template = any_changelog_format.template + tree = changelog.generate_tree_from_commits( + some_commits, [], parser, changelog_pattern + ) + result = changelog.render_changelog(tree, loader, template) + assert "Unreleased" in result + + +def test_render_changelog_tag_and_unreleased( + gitcommits, tags, any_changelog_format: ChangelogFormat +): + some_commits = gitcommits[:7] + single_tag = [ + tag for tag in tags if tag.rev == "56c8a8da84e42b526bcbe130bd194306f7c7e813" + ] + + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.changelog_pattern + loader = ConventionalCommitsCz.template_loader + template = any_changelog_format.template + tree = changelog.generate_tree_from_commits( + some_commits, single_tag, parser, changelog_pattern + ) + result = changelog.render_changelog(tree, loader, template) + + assert "Unreleased" in result + assert "## v1.1.1" in result + + +def test_render_changelog_with_change_type( + gitcommits, tags, any_changelog_format: ChangelogFormat +): + new_title = ":some-emoji: feature" + change_type_map = {"feat": new_title} + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.changelog_pattern + loader = ConventionalCommitsCz.template_loader + template = any_changelog_format.template + tree = changelog.generate_tree_from_commits( + gitcommits, tags, parser, changelog_pattern, change_type_map=change_type_map + ) + result = changelog.render_changelog(tree, loader, template) + assert new_title in result + + +def test_render_changelog_with_changelog_message_builder_hook( + gitcommits, tags, any_changelog_format: ChangelogFormat +): + def changelog_message_builder_hook(message: dict, commit: git.GitCommit) -> dict: + message["message"] = ( + f"{message['message']} [link](github.com/232323232) {commit.author} {commit.author_email}" + ) + return message + + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.changelog_pattern + loader = ConventionalCommitsCz.template_loader + template = any_changelog_format.template + tree = changelog.generate_tree_from_commits( + gitcommits, + tags, + parser, + changelog_pattern, + changelog_message_builder_hook=changelog_message_builder_hook, + ) + result = changelog.render_changelog(tree, loader, template) + + assert "[link](github.com/232323232) Commitizen author@cz.dev" in result + + +def test_changelog_message_builder_hook_can_remove_commits( + gitcommits, tags, any_changelog_format: ChangelogFormat +): + def changelog_message_builder_hook(message: dict, commit: git.GitCommit): + return None + + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.changelog_pattern + loader = ConventionalCommitsCz.template_loader + template = any_changelog_format.template + tree = changelog.generate_tree_from_commits( + gitcommits, + tags, + parser, + changelog_pattern, + changelog_message_builder_hook=changelog_message_builder_hook, + ) + result = changelog.render_changelog(tree, loader, template) + + RE_HEADER = re.compile(r"^## v?\d+\.\d+\.\d+(\w)* \(\d{4}-\d{2}-\d{2}\)$") + # Rendered changelog should be empty, only containing version headers + for no, line in enumerate(result.splitlines()): + if line := line.strip(): + assert RE_HEADER.match(line), f"Line {no} should not be there: {line}" + + +def test_render_changelog_with_changelog_message_builder_hook_multiple_entries( + gitcommits, tags, any_changelog_format: ChangelogFormat +): + def changelog_message_builder_hook(message: dict, commit: git.GitCommit): + messages = [message.copy(), message.copy(), message.copy()] + for idx, msg in enumerate(messages): + msg["message"] = "Message #{idx}" + return messages + + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.changelog_pattern + loader = ConventionalCommitsCz.template_loader + template = any_changelog_format.template + tree = changelog.generate_tree_from_commits( + gitcommits, + tags, + parser, + changelog_pattern, + changelog_message_builder_hook=changelog_message_builder_hook, + ) + result = changelog.render_changelog(tree, loader, template) + + for idx in range(3): + assert "Message #{idx}" in result + + +def test_changelog_message_builder_hook_can_access_and_modify_change_type( + gitcommits, tags, any_changelog_format: ChangelogFormat +): + def changelog_message_builder_hook(message: dict, commit: git.GitCommit): + assert "change_type" in message + message["change_type"] = "overridden" + return message + + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.changelog_pattern + loader = ConventionalCommitsCz.template_loader + template = any_changelog_format.template + tree = changelog.generate_tree_from_commits( + gitcommits, + tags, + parser, + changelog_pattern, + changelog_message_builder_hook=changelog_message_builder_hook, + ) + result = changelog.render_changelog(tree, loader, template) + + RE_HEADER = re.compile(r"^### (?P<type>.+)$") + # There should be only "overridden" change type headers + for no, line in enumerate(result.splitlines()): + if (line := line.strip()) and (match := RE_HEADER.match(line)): + change_type = match.group("type") + assert change_type == "overridden", ( + f"Line {no}: type {change_type} should have been overridden" + ) + + +def test_render_changelog_with_changelog_release_hook( + gitcommits, tags, any_changelog_format: ChangelogFormat +): + def changelog_release_hook(release: dict, tag: Optional[git.GitTag]) -> dict: + release["extra"] = "whatever" + return release + + parser = ConventionalCommitsCz.commit_parser + changelog_pattern = ConventionalCommitsCz.changelog_pattern + tree = changelog.generate_tree_from_commits( + gitcommits, + tags, + parser, + changelog_pattern, + changelog_release_hook=changelog_release_hook, + ) + for release in tree: + assert release["extra"] == "whatever" + + +def test_get_smart_tag_range_returns_an_extra_for_a_range(tags): + start, end = ( + tags[0], + tags[2], + ) # len here is 3, but we expect one more tag as designed + res = changelog.get_smart_tag_range(tags, start.name, end.name) + assert 4 == len(res) + + +def test_get_smart_tag_range_returns_an_extra_for_a_single_tag(tags): + start = tags[0] # len here is 1, but we expect one more tag as designed + res = changelog.get_smart_tag_range(tags, start.name) + assert 2 == len(res) + + +@dataclass +class TagDef: + name: str + is_version: bool + is_legacy: bool + is_ignored: bool + + +TAGS_PARAMS = ( + pytest.param(TagDef("1.2.3", True, False, False), id="version"), + # We test with `v-` prefix as `v` prefix is a special case kept for backward compatibility + pytest.param(TagDef("v-1.2.3", False, True, False), id="v-prefix"), + pytest.param(TagDef("project-1.2.3", False, True, False), id="project-prefix"), + pytest.param(TagDef("ignored", False, False, True), id="ignored"), + pytest.param(TagDef("unknown", False, False, False), id="unknown"), +) + + +@pytest.mark.parametrize("tag", TAGS_PARAMS) +def test_tag_rules_tag_format_only(tag: TagDef): + rules = changelog.TagRules(Pep440, "$version") + assert rules.is_version_tag(tag.name) is tag.is_version + + +@pytest.mark.parametrize("tag", TAGS_PARAMS) +def test_tag_rules_with_legacy_tags(tag: TagDef): + rules = changelog.TagRules( + scheme=Pep440, + tag_format="$version", + legacy_tag_formats=["v-$version", "project-${version}"], + ) + assert rules.is_version_tag(tag.name) is tag.is_version or tag.is_legacy + + +@pytest.mark.parametrize("tag", TAGS_PARAMS) +def test_tag_rules_with_ignored_tags(tag: TagDef): + rules = changelog.TagRules( + scheme=Pep440, tag_format="$version", ignored_tag_formats=["ignored"] + ) + assert rules.is_ignored_tag(tag.name) is tag.is_ignored + + +def test_tags_rules_get_version_tags(capsys: pytest.CaptureFixture): + tags = [ + git.GitTag("v1.1.0", "17efb44d2cd16f6621413691a543e467c7d2dda6", "2019-04-14"), + git.GitTag("v1.0.0", "aa44a92d68014d0da98965c0c2cb8c07957d4362", "2019-03-01"), + git.GitTag("1.0.0b2", "aab33d13110f26604fb786878856ec0b9e5fc32b", "2019-01-18"), + git.GitTag( + "project-not-a-version", + "7c7e96b723c2aaa1aec3a52561f680adf0b60e97", + "2019-01-17", + ), + git.GitTag( + "not-a-version", "c52eca6f74f844ab3ffbde61d98ef96071e132b7", "2018-12-17" + ), + git.GitTag( + "star-something", "c52eca6f74f844ab3ffbde61d98fe96071e132b2", "2018-11-12" + ), + git.GitTag("known", "b3f89892222340150e32631ae6b7aab65230036f", "2018-09-22"), + git.GitTag( + "ignored-0.9.3", "684e0259cc95c7c5e94854608cd3dcebbd53219e", "2018-09-22" + ), + git.GitTag( + "project-0.9.3", "dacc86159b260ee98eb5f57941c99ba731a01399", "2018-07-28" + ), + git.GitTag( + "anything-0.9", "5141f54503d2e1cf39bd666c0ca5ab5eb78772ab", "2018-01-10" + ), + git.GitTag( + "project-0.9.2", "1541f54503d2e1cf39bd777c0ca5ab5eb78772ba", "2017-11-11" + ), + git.GitTag( + "ignored-0.9.1", "46e9032e18a819e466618c7a014bcb0e9981af9e", "2017-11-11" + ), + ] + + rules = changelog.TagRules( + scheme=Pep440, + tag_format="v$version", + legacy_tag_formats=["$version", "project-${version}"], + ignored_tag_formats=[ + "known", + "ignored-${version}", + "star-*", + "*-${major}.${minor}", + ], + ) + + version_tags = rules.get_version_tags(tags, warn=True) + assert {t.name for t in version_tags} == { + "v1.1.0", + "v1.0.0", + "1.0.0b2", + "project-0.9.3", + "project-0.9.2", + } + + captured = capsys.readouterr() + assert captured.err.count("InvalidVersion") == 2 + assert captured.err.count("not-a-version") == 2 diff --git a/tests/test_changelog_format_asciidoc.py b/tests/test_changelog_format_asciidoc.py new file mode 100644 index 0000000..59ca561 --- /dev/null +++ b/tests/test_changelog_format_asciidoc.py @@ -0,0 +1,201 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from commitizen.changelog import Metadata +from commitizen.changelog_formats.asciidoc import AsciiDoc +from commitizen.config.base_config import BaseConfig + +CHANGELOG_A = """ += Changelog + +All notable changes to this project will be documented in this file. + +The format is based on https://keepachangelog.com/en/1.0.0/[Keep a Changelog], +and this project adheres to https://semver.org/spec/v2.0.0.html[Semantic Versioning]. + +== [Unreleased] +* Start using "changelog" over "change log" since it's the common usage. + +== [1.0.0] - 2017-06-20 +=== Added +* New visual identity by https://github.com/tylerfortune8[@tylerfortune8]. +* Version navigation. +""".strip() + +EXPECTED_A = Metadata( + latest_version="1.0.0", + latest_version_position=10, + unreleased_end=10, + unreleased_start=7, +) + + +CHANGELOG_B = """ +== [Unreleased] +* Start using "changelog" over "change log" since it's the common usage. + +== 1.2.0 +""".strip() + +EXPECTED_B = Metadata( + latest_version="1.2.0", + latest_version_position=3, + unreleased_end=3, + unreleased_start=0, +) + + +CHANGELOG_C = """ += Unreleased + +== v1.0.0 +""" +EXPECTED_C = Metadata( + latest_version="1.0.0", + latest_version_tag="v1.0.0", + latest_version_position=3, + unreleased_end=3, + unreleased_start=1, +) + +CHANGELOG_D = """ +== Unreleased +* Start using "changelog" over "change log" since it's the common usage. +""" + +EXPECTED_D = Metadata( + latest_version=None, + latest_version_position=None, + unreleased_end=2, + unreleased_start=1, +) + +CHANGELOG_E = """ += Changelog + +All notable changes to this project will be documented in this file. + +The format is based on https://keepachangelog.com/en/1.0.0/[Keep a Changelog], +and this project adheres to https://semver.org/spec/v2.0.0.html[Semantic Versioning]. + +== [Unreleased] +* Start using "changelog" over "change log" since it's the common usage. + +== [{tag_formatted_version}] - 2017-06-20 +=== Added +* New visual identity by https://github.com/tylerfortune8[@tylerfortune8]. +* Version navigation. +""".strip() + +EXPECTED_E = Metadata( + latest_version="1.0.0", + latest_version_position=10, + unreleased_end=10, + unreleased_start=7, +) + + +@pytest.fixture +def format(config: BaseConfig) -> AsciiDoc: + return AsciiDoc(config) + + +@pytest.fixture +def format_with_tags(config: BaseConfig, request) -> AsciiDoc: + config.settings["tag_format"] = request.param + config.settings["legacy_tag_formats"] = ["legacy-${version}"] + return AsciiDoc(config) + + +VERSIONS_EXAMPLES = [ + ("== [1.0.0] - 2017-06-20", ("1.0.0", "1.0.0")), + ( + "= https://github.com/angular/angular/compare/10.0.0-next.2...10.0.0-next.3[10.0.0-next.3] (2020-04-22)", + ("10.0.0-next.3", "10.0.0-next.3"), + ), + ("=== 0.19.1 (Jan 7, 2020)", ("0.19.1", "0.19.1")), + ("== 1.0.0", ("1.0.0", "1.0.0")), + ("== v1.0.0", ("1.0.0", "v1.0.0")), + ("== v1.0.0 - (2012-24-32)", ("1.0.0", "v1.0.0")), + ("= version 2020.03.24", ("2020.03.24", "2020.03.24")), + ("== [Unreleased]", None), + ("All notable changes to this project will be documented in this file.", None), + ("= Changelog", None), + ("=== Bug Fixes", None), +] + + +@pytest.mark.parametrize("line_from_changelog,output_version", VERSIONS_EXAMPLES) +def test_changelog_detect_version( + line_from_changelog: str, output_version: tuple[str, str] | None, format: AsciiDoc +): + version = format.parse_version_from_title(line_from_changelog) + assert version == output_version + + +TITLES_EXAMPLES = [ + ("== [1.0.0] - 2017-06-20", 2), + ("== [Unreleased]", 2), + ("= Unreleased", 1), +] + + +@pytest.mark.parametrize("line_from_changelog,output_title", TITLES_EXAMPLES) +def test_parse_title_type_of_line( + line_from_changelog: str, output_title: str, format: AsciiDoc +): + title = format.parse_title_level(line_from_changelog) + assert title == output_title + + +@pytest.mark.parametrize( + "content, expected", + ( + pytest.param(CHANGELOG_A, EXPECTED_A, id="A"), + pytest.param(CHANGELOG_B, EXPECTED_B, id="B"), + pytest.param(CHANGELOG_C, EXPECTED_C, id="C"), + pytest.param(CHANGELOG_D, EXPECTED_D, id="D"), + ), +) +def test_get_matadata( + tmp_path: Path, format: AsciiDoc, content: str, expected: Metadata +): + changelog = tmp_path / format.default_changelog_file + changelog.write_text(content) + + assert format.get_metadata(str(changelog)) == expected + + +@pytest.mark.parametrize( + "format_with_tags, tag_string, expected, ", + ( + pytest.param("${version}-example", "1.0.0-example", "1.0.0"), + pytest.param("${version}example", "1.0.0example", "1.0.0"), + pytest.param("example${version}", "example1.0.0", "1.0.0"), + pytest.param("example-${version}", "example-1.0.0", "1.0.0"), + pytest.param("example-${major}-${minor}-${patch}", "example-1-0-0", "1.0.0"), + pytest.param("example-${major}-${minor}", "example-1-0-0", None), + pytest.param( + "${major}-${minor}-${patch}-${prerelease}-example", + "1-0-0-rc1-example", + "1.0.0-rc1", + ), + pytest.param( + "${major}-${minor}-${patch}-${prerelease}${devrelease}-example", + "1-0-0-a1.dev1-example", + "1.0.0-a1.dev1", + ), + pytest.param("new-${version}", "legacy-1.0.0", "1.0.0"), + ), + indirect=["format_with_tags"], +) +def test_get_metadata_custom_tag_format( + tmp_path: Path, format_with_tags: AsciiDoc, tag_string: str, expected: Metadata +): + content = CHANGELOG_E.format(tag_formatted_version=tag_string) + changelog = tmp_path / format_with_tags.default_changelog_file + changelog.write_text(content) + assert format_with_tags.get_metadata(str(changelog)).latest_version == expected diff --git a/tests/test_changelog_format_markdown.py b/tests/test_changelog_format_markdown.py new file mode 100644 index 0000000..e1f0d67 --- /dev/null +++ b/tests/test_changelog_format_markdown.py @@ -0,0 +1,207 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from commitizen.changelog import Metadata +from commitizen.changelog_formats.markdown import Markdown +from commitizen.config.base_config import BaseConfig + +CHANGELOG_A = """ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] +- Start using "changelog" over "change log" since it's the common usage. + +## [1.0.0] - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. +""".strip() + +EXPECTED_A = Metadata( + latest_version="1.0.0", + latest_version_position=10, + unreleased_end=10, + unreleased_start=7, +) + + +CHANGELOG_B = """ +## [Unreleased] +- Start using "changelog" over "change log" since it's the common usage. + +## 1.2.0 +""".strip() + +EXPECTED_B = Metadata( + latest_version="1.2.0", + latest_version_position=3, + unreleased_end=3, + unreleased_start=0, +) + + +CHANGELOG_C = """ +# Unreleased + +## v1.0.0 +""" +EXPECTED_C = Metadata( + latest_version="1.0.0", + latest_version_tag="v1.0.0", + latest_version_position=3, + unreleased_end=3, + unreleased_start=1, +) + +CHANGELOG_D = """ +## Unreleased +- Start using "changelog" over "change log" since it's the common usage. +""" + +EXPECTED_D = Metadata( + latest_version=None, + latest_version_position=None, + unreleased_end=2, + unreleased_start=1, +) + +CHANGELOG_E = """ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] +- Start using "changelog" over "change log" since it's the common usage. + +## {tag_formatted_version} - 2017-06-20 +### Added +- New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +- Version navigation. +""".strip() + +EXPECTED_E = Metadata( + latest_version="1.0.0", + latest_version_position=10, + unreleased_end=10, + unreleased_start=7, +) + + +@pytest.fixture +def format(config: BaseConfig) -> Markdown: + return Markdown(config) + + +@pytest.fixture +def format_with_tags(config: BaseConfig, request) -> Markdown: + config.settings["tag_format"] = request.param + config.settings["legacy_tag_formats"] = ["legacy-${version}"] + return Markdown(config) + + +VERSIONS_EXAMPLES = [ + ("## [1.0.0] - 2017-06-20", ("1.0.0", "1.0.0")), + ( + "# [10.0.0-next.3](https://github.com/angular/angular/compare/10.0.0-next.2...10.0.0-next.3) (2020-04-22)", + ("10.0.0-next.3", "10.0.0-next.3"), + ), + ("### 0.19.1 (Jan 7, 2020)", ("0.19.1", "0.19.1")), + ("## 1.0.0", ("1.0.0", "1.0.0")), + ("## v1.0.0", ("1.0.0", "v1.0.0")), + ("## v1.0.0 - (2012-24-32)", ("1.0.0", "v1.0.0")), + ("# version 2020.03.24", ("2020.03.24", "2020.03.24")), + ("## [Unreleased]", None), + ("All notable changes to this project will be documented in this file.", None), + ("# Changelog", None), + ("### Bug Fixes", None), +] + + +@pytest.mark.parametrize("line_from_changelog,output_version", VERSIONS_EXAMPLES) +def test_changelog_detect_version( + line_from_changelog: str, output_version: tuple[str, str] | None, format: Markdown +): + version = format.parse_version_from_title(line_from_changelog) + assert version == output_version + + +TITLES_EXAMPLES = [ + ("## [1.0.0] - 2017-06-20", 2), + ("## [Unreleased]", 2), + ("# Unreleased", 1), +] + + +@pytest.mark.parametrize("line_from_changelog,output_title", TITLES_EXAMPLES) +def test_parse_title_type_of_line( + line_from_changelog: str, output_title: str, format: Markdown +): + title = format.parse_title_level(line_from_changelog) + assert title == output_title + + +@pytest.mark.parametrize( + "content, expected", + ( + pytest.param(CHANGELOG_A, EXPECTED_A, id="A"), + pytest.param(CHANGELOG_B, EXPECTED_B, id="B"), + pytest.param(CHANGELOG_C, EXPECTED_C, id="C"), + pytest.param(CHANGELOG_D, EXPECTED_D, id="D"), + ), +) +def test_get_matadata( + tmp_path: Path, format: Markdown, content: str, expected: Metadata +): + changelog = tmp_path / format.default_changelog_file + changelog.write_text(content) + + assert format.get_metadata(str(changelog)) == expected + + +@pytest.mark.parametrize( + "format_with_tags, tag_string, expected, ", + ( + pytest.param("${version}-example", "1.0.0-example", "1.0.0"), + pytest.param("${version}example", "1.0.0example", "1.0.0"), + pytest.param("example${version}", "example1.0.0", "1.0.0"), + pytest.param("example-${version}", "example-1.0.0", "1.0.0"), + pytest.param("example-${major}-${minor}-${patch}", "example-1-0-0", "1.0.0"), + pytest.param("example-${major}-${minor}", "example-1-0-0", None), + pytest.param( + "${major}-${minor}-${patch}-${prerelease}-example", + "1-0-0-rc1-example", + "1.0.0-rc1", + ), + pytest.param( + "${major}-${minor}-${patch}-${prerelease}-example", + "1-0-0-a1-example", + "1.0.0-a1", + ), + pytest.param( + "${major}-${minor}-${patch}-${prerelease}${devrelease}-example", + "1-0-0-a1.dev1-example", + "1.0.0-a1.dev1", + ), + pytest.param("new-${version}", "legacy-1.0.0", "1.0.0"), + ), + indirect=["format_with_tags"], +) +def test_get_metadata_custom_tag_format( + tmp_path: Path, format_with_tags: Markdown, tag_string: str, expected: Metadata +): + content = CHANGELOG_E.format(tag_formatted_version=tag_string) + changelog = tmp_path / format_with_tags.default_changelog_file + changelog.write_text(content) + + assert format_with_tags.get_metadata(str(changelog)).latest_version == expected diff --git a/tests/test_changelog_format_restructuredtext.py b/tests/test_changelog_format_restructuredtext.py new file mode 100644 index 0000000..74b6b73 --- /dev/null +++ b/tests/test_changelog_format_restructuredtext.py @@ -0,0 +1,381 @@ +from __future__ import annotations + +from pathlib import Path +from textwrap import dedent +from typing import TYPE_CHECKING + +import pytest + +from commitizen.changelog import Metadata +from commitizen.changelog_formats.restructuredtext import RestructuredText +from commitizen.config.base_config import BaseConfig + +if TYPE_CHECKING: + from _pytest.mark.structures import ParameterSet + + +CASES: list[ParameterSet] = [] + + +def case( + id: str, + content: str, + latest_version: str | None = None, + latest_version_position: int | None = None, + latest_version_tag: str | None = None, + unreleased_start: int | None = None, + unreleased_end: int | None = None, +): + CASES.append( + pytest.param( + dedent(content).strip(), + Metadata( + latest_version=latest_version, + latest_version_tag=latest_version_tag, + latest_version_position=latest_version_position, + unreleased_start=unreleased_start, + unreleased_end=unreleased_end, + ), + id=id, + ) + ) + + +case( + "underlined title with intro and unreleased section", + """ + Changelog + ######### + + All notable changes to this project will be documented in this file. + + The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`, + and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`. + + Unreleased + ========== + * Start using "changelog" over "change log" since it's the common usage. + + 1.0.0 - 2017-06-20 + ================== + Added + ----- + * New visual identity by `@tylerfortune8 <https://github.com/tylerfortune8>`. + * Version navigation. + """, + latest_version="1.0.0", + latest_version_position=12, + unreleased_start=8, + unreleased_end=12, +) + +case( + "unreleased section without preamble", + """ + Unreleased + ========== + * Start using "changelog" over "change log" since it's the common usage. + + 1.2.0 + ===== + """, + latest_version="1.2.0", + latest_version_position=4, + unreleased_start=0, + unreleased_end=4, +) + +case( + "basic underlined titles with v-prefixed version", + """ + Unreleased + ========== + + v1.0.0 + ====== + """, + latest_version="1.0.0", + latest_version_tag="v1.0.0", + latest_version_position=3, + unreleased_start=0, + unreleased_end=3, +) + +case( + "intermediate section in unreleased", + """ + Unreleased + ========== + + intermediate + ------------ + + 1.0.0 + ===== + """, + latest_version="1.0.0", + latest_version_position=6, + unreleased_start=0, + unreleased_end=6, +) + +case( + "weird section with different level than versions", + """ + Unreleased + ########## + + 1.0.0 + ===== + """, + latest_version="1.0.0", + latest_version_position=3, + unreleased_start=0, + unreleased_end=3, +) + +case( + "overlined title without release and intro", + """ + ========== + Unreleased + ========== + * Start using "changelog" over "change log" since it's the common usage. + """, + unreleased_start=0, + unreleased_end=4, +) + +case( + "underlined title with date", + """ + 1.0.0 - 2017-06-20 + ================== + """, + latest_version="1.0.0", + latest_version_position=0, +) + + +UNDERLINED_TITLES = ( + """ + title + ===== + """, + """ + title + ====== + """, + """ + title + ##### + """, + """ + title + ..... + """, + """ + title + !!!!! + """, +) + +NOT_UNDERLINED_TITLES = ( + """ + title + =.=.= + """, + """ + title + ==== + """, + """ + title + aaaaa + """, + """ + title + + """, +) + + +OVERLINED_TITLES = ( + """ + ===== + title + ===== + """, + """ + ====== + title + ====== + """, + """ + ##### + title + ##### + """, + """ + ..... + title + ..... + """, +) + +NOT_OVERLINED_TITLES = ( + """ + ==== + title + ===== + """, + """ + ===== + title + ==== + """, + """ + ==== + title + ==== + """, + """ + ===== + title + ##### + """, + """ + ##### + title + ===== + """, + """ + =.=.= + title + ===== + """, + """ + ===== + title + =.=.= + """, + """ + + title + ===== + """, + """ + ===== + title + + """, + """ + aaaaa + title + aaaaa + """, +) + +CHANGELOG = """ +Changelog + ######### + + All notable changes to this project will be documented in this file. + + The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`, + and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`. + + Unreleased + ========== + * Start using "changelog" over "change log" since it's the common usage. + + {tag_formatted_version} - 2017-06-20 + {underline} + Added + ----- + * New visual identity by `@tylerfortune8 <https://github.com/tylerfortune8>`. + * Version navigation. +""".strip() + + +@pytest.fixture +def format(config: BaseConfig) -> RestructuredText: + return RestructuredText(config) + + +@pytest.fixture +def format_with_tags(config: BaseConfig, request) -> RestructuredText: + config.settings["tag_format"] = request.param + config.settings["legacy_tag_formats"] = ["legacy-${version}"] + return RestructuredText(config) + + +@pytest.mark.parametrize("content, expected", CASES) +def test_get_matadata( + tmp_path: Path, format: RestructuredText, content: str, expected: Metadata +): + changelog = tmp_path / format.default_changelog_file + changelog.write_text(content) + + assert format.get_metadata(str(changelog)) == expected + + +@pytest.mark.parametrize( + "text, expected", + [(text, True) for text in UNDERLINED_TITLES] + + [(text, False) for text in NOT_UNDERLINED_TITLES], +) +def test_is_underlined_title(format: RestructuredText, text: str, expected: bool): + _, first, second = dedent(text).splitlines() + assert format.is_underlined_title(first, second) is expected + + +@pytest.mark.parametrize( + "text, expected", + [(text, True) for text in OVERLINED_TITLES] + + [(text, False) for text in NOT_OVERLINED_TITLES], +) +def test_is_overlined_title(format: RestructuredText, text: str, expected: bool): + _, first, second, third = dedent(text).splitlines() + + assert format.is_overlined_title(first, second, third) is expected + + +@pytest.mark.parametrize( + "format_with_tags, tag_string, expected, ", + ( + pytest.param("${version}-example", "1.0.0-example", "1.0.0"), + pytest.param("${version}", "1.0.0", "1.0.0"), + pytest.param("${version}example", "1.0.0example", "1.0.0"), + pytest.param("example${version}", "example1.0.0", "1.0.0"), + pytest.param("example-${version}", "example-1.0.0", "1.0.0"), + pytest.param("example-${major}-${minor}-${patch}", "example-1-0-0", "1.0.0"), + pytest.param("example-${major}-${minor}", "example-1-0-0", None), + pytest.param( + "${major}-${minor}-${patch}-${prerelease}-example", + "1-0-0-rc1-example", + "1.0.0-rc1", + ), + pytest.param( + "${major}-${minor}-${patch}-${prerelease}${devrelease}-example", + "1-0-0-a1.dev1-example", + "1.0.0-a1.dev1", + ), + pytest.param("new-${version}", "legacy-1.0.0", "1.0.0"), + ), + indirect=["format_with_tags"], +) +def test_get_metadata_custom_tag_format( + tmp_path: Path, + format_with_tags: RestructuredText, + tag_string: str, + expected: Metadata, +): + content = CHANGELOG.format( + tag_formatted_version=tag_string, + underline="=" * len(tag_string) + "=============", + ) + changelog = tmp_path / format_with_tags.default_changelog_file + changelog.write_text(content) + + assert format_with_tags.get_metadata(str(changelog)).latest_version == expected diff --git a/tests/test_changelog_format_textile.py b/tests/test_changelog_format_textile.py new file mode 100644 index 0000000..eb03484 --- /dev/null +++ b/tests/test_changelog_format_textile.py @@ -0,0 +1,195 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from commitizen.changelog import Metadata +from commitizen.changelog_formats.textile import Textile +from commitizen.config.base_config import BaseConfig + +CHANGELOG_A = """ +h1. Changelog + +All notable changes to this project will be documented in this file. + +The format is based on "Keep a Changelog":https://keepachangelog.com/en/1.0.0/, +and this project adheres to "Semantic Versioning":https://semver.org/spec/v2.0.0.html. + +h2. [Unreleased] +- Start using "changelog" over "change log" since it's the common usage. + +h2. [1.0.0] - 2017-06-20 +h3. Added +* New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +* Version navigation. +""".strip() + +EXPECTED_A = Metadata( + latest_version="1.0.0", + latest_version_position=10, + unreleased_end=10, + unreleased_start=7, +) + + +CHANGELOG_B = """ +h2. [Unreleased] +* Start using "changelog" over "change log" since it's the common usage. + +h2. 1.2.0 +""".strip() + +EXPECTED_B = Metadata( + latest_version="1.2.0", + latest_version_position=3, + unreleased_end=3, + unreleased_start=0, +) + + +CHANGELOG_C = """ +h1. Unreleased + +h2. v1.0.0 +""" +EXPECTED_C = Metadata( + latest_version="1.0.0", + latest_version_tag="v1.0.0", + latest_version_position=3, + unreleased_end=3, + unreleased_start=1, +) + +CHANGELOG_D = """ +h2. Unreleased +* Start using "changelog" over "change log" since it's the common usage. +""" + +EXPECTED_D = Metadata( + latest_version=None, + latest_version_position=None, + unreleased_end=2, + unreleased_start=1, +) + +CHANGELOG_E = """ +h1. Changelog + +All notable changes to this project will be documented in this file. + +The format is based on "Keep a Changelog":https://keepachangelog.com/en/1.0.0/, +and this project adheres to "Semantic Versioning":https://semver.org/spec/v2.0.0.html. + +h2. [Unreleased] +- Start using "changelog" over "change log" since it's the common usage. + +h2. [{tag_formatted_version}] - 2017-06-20 +h3. Added +* New visual identity by [@tylerfortune8](https://github.com/tylerfortune8). +* Version navigation. +""".strip() + + +@pytest.fixture +def format(config: BaseConfig) -> Textile: + return Textile(config) + + +@pytest.fixture +def format_with_tags(config: BaseConfig, request) -> Textile: + config.settings["tag_format"] = request.param + config.settings["legacy_tag_formats"] = ["legacy-${version}"] + return Textile(config) + + +VERSIONS_EXAMPLES = [ + ("h2. [1.0.0] - 2017-06-20", ("1.0.0", "1.0.0")), + ( + 'h1. "10.0.0-next.3":https://github.com/angular/angular/compare/10.0.0-next.2...10.0.0-next.3 (2020-04-22)', + ("10.0.0-next.3", "10.0.0-next.3"), + ), + ("h3. 0.19.1 (Jan 7, 2020)", ("0.19.1", "0.19.1")), + ("h2. 1.0.0", ("1.0.0", "1.0.0")), + ("h2. v1.0.0", ("1.0.0", "v1.0.0")), + ("h2. v1.0.0 - (2012-24-32)", ("1.0.0", "v1.0.0")), + ("h1. version 2020.03.24", ("2020.03.24", "2020.03.24")), + ("h2. [Unreleased]", None), + ("All notable changes to this project will be documented in this file.", None), + ("h1. Changelog", None), + ("h3. Bug Fixes", None), +] + + +@pytest.mark.parametrize("line_from_changelog,output_version", VERSIONS_EXAMPLES) +def test_changelog_detect_version( + line_from_changelog: str, output_version: tuple[str, str] | None, format: Textile +): + version = format.parse_version_from_title(line_from_changelog) + assert version == output_version + + +TITLES_EXAMPLES = [ + ("h2. [1.0.0] - 2017-06-20", 2), + ("h2. [Unreleased]", 2), + ("h1. Unreleased", 1), +] + + +@pytest.mark.parametrize("line_from_changelog,output_title", TITLES_EXAMPLES) +def test_parse_title_type_of_line( + line_from_changelog: str, output_title: str, format: Textile +): + title = format.parse_title_level(line_from_changelog) + assert title == output_title + + +@pytest.mark.parametrize( + "content, expected", + ( + pytest.param(CHANGELOG_A, EXPECTED_A, id="A"), + pytest.param(CHANGELOG_B, EXPECTED_B, id="B"), + pytest.param(CHANGELOG_C, EXPECTED_C, id="C"), + pytest.param(CHANGELOG_D, EXPECTED_D, id="D"), + ), +) +def test_get_matadata( + tmp_path: Path, format: Textile, content: str, expected: Metadata +): + changelog = tmp_path / format.default_changelog_file + changelog.write_text(content) + + assert format.get_metadata(str(changelog)) == expected + + +@pytest.mark.parametrize( + "format_with_tags, tag_string, expected, ", + ( + pytest.param("${version}-example", "1.0.0-example", "1.0.0"), + pytest.param("${version}example", "1.0.0example", "1.0.0"), + pytest.param("example${version}", "example1.0.0", "1.0.0"), + pytest.param("example-${version}", "example-1.0.0", "1.0.0"), + pytest.param("example-${major}-${minor}-${patch}", "example-1-0-0", "1.0.0"), + pytest.param("example-${major}-${minor}", "example-1-0-0", None), + pytest.param( + "${major}-${minor}-${patch}-${prerelease}-example", + "1-0-0-rc1-example", + "1.0.0-rc1", + ), + pytest.param( + "${major}-${minor}-${patch}-${prerelease}${devrelease}-example", + "1-0-0-a1.dev1-example", + "1.0.0-a1.dev1", + ), + pytest.param("new-${version}", "legacy-1.0.0", "1.0.0"), + ), + indirect=["format_with_tags"], +) +def test_get_metadata_custom_tag_format( + tmp_path: Path, format_with_tags: Textile, tag_string: str, expected: Metadata +): + content = CHANGELOG_E.format(tag_formatted_version=tag_string) + changelog = tmp_path / format_with_tags.default_changelog_file + changelog.write_text(content) + + assert format_with_tags.get_metadata(str(changelog)).latest_version == expected diff --git a/tests/test_changelog_formats.py b/tests/test_changelog_formats.py new file mode 100644 index 0000000..dec2372 --- /dev/null +++ b/tests/test_changelog_formats.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import pytest + +from commitizen import defaults +from commitizen.changelog_formats import ( + KNOWN_CHANGELOG_FORMATS, + ChangelogFormat, + get_changelog_format, + guess_changelog_format, +) +from commitizen.config.base_config import BaseConfig +from commitizen.exceptions import ChangelogFormatUnknown + + +@pytest.mark.parametrize("format", KNOWN_CHANGELOG_FORMATS.values()) +def test_guess_format(format: type[ChangelogFormat]): + assert guess_changelog_format(f"CHANGELOG.{format.extension}") is format + for ext in format.alternative_extensions: + assert guess_changelog_format(f"CHANGELOG.{ext}") is format + + +@pytest.mark.parametrize("filename", ("CHANGELOG", "NEWS", "file.unknown", None)) +def test_guess_format_unknown(filename: str): + assert guess_changelog_format(filename) is None + + +@pytest.mark.parametrize( + "name, expected", + [ + pytest.param(name, format, id=name) + for name, format in KNOWN_CHANGELOG_FORMATS.items() + ], +) +def test_get_format(config: BaseConfig, name: str, expected: type[ChangelogFormat]): + config.settings["changelog_format"] = name + assert isinstance(get_changelog_format(config), expected) + + +@pytest.mark.parametrize("filename", (None, "")) +def test_get_format_empty_filename(config: BaseConfig, filename: str | None): + config.settings["changelog_format"] = defaults.CHANGELOG_FORMAT + assert isinstance( + get_changelog_format(config, filename), + KNOWN_CHANGELOG_FORMATS[defaults.CHANGELOG_FORMAT], + ) + + +@pytest.mark.parametrize("filename", (None, "")) +def test_get_format_empty_filename_no_setting(config: BaseConfig, filename: str | None): + config.settings["changelog_format"] = None + with pytest.raises(ChangelogFormatUnknown): + get_changelog_format(config, filename) + + +@pytest.mark.parametrize("filename", ("extensionless", "file.unknown")) +def test_get_format_unknown(config: BaseConfig, filename: str | None): + with pytest.raises(ChangelogFormatUnknown): + get_changelog_format(config, filename) diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..a91e633 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,184 @@ +import os +import subprocess +import sys +from functools import partial + +import pytest +from pytest_mock import MockFixture + +from commitizen import cli +from commitizen.exceptions import ( + ConfigFileNotFound, + ExpectedExit, + InvalidCommandArgumentError, + NoCommandFoundError, + NotAGitProjectError, +) + + +def test_sysexit_no_argv(mocker: MockFixture, capsys): + testargs = ["cz"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(ExpectedExit): + cli.main() + out, _ = capsys.readouterr() + assert out.startswith("usage") + + +def test_cz_config_file_without_correct_file_path(mocker: MockFixture, capsys): + testargs = ["cz", "--config", "./config/pyproject.toml", "example"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(ConfigFileNotFound) as excinfo: + cli.main() + assert "Cannot found the config file" in str(excinfo.value) + + +def test_cz_with_arg_but_without_command(mocker: MockFixture): + testargs = ["cz", "--name", "cz_jira"] + mocker.patch.object(sys, "argv", testargs) + + with pytest.raises(NoCommandFoundError) as excinfo: + cli.main() + assert "Command is required" in str(excinfo.value) + + +def test_name(mocker: MockFixture, capsys): + testargs = ["cz", "-n", "cz_jira", "example"] + mocker.patch.object(sys, "argv", testargs) + + cli.main() + out, _ = capsys.readouterr() + assert out.startswith("JRA") + + +@pytest.mark.usefixtures("tmp_git_project") +def test_name_default_value(mocker: MockFixture, capsys): + testargs = ["cz", "example"] + mocker.patch.object(sys, "argv", testargs) + + cli.main() + out, _ = capsys.readouterr() + assert out.startswith("fix: correct minor typos in code") + + +def test_ls(mocker: MockFixture, capsys): + testargs = ["cz", "-n", "cz_jira", "ls"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + out, err = capsys.readouterr() + + assert "cz_conventional_commits" in out + assert isinstance(out, str) + + +def test_arg_debug(mocker: MockFixture): + testargs = ["cz", "--debug", "info"] + mocker.patch.object(sys, "argv", testargs) + cli.main() + excepthook = sys.excepthook + # `sys.excepthook` is replaced by a `partial` in `cli.main` + # it's not a normal function + assert isinstance(excepthook, partial) + assert excepthook.keywords.get("debug") is True + + +def test_commitizen_excepthook(capsys): + with pytest.raises(SystemExit) as excinfo: + cli.commitizen_excepthook(NotAGitProjectError, NotAGitProjectError(), "") + + assert excinfo.type is SystemExit + assert excinfo.value.code == NotAGitProjectError.exit_code + + +def test_commitizen_debug_excepthook(capsys): + with pytest.raises(SystemExit) as excinfo: + cli.commitizen_excepthook( + NotAGitProjectError, + NotAGitProjectError(), + "", + debug=True, + ) + + assert excinfo.type is SystemExit + assert excinfo.value.code == NotAGitProjectError.exit_code + assert "NotAGitProjectError" in str(excinfo.traceback[0]) + + +@pytest.mark.skipif( + os.name == "nt", + reason="`argcomplete` does not support Git Bash on Windows.", +) +def test_argcomplete_activation(): + """ + This function is testing the one-time activation of argcomplete for + commitizen only. + + Equivalent to run: + $ eval "$(register-python-argcomplete pytest)" + """ + output = subprocess.run(["register-python-argcomplete", "cz"]) + + assert output.returncode == 0 + + +def test_commitizen_excepthook_no_raises(capsys): + with pytest.raises(SystemExit) as excinfo: + cli.commitizen_excepthook( + NotAGitProjectError, + NotAGitProjectError(), + "", + no_raise=[NotAGitProjectError.exit_code], + ) + + assert excinfo.type is SystemExit + assert excinfo.value.code == 0 + + +def test_parse_no_raise_single_integer(): + input_str = "1" + result = cli.parse_no_raise(input_str) + assert result == [1] + + +def test_parse_no_raise_integers(): + input_str = "1,2,3" + result = cli.parse_no_raise(input_str) + assert result == [1, 2, 3] + + +def test_parse_no_raise_error_code(): + input_str = "NO_COMMITIZEN_FOUND,NO_COMMITS_FOUND,NO_PATTERN_MAP" + result = cli.parse_no_raise(input_str) + assert result == [1, 3, 5] + + +def test_parse_no_raise_mix_integer_error_code(): + input_str = "NO_COMMITIZEN_FOUND,2,NO_COMMITS_FOUND,4" + result = cli.parse_no_raise(input_str) + assert result == [1, 2, 3, 4] + + +def test_parse_no_raise_mix_invalid_arg_is_skipped(): + input_str = "NO_COMMITIZEN_FOUND,2,nothing,4" + result = cli.parse_no_raise(input_str) + assert result == [1, 2, 4] + + +def test_unknown_args_raises(mocker: MockFixture): + testargs = ["cz", "c", "-this_arg_is_not_supported"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(InvalidCommandArgumentError) as excinfo: + cli.main() + assert "Invalid commitizen arguments were found" in str(excinfo.value) + + +def test_unknown_args_before_double_dash_raises(mocker: MockFixture): + testargs = ["cz", "c", "-this_arg_is_not_supported", "--"] + mocker.patch.object(sys, "argv", testargs) + with pytest.raises(InvalidCommandArgumentError) as excinfo: + cli.main() + assert "Invalid commitizen arguments were found before -- separator" in str( + excinfo.value + ) diff --git a/tests/test_cmd.py b/tests/test_cmd.py new file mode 100644 index 0000000..e8a869e --- /dev/null +++ b/tests/test_cmd.py @@ -0,0 +1,53 @@ +import pytest + +from commitizen import cmd +from commitizen.exceptions import CharacterSetDecodeError + + +# https://docs.python.org/3/howto/unicode.html +def test_valid_utf8_encoded_strings(): + valid_strings = ( + "", + "ascii", + "๐Ÿคฆ๐Ÿปโ€โ™‚๏ธ", + "๏ทฝ", + "\u0000", + ) + assert all(s == cmd._try_decode(s.encode("utf-8")) for s in valid_strings) + + +# A word of caution: just because an encoding can be guessed for a given +# sequence of bytes and because that guessed encoding may yield a decoded +# string, does not mean that that string was the original! For more, see: +# https://docs.python.org/3/library/codecs.html#standard-encodings + + +# Pick a random, non-utf8 encoding to test. +def test_valid_cp1250_encoded_strings(): + valid_strings = ( + "", + "ascii", + "รครถรผรŸ", + "รงa va", + "jak se mรกte", + ) + for s in valid_strings: + assert cmd._try_decode(s.encode("cp1250")) or True + + +def test_invalid_bytes(): + invalid_bytes = (b"\x73\xe2\x9d\xff\x00",) + for s in invalid_bytes: + with pytest.raises(CharacterSetDecodeError): + cmd._try_decode(s) + + +def test_always_fail_decode(): + class _bytes(bytes): + def decode(self, encoding="utf-8", errors="strict"): + raise UnicodeDecodeError( + encoding, self, 0, 0, "Failing intentionally for testing" + ) + + with pytest.raises(CharacterSetDecodeError): + cmd._try_decode(_bytes()) diff --git a/tests/test_conf.py b/tests/test_conf.py new file mode 100644 index 0000000..80d5898 --- /dev/null +++ b/tests/test_conf.py @@ -0,0 +1,311 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any + +import pytest +import yaml + +from commitizen import config, defaults, git +from commitizen.exceptions import ConfigFileIsEmpty, InvalidConfigurationError + +PYPROJECT = """ +[tool.commitizen] +name = "cz_jira" +version = "1.0.0" +version_files = [ + "commitizen/__version__.py", + "pyproject.toml" +] +style = [ + ["pointer", "reverse"], + ["question", "underline"] +] +pre_bump_hooks = [ + "scripts/generate_documentation.sh" +] +post_bump_hooks = ["scripts/slack_notification.sh"] + +[tool.black] +line-length = 88 +target-version = ['py36', 'py37', 'py38'] +""" + +DICT_CONFIG = { + "commitizen": { + "name": "cz_jira", + "version": "1.0.0", + "version_files": ["commitizen/__version__.py", "pyproject.toml"], + "style": [["pointer", "reverse"], ["question", "underline"]], + "pre_bump_hooks": ["scripts/generate_documentation.sh"], + "post_bump_hooks": ["scripts/slack_notification.sh"], + } +} + +JSON_STR = r""" + { + "commitizen": { + "name": "cz_jira", + "version": "1.0.0", + "version_files": [ + "commitizen/__version__.py", + "pyproject.toml" + ] + } + } +""" + +YAML_STR = """ +commitizen: + name: cz_jira + version: 1.0.0 + version_files: + - commitizen/__version__.py + - pyproject.toml +""" + +_settings: dict[str, Any] = { + "name": "cz_jira", + "version": "1.0.0", + "version_provider": "commitizen", + "version_scheme": None, + "tag_format": "$version", + "legacy_tag_formats": [], + "ignored_tag_formats": [], + "bump_message": None, + "retry_after_failure": False, + "allow_abort": False, + "allowed_prefixes": ["Merge", "Revert", "Pull request", "fixup!", "squash!"], + "version_files": ["commitizen/__version__.py", "pyproject.toml"], + "style": [["pointer", "reverse"], ["question", "underline"]], + "changelog_file": "CHANGELOG.md", + "changelog_format": None, + "changelog_incremental": False, + "changelog_start_rev": None, + "changelog_merge_prerelease": False, + "update_changelog_on_bump": False, + "use_shortcuts": False, + "major_version_zero": False, + "pre_bump_hooks": ["scripts/generate_documentation.sh"], + "post_bump_hooks": ["scripts/slack_notification.sh"], + "prerelease_offset": 0, + "encoding": "utf-8", + "always_signoff": False, + "template": None, + "extras": {}, +} + +_new_settings: dict[str, Any] = { + "name": "cz_jira", + "version": "2.0.0", + "version_provider": "commitizen", + "version_scheme": None, + "tag_format": "$version", + "legacy_tag_formats": [], + "ignored_tag_formats": [], + "bump_message": None, + "retry_after_failure": False, + "allow_abort": False, + "allowed_prefixes": ["Merge", "Revert", "Pull request", "fixup!", "squash!"], + "version_files": ["commitizen/__version__.py", "pyproject.toml"], + "style": [["pointer", "reverse"], ["question", "underline"]], + "changelog_file": "CHANGELOG.md", + "changelog_format": None, + "changelog_incremental": False, + "changelog_start_rev": None, + "changelog_merge_prerelease": False, + "update_changelog_on_bump": False, + "use_shortcuts": False, + "major_version_zero": False, + "pre_bump_hooks": ["scripts/generate_documentation.sh"], + "post_bump_hooks": ["scripts/slack_notification.sh"], + "prerelease_offset": 0, + "encoding": "utf-8", + "always_signoff": False, + "template": None, + "extras": {}, +} + + +@pytest.fixture +def config_files_manager(request, tmpdir): + with tmpdir.as_cwd(): + filename = request.param + with open(filename, "w", encoding="utf-8") as f: + if "toml" in filename: + f.write(PYPROJECT) + elif "json" in filename: + json.dump(DICT_CONFIG, f) + elif "yaml" in filename: + yaml.dump(DICT_CONFIG, f) + yield + + +def test_find_git_project_root(tmpdir): + assert git.find_git_project_root() == Path(os.getcwd()) + + with tmpdir.as_cwd() as _: + assert git.find_git_project_root() is None + + +@pytest.mark.parametrize( + "config_files_manager", defaults.config_files.copy(), indirect=True +) +def test_set_key(config_files_manager): + _conf = config.read_cfg() + _conf.set_key("version", "2.0.0") + cfg = config.read_cfg() + assert cfg.settings == _new_settings + + +class TestReadCfg: + @pytest.mark.parametrize( + "config_files_manager", defaults.config_files.copy(), indirect=True + ) + def test_load_conf(_, config_files_manager): + cfg = config.read_cfg() + assert cfg.settings == _settings + + def test_conf_returns_default_when_no_files(_, tmpdir): + with tmpdir.as_cwd(): + cfg = config.read_cfg() + assert cfg.settings == defaults.DEFAULT_SETTINGS + + def test_load_empty_pyproject_toml_and_cz_toml_with_config(_, tmpdir): + with tmpdir.as_cwd(): + p = tmpdir.join("pyproject.toml") + p.write("") + p = tmpdir.join(".cz.toml") + p.write(PYPROJECT) + + cfg = config.read_cfg() + assert cfg.settings == _settings + + def test_load_pyproject_toml_from_config_argument(_, tmpdir): + with tmpdir.as_cwd(): + _not_root_path = tmpdir.mkdir("not_in_root").join("pyproject.toml") + _not_root_path.write(PYPROJECT) + + cfg = config.read_cfg(filepath="./not_in_root/pyproject.toml") + assert cfg.settings == _settings + + def test_load_cz_json_not_from_config_argument(_, tmpdir): + with tmpdir.as_cwd(): + _not_root_path = tmpdir.mkdir("not_in_root").join(".cz.json") + _not_root_path.write(JSON_STR) + + cfg = config.read_cfg(filepath="./not_in_root/.cz.json") + json_cfg_by_class = config.JsonConfig(data=JSON_STR, path=_not_root_path) + assert cfg.settings == json_cfg_by_class.settings + + def test_load_cz_yaml_not_from_config_argument(_, tmpdir): + with tmpdir.as_cwd(): + _not_root_path = tmpdir.mkdir("not_in_root").join(".cz.yaml") + _not_root_path.write(YAML_STR) + + cfg = config.read_cfg(filepath="./not_in_root/.cz.yaml") + yaml_cfg_by_class = config.YAMLConfig(data=YAML_STR, path=_not_root_path) + assert cfg.settings == yaml_cfg_by_class._settings + + def test_load_empty_pyproject_toml_from_config_argument(_, tmpdir): + with tmpdir.as_cwd(): + _not_root_path = tmpdir.mkdir("not_in_root").join("pyproject.toml") + _not_root_path.write("") + + with pytest.raises(ConfigFileIsEmpty): + config.read_cfg(filepath="./not_in_root/pyproject.toml") + + +@pytest.mark.parametrize( + "config_file, exception_string", + [ + (".cz.toml", r"\.cz\.toml"), + ("cz.toml", r"cz\.toml"), + ("pyproject.toml", r"pyproject\.toml"), + ], + ids=[".cz.toml", "cz.toml", "pyproject.toml"], +) +class TestTomlConfig: + def test_init_empty_config_content(self, tmpdir, config_file, exception_string): + path = tmpdir.mkdir("commitizen").join(config_file) + toml_config = config.TomlConfig(data="", path=path) + toml_config.init_empty_config_content() + + with open(path, encoding="utf-8") as toml_file: + assert toml_file.read() == "[tool.commitizen]\n" + + def test_init_empty_config_content_with_existing_content( + self, tmpdir, config_file, exception_string + ): + existing_content = "[tool.black]\nline-length = 88\n" + + path = tmpdir.mkdir("commitizen").join(config_file) + path.write(existing_content) + toml_config = config.TomlConfig(data="", path=path) + toml_config.init_empty_config_content() + + with open(path, encoding="utf-8") as toml_file: + assert toml_file.read() == existing_content + "\n[tool.commitizen]\n" + + def test_init_with_invalid_config_content( + self, tmpdir, config_file, exception_string + ): + existing_content = "invalid toml content" + path = tmpdir.mkdir("commitizen").join(config_file) + + with pytest.raises(InvalidConfigurationError, match=exception_string): + config.TomlConfig(data=existing_content, path=path) + + +@pytest.mark.parametrize( + "config_file, exception_string", + [ + (".cz.json", r"\.cz\.json"), + ("cz.json", r"cz\.json"), + ], + ids=[".cz.json", "cz.json"], +) +class TestJsonConfig: + def test_init_empty_config_content(self, tmpdir, config_file, exception_string): + path = tmpdir.mkdir("commitizen").join(config_file) + json_config = config.JsonConfig(data="{}", path=path) + json_config.init_empty_config_content() + + with open(path, encoding="utf-8") as json_file: + assert json.load(json_file) == {"commitizen": {}} + + def test_init_with_invalid_config_content( + self, tmpdir, config_file, exception_string + ): + existing_content = "invalid json content" + path = tmpdir.mkdir("commitizen").join(config_file) + + with pytest.raises(InvalidConfigurationError, match=exception_string): + config.JsonConfig(data=existing_content, path=path) + + +@pytest.mark.parametrize( + "config_file, exception_string", + [ + (".cz.yaml", r"\.cz\.yaml"), + ("cz.yaml", r"cz\.yaml"), + ], + ids=[".cz.yaml", "cz.yaml"], +) +class TestYamlConfig: + def test_init_empty_config_content(self, tmpdir, config_file, exception_string): + path = tmpdir.mkdir("commitizen").join(config_file) + yaml_config = config.YAMLConfig(data="{}", path=path) + yaml_config.init_empty_config_content() + + with open(path) as yaml_file: + assert yaml.safe_load(yaml_file) == {"commitizen": {}} + + def test_init_with_invalid_content(self, tmpdir, config_file, exception_string): + existing_content = "invalid: .cz.yaml: content: maybe?" + path = tmpdir.mkdir("commitizen").join(config_file) + + with pytest.raises(InvalidConfigurationError, match=exception_string): + config.YAMLConfig(data=existing_content, path=path) diff --git a/tests/test_cz_base.py b/tests/test_cz_base.py new file mode 100644 index 0000000..4ee1cc6 --- /dev/null +++ b/tests/test_cz_base.py @@ -0,0 +1,50 @@ +import pytest + +from commitizen.cz.base import BaseCommitizen + + +class DummyCz(BaseCommitizen): + def questions(self): + return [{"type": "input", "name": "commit", "message": "Initial commit:\n"}] + + def message(self, answers: dict): + return answers["commit"] + + +def test_base_raises_error(config): + with pytest.raises(TypeError): + BaseCommitizen(config) + + +def test_questions(config): + cz = DummyCz(config) + assert isinstance(cz.questions(), list) + + +def test_message(config): + cz = DummyCz(config) + assert cz.message({"commit": "holis"}) == "holis" + + +def test_example(config): + cz = DummyCz(config) + with pytest.raises(NotImplementedError): + cz.example() + + +def test_schema(config): + cz = DummyCz(config) + with pytest.raises(NotImplementedError): + cz.schema() + + +def test_info(config): + cz = DummyCz(config) + with pytest.raises(NotImplementedError): + cz.info() + + +def test_process_commit(config): + cz = DummyCz(config) + message = cz.process_commit("test(test_scope): this is test msg") + assert message == "test(test_scope): this is test msg" diff --git a/tests/test_cz_conventional_commits.py b/tests/test_cz_conventional_commits.py new file mode 100644 index 0000000..6d4e0f7 --- /dev/null +++ b/tests/test_cz_conventional_commits.py @@ -0,0 +1,155 @@ +import pytest + +from commitizen.cz.conventional_commits.conventional_commits import ( + ConventionalCommitsCz, + parse_scope, + parse_subject, +) +from commitizen.cz.exceptions import AnswerRequiredError + +valid_scopes = ["", "simple", "dash-separated", "camelCaseUPPERCASE"] + +scopes_transformations = [["with spaces", "with-spaces"], [None, ""]] + +valid_subjects = ["this is a normal text", "aword"] + +subjects_transformations = [["with dot.", "with dot"]] + +invalid_subjects = ["", " ", ".", " .", "", None] + + +def test_parse_scope_valid_values(): + for valid_scope in valid_scopes: + assert valid_scope == parse_scope(valid_scope) + + +def test_scopes_transformations(): + for scopes_transformation in scopes_transformations: + invalid_scope, transformed_scope = scopes_transformation + assert transformed_scope == parse_scope(invalid_scope) + + +def test_parse_subject_valid_values(): + for valid_subject in valid_subjects: + assert valid_subject == parse_subject(valid_subject) + + +def test_parse_subject_invalid_values(): + for valid_subject in invalid_subjects: + with pytest.raises(AnswerRequiredError): + parse_subject(valid_subject) + + +def test_subject_transformations(): + for subject_transformation in subjects_transformations: + invalid_subject, transformed_subject = subject_transformation + assert transformed_subject == parse_subject(invalid_subject) + + +def test_questions(config): + conventional_commits = ConventionalCommitsCz(config) + questions = conventional_commits.questions() + assert isinstance(questions, list) + assert isinstance(questions[0], dict) + + +def test_choices_all_have_keyboard_shortcuts(config): + conventional_commits = ConventionalCommitsCz(config) + questions = conventional_commits.questions() + + list_questions = (q for q in questions if q["type"] == "list") + for select in list_questions: + assert all("key" in choice for choice in select["choices"]) + + +def test_small_answer(config): + conventional_commits = ConventionalCommitsCz(config) + answers = { + "prefix": "fix", + "scope": "users", + "subject": "email pattern corrected", + "is_breaking_change": False, + "body": "", + "footer": "", + } + message = conventional_commits.message(answers) + assert message == "fix(users): email pattern corrected" + + +def test_long_answer(config): + conventional_commits = ConventionalCommitsCz(config) + answers = { + "prefix": "fix", + "scope": "users", + "subject": "email pattern corrected", + "is_breaking_change": False, + "body": "complete content", + "footer": "closes #24", + } + message = conventional_commits.message(answers) + assert ( + message + == "fix(users): email pattern corrected\n\ncomplete content\n\ncloses #24" # noqa + ) + + +def test_breaking_change_in_footer(config): + conventional_commits = ConventionalCommitsCz(config) + answers = { + "prefix": "fix", + "scope": "users", + "subject": "email pattern corrected", + "is_breaking_change": True, + "body": "complete content", + "footer": "migrate by renaming user to users", + } + message = conventional_commits.message(answers) + print(message) + assert ( + message + == "fix(users): email pattern corrected\n\ncomplete content\n\nBREAKING CHANGE: migrate by renaming user to users" # noqa + ) + + +def test_example(config): + """just testing a string is returned. not the content""" + conventional_commits = ConventionalCommitsCz(config) + example = conventional_commits.example() + assert isinstance(example, str) + + +def test_schema(config): + """just testing a string is returned. not the content""" + conventional_commits = ConventionalCommitsCz(config) + schema = conventional_commits.schema() + assert isinstance(schema, str) + + +def test_info(config): + """just testing a string is returned. not the content""" + conventional_commits = ConventionalCommitsCz(config) + info = conventional_commits.info() + assert isinstance(info, str) + + +@pytest.mark.parametrize( + ("commit_message", "expected_message"), + [ + ( + "test(test_scope): this is test msg", + "this is test msg", + ), + ( + "test(test_scope)!: this is test msg", + "this is test msg", + ), + ( + "test!(test_scope): this is test msg", + "", + ), + ], +) +def test_process_commit(commit_message, expected_message, config): + conventional_commits = ConventionalCommitsCz(config) + message = conventional_commits.process_commit(commit_message) + assert message == expected_message diff --git a/tests/test_cz_customize.py b/tests/test_cz_customize.py new file mode 100644 index 0000000..210c8b6 --- /dev/null +++ b/tests/test_cz_customize.py @@ -0,0 +1,600 @@ +import pytest + +from commitizen.config import BaseConfig, JsonConfig, TomlConfig, YAMLConfig +from commitizen.cz.customize import CustomizeCommitsCz +from commitizen.exceptions import MissingCzCustomizeConfigError + +TOML_STR = r""" + [tool.commitizen.customize] + message_template = "{{change_type}}:{% if show_message %} {{message}}{% endif %}" + example = "feature: this feature enables customization through a config file" + schema = "<type>: <body>" + schema_pattern = "(feature|bug fix):(\\s.*)" + commit_parser = "^(?P<change_type>feature|bug fix):\\s(?P<message>.*)?" + changelog_pattern = "^(feature|bug fix)?(!)?" + change_type_map = {"feature" = "Feat", "bug fix" = "Fix"} + + bump_pattern = "^(break|new|fix|hotfix)" + bump_map = {"break" = "MAJOR", "new" = "MINOR", "fix" = "PATCH", "hotfix" = "PATCH"} + change_type_order = ["perf", "BREAKING CHANGE", "feat", "fix", "refactor"] + info = "This is a customized cz." + + [[tool.commitizen.customize.questions]] + type = "list" + name = "change_type" + choices = [ + {value = "feature", name = "feature: A new feature."}, + {value = "bug fix", name = "bug fix: A bug fix."} + ] + message = "Select the type of change you are committing" + + [[tool.commitizen.customize.questions]] + type = "input" + name = "message" + message = "Body." + + [[tool.commitizen.customize.questions]] + type = "confirm" + name = "show_message" + message = "Do you want to add body message in commit?" +""" + +JSON_STR = r""" + { + "commitizen": { + "name": "cz_jira", + "version": "1.0.0", + "version_files": [ + "commitizen/__version__.py", + "pyproject.toml" + ], + "customize": { + "message_template": "{{change_type}}:{% if show_message %} {{message}}{% endif %}", + "example": "feature: this feature enables customization through a config file", + "schema": "<type>: <body>", + "schema_pattern": "(feature|bug fix):(\\s.*)", + "bump_pattern": "^(break|new|fix|hotfix)", + "bump_map": { + "break": "MAJOR", + "new": "MINOR", + "fix": "PATCH", + "hotfix": "PATCH" + }, + "commit_parser": "^(?P<change_type>feature|bug fix):\\s(?P<message>.*)?", + "changelog_pattern": "^(feature|bug fix)?(!)?", + "change_type_map": {"feature": "Feat", "bug fix": "Fix"}, + "change_type_order": ["perf", "BREAKING CHANGE", "feat", "fix", "refactor"], + "info": "This is a customized cz.", + "questions": [ + { + "type": "list", + "name": "change_type", + "choices": [ + { + "value": "feature", + "name": "feature: A new feature." + }, + { + "value": "bug fix", + "name": "bug fix: A bug fix." + } + ], + "message": "Select the type of change you are committing" + }, + { + "type": "input", + "name": "message", + "message": "Body." + }, + { + "type": "confirm", + "name": "show_message", + "message": "Do you want to add body message in commit?" + } + ] + } + } + } +""" + +YAML_STR = """ +commitizen: + name: cz_jira + version: 1.0.0 + version_files: + - commitizen/__version__.py + - pyproject.toml + customize: + message_template: "{{change_type}}:{% if show_message %} {{message}}{% endif %}" + example: 'feature: this feature enables customization through a config file' + schema: "<type>: <body>" + schema_pattern: "(feature|bug fix):(\\s.*)" + bump_pattern: "^(break|new|fix|hotfix)" + bump_map: + break: MAJOR + new: MINOR + fix: PATCH + hotfix: PATCH + change_type_order: ["perf", "BREAKING CHANGE", "feat", "fix", "refactor"] + info: This is a customized cz. + questions: + - type: list + name: change_type + choices: + - value: feature + name: 'feature: A new feature.' + - value: bug fix + name: 'bug fix: A bug fix.' + message: Select the type of change you are committing + - type: input + name: message + message: Body. + - type: confirm + name: show_message + message: Do you want to add body message in commit? +""" + +TOML_WITH_UNICODE = r""" + [tool.commitizen] + name = "cz_customize" + version = "1.0.0" + version_files = [ + "commitizen/__version__.py", + "pyproject.toml:version" + ] + [tool.commitizen.customize] + message_template = "{{change_type}}:{% if show_message %} {{message}}{% endif %}" + example = "โœจ feature: this feature enables customization through a config file" + schema = "<type>: <body>" + schema_pattern = "(โœจ feature|๐Ÿ› bug fix):(\\s.*)" + commit_parser = "^(?P<change_type>โœจ feature|๐Ÿ› bug fix):\\s(?P<message>.*)?" + changelog_pattern = "^(โœจ feature|๐Ÿ› bug fix)?(!)?" + change_type_map = {"โœจ feature" = "Feat", "๐Ÿ› bug fix" = "Fix"} + bump_pattern = "^(โœจ feat|๐Ÿ› bug fix)" + bump_map = {"break" = "MAJOR", "โœจ feat" = "MINOR", "๐Ÿ› bug fix" = "MINOR"} + change_type_order = ["perf", "BREAKING CHANGE", "feat", "fix", "refactor"] + info = "This is a customized cz with emojis ๐ŸŽ‰!" + [[tool.commitizen.customize.questions]] + type = "list" + name = "change_type" + choices = [ + {value = "โœจ feature", name = "โœจ feature: A new feature."}, + {value = "๐Ÿ› bug fix", name = "๐Ÿ› bug fix: A bug fix."} + ] + message = "Select the type of change you are committing" + [[tool.commitizen.customize.questions]] + type = "input" + name = "message" + message = "Body." + [[tool.commitizen.customize.questions]] + type = "confirm" + name = "show_message" + message = "Do you want to add body message in commit?" +""" + +JSON_WITH_UNICODE = r""" + { + "commitizen": { + "name": "cz_customize", + "version": "1.0.0", + "version_files": [ + "commitizen/__version__.py", + "pyproject.toml:version" + ], + "customize": { + "message_template": "{{change_type}}:{% if show_message %} {{message}}{% endif %}", + "example": "โœจ feature: this feature enables customization through a config file", + "schema": "<type>: <body>", + "schema_pattern": "(โœจ feature|๐Ÿ› bug fix):(\\s.*)", + "bump_pattern": "^(โœจ feat|๐Ÿ› bug fix)", + "bump_map": { + "break": "MAJOR", + "โœจ feat": "MINOR", + "๐Ÿ› bug fix": "MINOR" + }, + "commit_parser": "^(?P<change_type>โœจ feature|๐Ÿ› bug fix):\\s(?P<message>.*)?", + "changelog_pattern": "^(โœจ feature|๐Ÿ› bug fix)?(!)?", + "change_type_map": {"โœจ feature": "Feat", "๐Ÿ› bug fix": "Fix"}, + "change_type_order": ["perf", "BREAKING CHANGE", "feat", "fix", "refactor"], + "info": "This is a customized cz with emojis ๐ŸŽ‰!", + "questions": [ + { + "type": "list", + "name": "change_type", + "choices": [ + { + "value": "โœจ feature", + "name": "โœจ feature: A new feature." + }, + { + "value": "๐Ÿ› bug fix", + "name": "๐Ÿ› bug fix: A bug fix." + } + ], + "message": "Select the type of change you are committing" + }, + { + "type": "input", + "name": "message", + "message": "Body." + }, + { + "type": "confirm", + "name": "show_message", + "message": "Do you want to add body message in commit?" + } + ] + } + } + } +""" + +TOML_STR_INFO_PATH = """ + [tool.commitizen.customize] + message_template = "{{change_type}}:{% if show_message %} {{message}}{% endif %}" + example = "feature: this feature enables customization through a config file" + schema = "<type>: <body>" + bump_pattern = "^(break|new|fix|hotfix)" + bump_map = {"break" = "MAJOR", "new" = "MINOR", "fix" = "PATCH", "hotfix" = "PATCH"} + info_path = "info.txt" +""" + +JSON_STR_INFO_PATH = r""" + { + "commitizen": { + "customize": { + "message_template": "{{change_type}}:{% if show_message %} {{message}}{% endif %}", + "example": "feature: this feature enables customization through a config file", + "schema": "<type>: <body>", + "bump_pattern": "^(break|new|fix|hotfix)", + "bump_map": { + "break": "MAJOR", + "new": "MINOR", + "fix": "PATCH", + "hotfix": "PATCH" + }, + "info_path": "info.txt" + } + } + } +""" + +YAML_STR_INFO_PATH = """ +commitizen: + customize: + message_template: "{{change_type}}:{% if show_message %} {{message}}{% endif %}" + example: 'feature: this feature enables customization through a config file' + schema: "<type>: <body>" + bump_pattern: "^(break|new|fix|hotfix)" + bump_map: + break: MAJOR + new: MINOR + fix: PATCH + hotfix: PATCH + info_path: info.txt +""" + +TOML_STR_WITHOUT_INFO = """ + [tool.commitizen.customize] + message_template = "{{change_type}}:{% if show_message %} {{message}}{% endif %}" + example = "feature: this feature enables customization through a config file" + schema = "<type>: <body>" + bump_pattern = "^(break|new|fix|hotfix)" + bump_map = {"break" = "MAJOR", "new" = "MINOR", "fix" = "PATCH", "hotfix" = "PATCH"} +""" + +JSON_STR_WITHOUT_PATH = r""" + { + "commitizen": { + "customize": { + "message_template": "{{change_type}}:{% if show_message %} {{message}}{% endif %}", + "example": "feature: this feature enables customization through a config file", + "schema": "<type>: <body>", + "bump_pattern": "^(break|new|fix|hotfix)", + "bump_map": { + "break": "MAJOR", + "new": "MINOR", + "fix": "PATCH", + "hotfix": "PATCH" + } + } + } + } +""" + +YAML_STR_WITHOUT_PATH = """ +commitizen: + customize: + message_template: "{{change_type}}:{% if show_message %} {{message}}{% endif %}" + example: 'feature: this feature enables customization through a config file' + schema: "<type>: <body>" + bump_pattern: "^(break|new|fix|hotfix)" + bump_map: + break: MAJOR + new: MINOR + fix: PATCH + hotfix: PATCH +""" + + +@pytest.fixture( + params=[ + TomlConfig(data=TOML_STR, path="not_exist.toml"), + JsonConfig(data=JSON_STR, path="not_exist.json"), + ] +) +def config(request): + """Parametrize the config fixture + + This fixture allow to test multiple config formats, + without add the builtin parametrize decorator + """ + return request.param + + +@pytest.fixture( + params=[ + TomlConfig(data=TOML_STR_INFO_PATH, path="not_exist.toml"), + JsonConfig(data=JSON_STR_INFO_PATH, path="not_exist.json"), + YAMLConfig(data=YAML_STR_INFO_PATH, path="not_exist.yaml"), + ] +) +def config_info(request): + return request.param + + +@pytest.fixture( + params=[ + TomlConfig(data=TOML_STR_WITHOUT_INFO, path="not_exist.toml"), + JsonConfig(data=JSON_STR_WITHOUT_PATH, path="not_exist.json"), + YAMLConfig(data=YAML_STR_WITHOUT_PATH, path="not_exist.yaml"), + ] +) +def config_without_info(request): + return request.param + + +@pytest.fixture( + params=[ + TomlConfig(data=TOML_WITH_UNICODE, path="not_exist.toml"), + JsonConfig(data=JSON_WITH_UNICODE, path="not_exist.json"), + ] +) +def config_with_unicode(request): + return request.param + + +def test_initialize_cz_customize_failed(): + with pytest.raises(MissingCzCustomizeConfigError) as excinfo: + config = BaseConfig() + _ = CustomizeCommitsCz(config) + + assert MissingCzCustomizeConfigError.message in str(excinfo.value) + + +def test_bump_pattern(config): + cz = CustomizeCommitsCz(config) + assert cz.bump_pattern == "^(break|new|fix|hotfix)" + + +def test_bump_pattern_unicode(config_with_unicode): + cz = CustomizeCommitsCz(config_with_unicode) + assert cz.bump_pattern == "^(โœจ feat|๐Ÿ› bug fix)" + + +def test_bump_map(config): + cz = CustomizeCommitsCz(config) + assert cz.bump_map == { + "break": "MAJOR", + "new": "MINOR", + "fix": "PATCH", + "hotfix": "PATCH", + } + + +def test_bump_map_unicode(config_with_unicode): + cz = CustomizeCommitsCz(config_with_unicode) + assert cz.bump_map == { + "break": "MAJOR", + "โœจ feat": "MINOR", + "๐Ÿ› bug fix": "MINOR", + } + + +def test_change_type_order(config): + cz = CustomizeCommitsCz(config) + assert cz.change_type_order == [ + "perf", + "BREAKING CHANGE", + "feat", + "fix", + "refactor", + ] + + +def test_change_type_order_unicode(config_with_unicode): + cz = CustomizeCommitsCz(config_with_unicode) + assert cz.change_type_order == [ + "perf", + "BREAKING CHANGE", + "feat", + "fix", + "refactor", + ] + + +def test_questions(config): + cz = CustomizeCommitsCz(config) + questions = cz.questions() + expected_questions = [ + { + "type": "list", + "name": "change_type", + "choices": [ + {"value": "feature", "name": "feature: A new feature."}, + {"value": "bug fix", "name": "bug fix: A bug fix."}, + ], + "message": "Select the type of change you are committing", + }, + {"type": "input", "name": "message", "message": "Body."}, + { + "type": "confirm", + "name": "show_message", + "message": "Do you want to add body message in commit?", + }, + ] + assert list(questions) == expected_questions + + +def test_questions_unicode(config_with_unicode): + cz = CustomizeCommitsCz(config_with_unicode) + questions = cz.questions() + expected_questions = [ + { + "type": "list", + "name": "change_type", + "choices": [ + {"value": "โœจ feature", "name": "โœจ feature: A new feature."}, + {"value": "๐Ÿ› bug fix", "name": "๐Ÿ› bug fix: A bug fix."}, + ], + "message": "Select the type of change you are committing", + }, + {"type": "input", "name": "message", "message": "Body."}, + { + "type": "confirm", + "name": "show_message", + "message": "Do you want to add body message in commit?", + }, + ] + assert list(questions) == expected_questions + + +def test_answer(config): + cz = CustomizeCommitsCz(config) + answers = { + "change_type": "feature", + "message": "this feature enaable customize through config file", + "show_message": True, + } + message = cz.message(answers) + assert message == "feature: this feature enaable customize through config file" + + cz = CustomizeCommitsCz(config) + answers = { + "change_type": "feature", + "message": "this feature enaable customize through config file", + "show_message": False, + } + message = cz.message(answers) + assert message == "feature:" + + +def test_answer_unicode(config_with_unicode): + cz = CustomizeCommitsCz(config_with_unicode) + answers = { + "change_type": "โœจ feature", + "message": "this feature enables customization through a config file", + "show_message": True, + } + message = cz.message(answers) + assert ( + message + == "โœจ feature: this feature enables customization through a config file" + ) + + cz = CustomizeCommitsCz(config_with_unicode) + answers = { + "change_type": "โœจ feature", + "message": "this feature enables customization through a config file", + "show_message": False, + } + message = cz.message(answers) + assert message == "โœจ feature:" + + +def test_example(config): + cz = CustomizeCommitsCz(config) + assert ( + "feature: this feature enables customization through a config file" + in cz.example() + ) + + +def test_example_unicode(config_with_unicode): + cz = CustomizeCommitsCz(config_with_unicode) + assert ( + "โœจ feature: this feature enables customization through a config file" + in cz.example() + ) + + +def test_schema(config): + cz = CustomizeCommitsCz(config) + assert "<type>: <body>" in cz.schema() + + +def test_schema_pattern(config): + cz = CustomizeCommitsCz(config) + assert r"(feature|bug fix):(\s.*)" in cz.schema_pattern() + + +def test_schema_pattern_unicode(config_with_unicode): + cz = CustomizeCommitsCz(config_with_unicode) + assert r"(โœจ feature|๐Ÿ› bug fix):(\s.*)" in cz.schema_pattern() + + +def test_info(config): + cz = CustomizeCommitsCz(config) + assert "This is a customized cz." in cz.info() + + +def test_info_unicode(config_with_unicode): + cz = CustomizeCommitsCz(config_with_unicode) + assert "This is a customized cz with emojis ๐ŸŽ‰!" in cz.info() + + +def test_info_with_info_path(tmpdir, config_info): + with tmpdir.as_cwd(): + tmpfile = tmpdir.join("info.txt") + tmpfile.write("Test info") + + cz = CustomizeCommitsCz(config_info) + assert "Test info" in cz.info() + + +def test_info_without_info(config_without_info): + cz = CustomizeCommitsCz(config_without_info) + assert cz.info() == "" + + +def test_commit_parser(config): + cz = CustomizeCommitsCz(config) + assert cz.commit_parser == "^(?P<change_type>feature|bug fix):\\s(?P<message>.*)?" + + +def test_commit_parser_unicode(config_with_unicode): + cz = CustomizeCommitsCz(config_with_unicode) + assert ( + cz.commit_parser + == "^(?P<change_type>โœจ feature|๐Ÿ› bug fix):\\s(?P<message>.*)?" + ) + + +def test_changelog_pattern(config): + cz = CustomizeCommitsCz(config) + assert cz.changelog_pattern == "^(feature|bug fix)?(!)?" + + +def test_changelog_pattern_unicode(config_with_unicode): + cz = CustomizeCommitsCz(config_with_unicode) + assert cz.changelog_pattern == "^(โœจ feature|๐Ÿ› bug fix)?(!)?" + + +def test_change_type_map(config): + cz = CustomizeCommitsCz(config) + assert cz.change_type_map == {"feature": "Feat", "bug fix": "Fix"} + + +def test_change_type_map_unicode(config_with_unicode): + cz = CustomizeCommitsCz(config_with_unicode) + assert cz.change_type_map == {"โœจ feature": "Feat", "๐Ÿ› bug fix": "Fix"} diff --git a/tests/test_cz_jira.py b/tests/test_cz_jira.py new file mode 100644 index 0000000..03055c1 --- /dev/null +++ b/tests/test_cz_jira.py @@ -0,0 +1,39 @@ +from commitizen.cz.jira import JiraSmartCz + + +def test_questions(config): + cz = JiraSmartCz(config) + questions = cz.questions() + assert isinstance(questions, list) + assert isinstance(questions[0], dict) + + +def test_answer(config): + cz = JiraSmartCz(config) + answers = { + "message": "new test", + "issues": "JRA-34", + "workflow": "", + "time": "", + "comment": "", + } + message = cz.message(answers) + assert message == "new test JRA-34" + + +def test_example(config): + cz = JiraSmartCz(config) + assert "JRA-34 #comment corrected indent issue\n" in cz.example() + + +def test_schema(config): + cz = JiraSmartCz(config) + assert "<ignored text>" in cz.schema() + + +def test_info(config): + cz = JiraSmartCz(config) + assert ( + "Smart Commits allow repository committers to perform " + "actions such as transitioning JIRA Software" + ) in cz.info() diff --git a/tests/test_cz_utils.py b/tests/test_cz_utils.py new file mode 100644 index 0000000..25c960c --- /dev/null +++ b/tests/test_cz_utils.py @@ -0,0 +1,26 @@ +import pytest +from pytest_mock import MockFixture + +from commitizen.cz import exceptions, utils + + +def test_required_validator(): + assert utils.required_validator("test") == "test" + + with pytest.raises(exceptions.AnswerRequiredError): + utils.required_validator("") + + +def test_multiple_line_breaker(): + message = "this is the first line | and this is the second line " + result = utils.multiple_line_breaker(message) + assert result == "this is the first line\nand this is the second line" + + result = utils.multiple_line_breaker(message, "is") + assert result == "th\n\nthe first line | and th\n\nthe second line" + + +def test_get_backup_file_path_no_project_root(mocker: MockFixture): + project_root_mock = mocker.patch("commitizen.git.find_git_project_root") + project_root_mock.return_value = None + assert utils.get_backup_file_path() diff --git a/tests/test_factory.py b/tests/test_factory.py new file mode 100644 index 0000000..390742f --- /dev/null +++ b/tests/test_factory.py @@ -0,0 +1,97 @@ +import sys +from textwrap import dedent + +if sys.version_info >= (3, 10): + from importlib import metadata +else: + import importlib_metadata as metadata + +import pytest + +from commitizen import BaseCommitizen, defaults, factory +from commitizen.config import BaseConfig +from commitizen.cz import discover_plugins +from commitizen.cz.conventional_commits import ConventionalCommitsCz +from commitizen.cz.customize import CustomizeCommitsCz +from commitizen.cz.jira import JiraSmartCz +from commitizen.exceptions import NoCommitizenFoundException + + +class Plugin: + pass + + +class OtherPlugin: + pass + + +def test_factory(): + config = BaseConfig() + config.settings.update({"name": defaults.DEFAULT_SETTINGS["name"]}) + r = factory.commiter_factory(config) + assert isinstance(r, BaseCommitizen) + + +def test_factory_fails(): + config = BaseConfig() + config.settings.update({"name": "Nothing"}) + with pytest.raises(NoCommitizenFoundException) as excinfo: + factory.commiter_factory(config) + + assert "The committer has not been found in the system." in str(excinfo) + + +def test_discover_plugins(tmp_path): + legacy_plugin_folder = tmp_path / "cz_legacy" + legacy_plugin_folder.mkdir() + init_file = legacy_plugin_folder / "__init__.py" + init_file.write_text( + dedent( + """\ + class Plugin: pass + + discover_this = Plugin + """ + ) + ) + + sys.path.append(tmp_path.as_posix()) + with pytest.warns(UserWarning) as record: + discovered_plugins = discover_plugins([tmp_path.as_posix()]) + sys.path.pop() + + assert ( + record[0].message.args[0] + == "Legacy plugin 'cz_legacy' has been ignored: please expose it the 'commitizen.plugin' entrypoint" + ) + assert "cz_legacy" not in discovered_plugins + + +def test_discover_external_plugin(mocker): + ep_plugin = metadata.EntryPoint( + "test", "tests.test_factory:Plugin", "commitizen.plugin" + ) + ep_other_plugin = metadata.EntryPoint( + "not-selected", "tests.test_factory::OtherPlugin", "commitizen.not_a_plugin" + ) + eps = [ep_plugin, ep_other_plugin] + + def mock_entrypoints(**kwargs): + group = kwargs.get("group") + return metadata.EntryPoints(ep for ep in eps if ep.group == group) + + mocker.patch.object(metadata, "entry_points", side_effect=mock_entrypoints) + + assert discover_plugins() == {"test": Plugin} + + +def test_discover_internal_plugins(): + expected = { + "cz_conventional_commits": ConventionalCommitsCz, + "cz_jira": JiraSmartCz, + "cz_customize": CustomizeCommitsCz, + } + + discovered = discover_plugins() + + assert set(expected.items()).issubset(set(discovered.items())) diff --git a/tests/test_git.py b/tests/test_git.py new file mode 100644 index 0000000..8b2fc2b --- /dev/null +++ b/tests/test_git.py @@ -0,0 +1,403 @@ +from __future__ import annotations + +import inspect +import os +import platform +import shutil + +import pytest +from pytest_mock import MockFixture + +from commitizen import cmd, exceptions, git +from tests.utils import ( + FakeCommand, + create_branch, + create_file_and_commit, + create_tag, + switch_branch, +) + + +def test_git_object_eq(): + git_commit = git.GitCommit( + rev="sha1-code", title="this is title", body="this is body" + ) + git_tag = git.GitTag(rev="sha1-code", name="0.0.1", date="2020-01-21") + + assert git_commit == git_tag + assert git_commit != "sha1-code" + + +def test_get_tags(mocker: MockFixture): + tag_str = ( + "v1.0.0---inner_delimiter---333---inner_delimiter---2020-01-20---inner_delimiter---\n" + "v0.5.0---inner_delimiter---222---inner_delimiter---2020-01-17---inner_delimiter---\n" + "v0.0.1---inner_delimiter---111---inner_delimiter---2020-01-17---inner_delimiter---\n" + ) + mocker.patch("commitizen.cmd.run", return_value=FakeCommand(out=tag_str)) + + git_tags = git.get_tags() + latest_git_tag = git_tags[0] + assert latest_git_tag.rev == "333" + assert latest_git_tag.name == "v1.0.0" + assert latest_git_tag.date == "2020-01-20" + + mocker.patch( + "commitizen.cmd.run", return_value=FakeCommand(out="", err="No tag available") + ) + assert git.get_tags() == [] + + +def test_get_reachable_tags(tmp_commitizen_project): + with tmp_commitizen_project.as_cwd(): + create_file_and_commit("Initial state") + create_tag("1.0.0") + # create develop + create_branch("develop") + switch_branch("develop") + + # add a feature to develop + create_file_and_commit("develop") + create_tag("1.1.0b0") + + # create staging + switch_branch("master") + create_file_and_commit("master") + create_tag("1.0.1") + + tags = git.get_tags(reachable_only=True) + tag_names = set([t.name for t in tags]) + # 1.1.0b0 is not present + assert tag_names == {"1.0.0", "1.0.1"} + + +@pytest.mark.parametrize("locale", ["en_US", "fr_FR"]) +def test_get_reachable_tags_with_commits( + tmp_commitizen_project, locale: str, monkeypatch: pytest.MonkeyPatch +): + monkeypatch.setenv("LANG", f"{locale}.UTF-8") + monkeypatch.setenv("LANGUAGE", f"{locale}.UTF-8") + monkeypatch.setenv("LC_ALL", f"{locale}.UTF-8") + with tmp_commitizen_project.as_cwd(): + tags = git.get_tags(reachable_only=True) + assert tags == [] + + +def test_get_tag_names(mocker: MockFixture): + tag_str = "v1.0.0\nv0.5.0\nv0.0.1\n" + mocker.patch("commitizen.cmd.run", return_value=FakeCommand(out=tag_str)) + + assert git.get_tag_names() == ["v1.0.0", "v0.5.0", "v0.0.1"] + + mocker.patch( + "commitizen.cmd.run", return_value=FakeCommand(out="", err="No tag available") + ) + assert git.get_tag_names() == [] + + +def test_git_message_with_empty_body(): + commit_title = "Some Title" + commit = git.GitCommit("test_rev", "Some Title", body="") + + assert commit.message == commit_title + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_get_log_as_str_list_empty(): + """ensure an exception or empty list in an empty project""" + try: + gitlog = git._get_log_as_str_list(start=None, end="HEAD", args="") + except exceptions.GitCommandError: + return + assert len(gitlog) == 0, "list should be empty if no assert" + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_get_commits(): + create_file_and_commit("feat(users): add username") + create_file_and_commit("fix: username exception") + commits = git.get_commits() + assert len(commits) == 2 + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_get_commits_author_and_email(): + create_file_and_commit("fix: username exception") + commit = git.get_commits()[0] + + assert commit.author != "" + assert "@" in commit.author_email + + +def test_get_commits_without_email(mocker: MockFixture): + raw_commit = ( + "a515bb8f71c403f6f7d1c17b9d8ebf2ce3959395\n" + "95bbfc703eb99cb49ba0d6ffd8469911303dbe63 12d3b4bdaa996ea7067a07660bb5df4772297bdd\n" + "\n" + "user name\n" + "\n" + "----------commit-delimiter----------\n" + "12d3b4bdaa996ea7067a07660bb5df4772297bdd\n" + "de33bc5070de19600f2f00262b3c15efea762408\n" + "feat(users): add username\n" + "user name\n" + "\n" + "----------commit-delimiter----------\n" + ) + mocker.patch("commitizen.cmd.run", return_value=FakeCommand(out=raw_commit)) + + commits = git.get_commits() + + assert commits[0].author == "user name" + assert commits[1].author == "user name" + + assert commits[0].author_email == "" + assert commits[1].author_email == "" + + assert commits[0].title == "" + assert commits[1].title == "feat(users): add username" + + +def test_get_commits_without_breakline_in_each_commit(mocker: MockFixture): + raw_commit = ( + "ae9ba6fc5526cf478f52ef901418d85505109744\n" + "ff2f56ca844de72a9d59590831087bf5a97bac84\n" + "bump: version 2.13.0 โ†’ 2.14.0\n" + "GitHub Action\n" + "action@github.com\n" + "----------commit-delimiter----------\n" + "ff2f56ca844de72a9d59590831087bf5a97bac84\n" + "b4dc83284dc8c9729032a774a037df1d1f2397d5 20a54bf1b82cd7b573351db4d1e8814dd0be205d\n" + "Merge pull request #332 from cliles/feature/271-redux\n" + "User\n" + "user@email.com\n" + "Feature/271 redux----------commit-delimiter----------\n" + "20a54bf1b82cd7b573351db4d1e8814dd0be205d\n" + "658f38c3fe832cdab63ed4fb1f7b3a0969a583be\n" + "feat(#271): enable creation of annotated tags when bumping\n" + "User 2\n" + "user@email.edu\n" + "----------commit-delimiter----------\n" + ) + mocker.patch("commitizen.cmd.run", return_value=FakeCommand(out=raw_commit)) + + commits = git.get_commits() + + assert commits[0].author == "GitHub Action" + assert commits[1].author == "User" + assert commits[2].author == "User 2" + + assert commits[0].author_email == "action@github.com" + assert commits[1].author_email == "user@email.com" + assert commits[2].author_email == "user@email.edu" + + assert commits[0].title == "bump: version 2.13.0 โ†’ 2.14.0" + assert commits[1].title == "Merge pull request #332 from cliles/feature/271-redux" + assert ( + commits[2].title == "feat(#271): enable creation of annotated tags when bumping" + ) + + +def test_get_commits_with_and_without_parents(mocker: MockFixture): + raw_commit = ( + "4206e661bacf9643373255965f34bbdb382cb2b9\n" + "ae9ba6fc5526cf478f52ef901418d85505109744 bf8479e7aa1a5b9d2f491b79e3a4d4015519903e\n" + "Merge pull request from someone\n" + "Maintainer\n" + "maintainer@email.com\n" + "This is a much needed feature----------commit-delimiter----------\n" + "ae9ba6fc5526cf478f52ef901418d85505109744\n" + "ff2f56ca844de72a9d59590831087bf5a97bac84\n" + "Release 0.1.0\n" + "GitHub Action\n" + "action@github.com\n" + "----------commit-delimiter----------\n" + "ff2f56ca844de72a9d59590831087bf5a97bac84\n" + "\n" + "Initial commit\n" + "User\n" + "user@email.com\n" + "----------commit-delimiter----------\n" + ) + mocker.patch("commitizen.cmd.run", return_value=FakeCommand(out=raw_commit)) + + commits = git.get_commits() + + assert commits[0].author == "Maintainer" + assert commits[1].author == "GitHub Action" + assert commits[2].author == "User" + + assert commits[0].author_email == "maintainer@email.com" + assert commits[1].author_email == "action@github.com" + assert commits[2].author_email == "user@email.com" + + assert commits[0].title == "Merge pull request from someone" + assert commits[1].title == "Release 0.1.0" + assert commits[2].title == "Initial commit" + + assert commits[0].body == "This is a much needed feature" + assert commits[1].body == "" + assert commits[2].body == "" + + assert commits[0].parents == [ + "ae9ba6fc5526cf478f52ef901418d85505109744", + "bf8479e7aa1a5b9d2f491b79e3a4d4015519903e", + ] + assert commits[1].parents == ["ff2f56ca844de72a9d59590831087bf5a97bac84"] + assert commits[2].parents == [] + + +def test_get_commits_with_signature(): + config_file = ".git/config" + config_backup = ".git/config.bak" + shutil.copy(config_file, config_backup) + + try: + # temporarily turn on --show-signature + cmd.run("git config log.showsignature true") + + # retrieve a commit that we know has a signature + commit = git.get_commits( + start="bec20ebf433f2281c70f1eb4b0b6a1d0ed83e9b2", + end="9eae518235d051f145807ddf971ceb79ad49953a", + )[0] + + assert commit.title.startswith("fix") + finally: + # restore the repo's original config + shutil.move(config_backup, config_file) + + +def test_get_tag_names_has_correct_arrow_annotation(): + arrow_annotation = inspect.getfullargspec(git.get_tag_names).annotations["return"] + + assert arrow_annotation == "list[str | None]" + + +def test_get_latest_tag_name(tmp_commitizen_project): + with tmp_commitizen_project.as_cwd(): + tag_name = git.get_latest_tag_name() + assert tag_name is None + + create_file_and_commit("feat(test): test") + cmd.run("git tag 1.0") + tag_name = git.get_latest_tag_name() + assert tag_name == "1.0" + + +def test_is_staging_clean_when_adding_file(tmp_commitizen_project): + with tmp_commitizen_project.as_cwd(): + assert git.is_staging_clean() is True + + cmd.run("touch test_file") + + assert git.is_staging_clean() is True + + cmd.run("git add test_file") + + assert git.is_staging_clean() is False + + +def test_is_staging_clean_when_updating_file(tmp_commitizen_project): + with tmp_commitizen_project.as_cwd(): + assert git.is_staging_clean() is True + + cmd.run("touch test_file") + cmd.run("git add test_file") + if os.name == "nt": + cmd.run('git commit -m "add test_file"') + else: + cmd.run("git commit -m 'add test_file'") + cmd.run("echo 'test' > test_file") + + assert git.is_staging_clean() is True + + cmd.run("git add test_file") + + assert git.is_staging_clean() is False + + +def test_git_eol_style(tmp_commitizen_project): + with tmp_commitizen_project.as_cwd(): + assert git.get_eol_style() == git.EOLTypes.NATIVE + + cmd.run("git config core.eol lf") + assert git.get_eol_style() == git.EOLTypes.LF + + cmd.run("git config core.eol crlf") + assert git.get_eol_style() == git.EOLTypes.CRLF + + cmd.run("git config core.eol native") + assert git.get_eol_style() == git.EOLTypes.NATIVE + + +def test_eoltypes_get_eol_for_open(): + assert git.EOLTypes.get_eol_for_open(git.EOLTypes.NATIVE) == os.linesep + assert git.EOLTypes.get_eol_for_open(git.EOLTypes.LF) == "\n" + assert git.EOLTypes.get_eol_for_open(git.EOLTypes.CRLF) == "\r\n" + + +def test_get_core_editor(mocker): + mocker.patch.dict(os.environ, {"GIT_EDITOR": "nano"}) + assert git.get_core_editor() == "nano" + + mocker.patch.dict(os.environ, clear=True) + mocker.patch( + "commitizen.cmd.run", + return_value=cmd.Command( + out="vim", err="", stdout=b"", stderr=b"", return_code=0 + ), + ) + assert git.get_core_editor() == "vim" + + mocker.patch( + "commitizen.cmd.run", + return_value=cmd.Command(out="", err="", stdout=b"", stderr=b"", return_code=1), + ) + assert git.get_core_editor() is None + + +def test_create_tag_with_message(tmp_commitizen_project): + with tmp_commitizen_project.as_cwd(): + create_file_and_commit("feat(test): test") + tag_name = "1.0" + tag_message = "test message" + create_tag(tag_name, tag_message) + assert git.get_latest_tag_name() == tag_name + assert git.get_tag_message(tag_name) == ( + tag_message if platform.system() != "Windows" else f"'{tag_message}'" + ) + + +@pytest.mark.parametrize( + "file_path,expected_cmd", + [ + ( + "/tmp/temp file", + 'git commit --signoff -F "/tmp/temp file"', + ), + ( + "/tmp dir/temp file", + 'git commit --signoff -F "/tmp dir/temp file"', + ), + ( + "/tmp/tempfile", + 'git commit --signoff -F "/tmp/tempfile"', + ), + ], + ids=[ + "File contains spaces", + "Path contains spaces", + "Path does not contain spaces", + ], +) +def test_commit_with_spaces_in_path(mocker, file_path, expected_cmd): + mock_run = mocker.patch("commitizen.cmd.run", return_value=FakeCommand()) + mock_unlink = mocker.patch("os.unlink") + mock_temp_file = mocker.patch("commitizen.git.NamedTemporaryFile") + mock_temp_file.return_value.name = file_path + + git.commit("feat: new feature", "--signoff") + + mock_run.assert_called_once_with(expected_cmd) + mock_unlink.assert_called_once_with(file_path) diff --git a/tests/test_version_scheme_pep440.py b/tests/test_version_scheme_pep440.py new file mode 100644 index 0000000..6b1f621 --- /dev/null +++ b/tests/test_version_scheme_pep440.py @@ -0,0 +1,338 @@ +import itertools +import random + +import pytest + +from commitizen.version_schemes import Pep440, VersionProtocol + +simple_flow = [ + (("0.1.0", "PATCH", None, 0, None), "0.1.1"), + (("0.1.0", "PATCH", None, 0, 1), "0.1.1.dev1"), + (("0.1.1", "MINOR", None, 0, None), "0.2.0"), + (("0.2.0", "MINOR", None, 0, None), "0.3.0"), + (("0.2.0", "MINOR", None, 0, 1), "0.3.0.dev1"), + (("0.3.0", "PATCH", None, 0, None), "0.3.1"), + (("0.3.0", "PATCH", "alpha", 0, None), "0.3.1a0"), + (("0.3.1a0", None, "alpha", 0, None), "0.3.1a1"), + (("0.3.0", "PATCH", "alpha", 1, None), "0.3.1a1"), + (("0.3.1a0", None, "alpha", 1, None), "0.3.1a1"), + (("0.3.1a0", None, None, 0, None), "0.3.1"), + (("0.3.1", "PATCH", None, 0, None), "0.3.2"), + (("0.4.2", "MAJOR", "alpha", 0, None), "1.0.0a0"), + (("1.0.0a0", None, "alpha", 0, None), "1.0.0a1"), + (("1.0.0a1", None, "alpha", 0, None), "1.0.0a2"), + (("1.0.0a1", None, "alpha", 0, 1), "1.0.0a2.dev1"), + (("1.0.0a2.dev0", None, "alpha", 0, 1), "1.0.0a3.dev1"), + (("1.0.0a2.dev0", None, "alpha", 0, 0), "1.0.0a3.dev0"), + (("1.0.0a1", None, "beta", 0, None), "1.0.0b0"), + (("1.0.0b0", None, "beta", 0, None), "1.0.0b1"), + (("1.0.0b1", None, "rc", 0, None), "1.0.0rc0"), + (("1.0.0rc0", None, "rc", 0, None), "1.0.0rc1"), + (("1.0.0rc0", None, "rc", 0, 1), "1.0.0rc1.dev1"), + (("1.0.0rc0", "PATCH", None, 0, None), "1.0.0"), + (("1.0.0a3.dev0", None, "beta", 0, None), "1.0.0b0"), + (("1.0.0", "PATCH", None, 0, None), "1.0.1"), + (("1.0.1", "PATCH", None, 0, None), "1.0.2"), + (("1.0.2", "MINOR", None, 0, None), "1.1.0"), + (("1.1.0", "MINOR", None, 0, None), "1.2.0"), + (("1.2.0", "PATCH", None, 0, None), "1.2.1"), + (("1.2.1", "MAJOR", None, 0, None), "2.0.0"), +] + +local_versions = [ + (("4.5.0+0.1.0", "PATCH", None, 0, None), "4.5.0+0.1.1"), + (("4.5.0+0.1.1", "MINOR", None, 0, None), "4.5.0+0.2.0"), + (("4.5.0+0.2.0", "MAJOR", None, 0, None), "4.5.0+1.0.0"), +] + +# never bump backwards on pre-releases +linear_prerelease_cases = [ + (("0.1.1b1", None, "alpha", 0, None), "0.1.1b2"), + (("0.1.1rc0", None, "alpha", 0, None), "0.1.1rc1"), + (("0.1.1rc0", None, "beta", 0, None), "0.1.1rc1"), +] + +weird_cases = [ + (("1.1", "PATCH", None, 0, None), "1.1.1"), + (("1", "MINOR", None, 0, None), "1.1.0"), + (("1", "MAJOR", None, 0, None), "2.0.0"), + (("1a0", None, "alpha", 0, None), "1.0.0a1"), + (("1a0", None, "alpha", 1, None), "1.0.0a1"), + (("1", None, "beta", 0, None), "1.0.0b0"), + (("1", None, "beta", 1, None), "1.0.0b1"), + (("1beta", None, "beta", 0, None), "1.0.0b1"), + (("1.0.0alpha1", None, "alpha", 0, None), "1.0.0a2"), + (("1", None, "rc", 0, None), "1.0.0rc0"), + (("1.0.0rc1+e20d7b57f3eb", "PATCH", None, 0, None), "1.0.0"), +] + +# test driven development +tdd_cases = [ + (("0.1.1", "PATCH", None, 0, None), "0.1.2"), + (("0.1.1", "MINOR", None, 0, None), "0.2.0"), + (("2.1.1", "MAJOR", None, 0, None), "3.0.0"), + (("0.9.0", "PATCH", "alpha", 0, None), "0.9.1a0"), + (("0.9.0", "MINOR", "alpha", 0, None), "0.10.0a0"), + (("0.9.0", "MAJOR", "alpha", 0, None), "1.0.0a0"), + (("0.9.0", "MAJOR", "alpha", 1, None), "1.0.0a1"), + (("1.0.0a2", None, "beta", 0, None), "1.0.0b0"), + (("1.0.0a2", None, "beta", 1, None), "1.0.0b1"), + (("1.0.0beta1", None, "rc", 0, None), "1.0.0rc0"), + (("1.0.0rc1", None, "rc", 0, None), "1.0.0rc2"), +] + +# additional pre-release tests run through various release scenarios +prerelease_cases = [ + # + (("3.3.3", "PATCH", "alpha", 0, None), "3.3.4a0"), + (("3.3.4a0", "PATCH", "alpha", 0, None), "3.3.4a1"), + (("3.3.4a1", "MINOR", "alpha", 0, None), "3.4.0a0"), + (("3.4.0a0", "PATCH", "alpha", 0, None), "3.4.0a1"), + (("3.4.0a1", "MINOR", "alpha", 0, None), "3.4.0a2"), + (("3.4.0a2", "MAJOR", "alpha", 0, None), "4.0.0a0"), + (("4.0.0a0", "PATCH", "alpha", 0, None), "4.0.0a1"), + (("4.0.0a1", "MINOR", "alpha", 0, None), "4.0.0a2"), + (("4.0.0a2", "MAJOR", "alpha", 0, None), "4.0.0a3"), + # + (("1.0.0", "PATCH", "alpha", 0, None), "1.0.1a0"), + (("1.0.1a0", "PATCH", "alpha", 0, None), "1.0.1a1"), + (("1.0.1a1", "MINOR", "alpha", 0, None), "1.1.0a0"), + (("1.1.0a0", "PATCH", "alpha", 0, None), "1.1.0a1"), + (("1.1.0a1", "MINOR", "alpha", 0, None), "1.1.0a2"), + (("1.1.0a2", "MAJOR", "alpha", 0, None), "2.0.0a0"), + # + (("1.0.0", "MINOR", "alpha", 0, None), "1.1.0a0"), + (("1.1.0a0", "PATCH", "alpha", 0, None), "1.1.0a1"), + (("1.1.0a1", "MINOR", "alpha", 0, None), "1.1.0a2"), + (("1.1.0a2", "PATCH", "alpha", 0, None), "1.1.0a3"), + (("1.1.0a3", "MAJOR", "alpha", 0, None), "2.0.0a0"), + # + (("1.0.0", "MAJOR", "alpha", 0, None), "2.0.0a0"), + (("2.0.0a0", "MINOR", "alpha", 0, None), "2.0.0a1"), + (("2.0.0a1", "PATCH", "alpha", 0, None), "2.0.0a2"), + (("2.0.0a2", "MAJOR", "alpha", 0, None), "2.0.0a3"), + (("2.0.0a3", "MINOR", "alpha", 0, None), "2.0.0a4"), + (("2.0.0a4", "PATCH", "alpha", 0, None), "2.0.0a5"), + (("2.0.0a5", "MAJOR", "alpha", 0, None), "2.0.0a6"), + # + (("2.0.0b0", "MINOR", "alpha", 0, None), "2.0.0b1"), + (("2.0.0b0", "PATCH", "alpha", 0, None), "2.0.0b1"), + # + (("1.0.1a0", "PATCH", None, 0, None), "1.0.1"), + (("1.0.1a0", "MINOR", None, 0, None), "1.1.0"), + (("1.0.1a0", "MAJOR", None, 0, None), "2.0.0"), + # + (("1.1.0a0", "PATCH", None, 0, None), "1.1.0"), + (("1.1.0a0", "MINOR", None, 0, None), "1.1.0"), + (("1.1.0a0", "MAJOR", None, 0, None), "2.0.0"), + # + (("2.0.0a0", "MINOR", None, 0, None), "2.0.0"), + (("2.0.0a0", "MAJOR", None, 0, None), "2.0.0"), + (("2.0.0a0", "PATCH", None, 0, None), "2.0.0"), + # + (("3.0.0a1", None, None, 0, None), "3.0.0"), + (("3.0.0b1", None, None, 0, None), "3.0.0"), + (("3.0.0rc1", None, None, 0, None), "3.0.0"), + # + (("3.1.4", None, "alpha", 0, None), "3.1.4a0"), + (("3.1.4", None, "beta", 0, None), "3.1.4b0"), + (("3.1.4", None, "rc", 0, None), "3.1.4rc0"), + # + (("3.1.4", None, "alpha", 0, None), "3.1.4a0"), + (("3.1.4a0", "PATCH", "alpha", 0, None), "3.1.4a1"), # UNEXPECTED! + (("3.1.4a0", "MINOR", "alpha", 0, None), "3.2.0a0"), + (("3.1.4a0", "MAJOR", "alpha", 0, None), "4.0.0a0"), +] + +excact_cases = [ + (("1.0.0", "PATCH", None, 0, None), "1.0.1"), + (("1.0.0", "MINOR", None, 0, None), "1.1.0"), + # with exact_increment=False: "1.0.0b0" + (("1.0.0a1", "PATCH", "beta", 0, None), "1.0.1b0"), + # with exact_increment=False: "1.0.0b1" + (("1.0.0b0", "PATCH", "beta", 0, None), "1.0.1b0"), + # with exact_increment=False: "1.0.0rc0" + (("1.0.0b1", "PATCH", "rc", 0, None), "1.0.1rc0"), + # with exact_increment=False: "1.0.0-rc1" + (("1.0.0rc0", "PATCH", "rc", 0, None), "1.0.1rc0"), + # with exact_increment=False: "1.0.0rc1-dev1" + (("1.0.0rc0", "PATCH", "rc", 0, 1), "1.0.1rc0.dev1"), + # with exact_increment=False: "1.0.0b0" + (("1.0.0a1", "MINOR", "beta", 0, None), "1.1.0b0"), + # with exact_increment=False: "1.0.0b1" + (("1.0.0b0", "MINOR", "beta", 0, None), "1.1.0b0"), + # with exact_increment=False: "1.0.0b1" + (("1.0.0b0", "MINOR", "alpha", 0, None), "1.1.0a0"), + # with exact_increment=False: "1.0.0rc0" + (("1.0.0b1", "MINOR", "rc", 0, None), "1.1.0rc0"), + # with exact_increment=False: "1.0.0rc1" + (("1.0.0rc0", "MINOR", "rc", 0, None), "1.1.0rc0"), + # with exact_increment=False: "1.0.0rc1-dev1" + (("1.0.0rc0", "MINOR", "rc", 0, 1), "1.1.0rc0.dev1"), + # with exact_increment=False: "2.0.0" + (("2.0.0b0", "MAJOR", None, 0, None), "3.0.0"), + # with exact_increment=False: "2.0.0" + (("2.0.0b0", "MINOR", None, 0, None), "2.1.0"), + # with exact_increment=False: "2.0.0" + (("2.0.0b0", "PATCH", None, 0, None), "2.0.1"), + # same with exact_increment=False + (("2.0.0b0", "MAJOR", "alpha", 0, None), "3.0.0a0"), + # with exact_increment=False: "2.0.0b1" + (("2.0.0b0", "MINOR", "alpha", 0, None), "2.1.0a0"), + # with exact_increment=False: "2.0.0b1" + (("2.0.0b0", "PATCH", "alpha", 0, None), "2.0.1a0"), +] + + +@pytest.mark.parametrize( + "test_input,expected", + itertools.chain( + tdd_cases, + weird_cases, + simple_flow, + linear_prerelease_cases, + prerelease_cases, + ), +) +def test_bump_pep440_version(test_input, expected): + current_version = test_input[0] + increment = test_input[1] + prerelease = test_input[2] + prerelease_offset = test_input[3] + devrelease = test_input[4] + assert ( + str( + Pep440(current_version).bump( + increment=increment, + prerelease=prerelease, + prerelease_offset=prerelease_offset, + devrelease=devrelease, + ) + ) + == expected + ) + + +@pytest.mark.parametrize("test_input, expected", excact_cases) +def test_bump_pep440_version_force(test_input, expected): + current_version = test_input[0] + increment = test_input[1] + prerelease = test_input[2] + prerelease_offset = test_input[3] + devrelease = test_input[4] + assert ( + str( + Pep440(current_version).bump( + increment=increment, + prerelease=prerelease, + prerelease_offset=prerelease_offset, + devrelease=devrelease, + exact_increment=True, + ) + ) + == expected + ) + + +@pytest.mark.parametrize("test_input,expected", local_versions) +def test_bump_pep440_version_local(test_input, expected): + current_version = test_input[0] + increment = test_input[1] + prerelease = test_input[2] + prerelease_offset = test_input[3] + devrelease = test_input[4] + is_local_version = True + assert ( + str( + Pep440(current_version).bump( + increment=increment, + prerelease=prerelease, + prerelease_offset=prerelease_offset, + devrelease=devrelease, + is_local_version=is_local_version, + ) + ) + == expected + ) + + +def test_pep440_scheme_property(): + version = Pep440("0.0.1") + assert version.scheme is Pep440 + + +def test_pep440_implement_version_protocol(): + assert isinstance(Pep440("0.0.1"), VersionProtocol) + + +def test_pep440_sortable(): + test_input = [x[0][0] for x in simple_flow] + test_input.extend([x[1] for x in simple_flow]) + # randomize + random_input = [Pep440(x) for x in random.sample(test_input, len(test_input))] + assert len(random_input) == len(test_input) + sorted_result = [str(x) for x in sorted(random_input)] + assert sorted_result == [ + "0.1.0", + "0.1.0", + "0.1.1.dev1", + "0.1.1", + "0.1.1", + "0.2.0", + "0.2.0", + "0.2.0", + "0.3.0.dev1", + "0.3.0", + "0.3.0", + "0.3.0", + "0.3.0", + "0.3.1a0", + "0.3.1a0", + "0.3.1a0", + "0.3.1a0", + "0.3.1a1", + "0.3.1a1", + "0.3.1a1", + "0.3.1", + "0.3.1", + "0.3.1", + "0.3.2", + "0.4.2", + "1.0.0a0", + "1.0.0a0", + "1.0.0a1", + "1.0.0a1", + "1.0.0a1", + "1.0.0a1", + "1.0.0a2.dev0", + "1.0.0a2.dev0", + "1.0.0a2.dev1", + "1.0.0a2", + "1.0.0a3.dev0", + "1.0.0a3.dev0", + "1.0.0a3.dev1", + "1.0.0b0", + "1.0.0b0", + "1.0.0b0", + "1.0.0b1", + "1.0.0b1", + "1.0.0rc0", + "1.0.0rc0", + "1.0.0rc0", + "1.0.0rc0", + "1.0.0rc1.dev1", + "1.0.0rc1", + "1.0.0", + "1.0.0", + "1.0.1", + "1.0.1", + "1.0.2", + "1.0.2", + "1.1.0", + "1.1.0", + "1.2.0", + "1.2.0", + "1.2.1", + "1.2.1", + "2.0.0", + ] diff --git a/tests/test_version_scheme_semver.py b/tests/test_version_scheme_semver.py new file mode 100644 index 0000000..71d5e58 --- /dev/null +++ b/tests/test_version_scheme_semver.py @@ -0,0 +1,269 @@ +import itertools +import random + +import pytest + +from commitizen.version_schemes import SemVer, VersionProtocol + +simple_flow = [ + (("0.1.0", "PATCH", None, 0, None), "0.1.1"), + (("0.1.0", "PATCH", None, 0, 1), "0.1.1-dev1"), + (("0.1.1", "MINOR", None, 0, None), "0.2.0"), + (("0.2.0", "MINOR", None, 0, None), "0.3.0"), + (("0.2.0", "MINOR", None, 0, 1), "0.3.0-dev1"), + (("0.3.0", "PATCH", None, 0, None), "0.3.1"), + (("0.3.0", "PATCH", "alpha", 0, None), "0.3.1-a0"), + (("0.3.1a0", None, "alpha", 0, None), "0.3.1-a1"), + (("0.3.0", "PATCH", "alpha", 1, None), "0.3.1-a1"), + (("0.3.1a0", None, "alpha", 1, None), "0.3.1-a1"), + (("0.3.1a0", None, None, 0, None), "0.3.1"), + (("0.3.1", "PATCH", None, 0, None), "0.3.2"), + (("0.4.2", "MAJOR", "alpha", 0, None), "1.0.0-a0"), + (("1.0.0a0", None, "alpha", 0, None), "1.0.0-a1"), + (("1.0.0a1", None, "alpha", 0, None), "1.0.0-a2"), + (("1.0.0a1", None, "alpha", 0, 1), "1.0.0-a2-dev1"), + (("1.0.0a2.dev0", None, "alpha", 0, 1), "1.0.0-a3-dev1"), + (("1.0.0a2.dev0", None, "alpha", 0, 0), "1.0.0-a3-dev0"), + (("1.0.0a1", None, "beta", 0, None), "1.0.0-b0"), + (("1.0.0b0", None, "beta", 0, None), "1.0.0-b1"), + (("1.0.0b1", None, "rc", 0, None), "1.0.0-rc0"), + (("1.0.0rc0", None, "rc", 0, None), "1.0.0-rc1"), + (("1.0.0rc0", None, "rc", 0, 1), "1.0.0-rc1-dev1"), + (("1.0.0rc0", "PATCH", None, 0, None), "1.0.0"), + (("1.0.0a3.dev0", None, "beta", 0, None), "1.0.0-b0"), + (("1.0.0", "PATCH", None, 0, None), "1.0.1"), + (("1.0.1", "PATCH", None, 0, None), "1.0.2"), + (("1.0.2", "MINOR", None, 0, None), "1.1.0"), + (("1.1.0", "MINOR", None, 0, None), "1.2.0"), + (("1.2.0", "PATCH", None, 0, None), "1.2.1"), + (("1.2.1", "MAJOR", None, 0, None), "2.0.0"), +] + +local_versions = [ + (("4.5.0+0.1.0", "PATCH", None, 0, None), "4.5.0+0.1.1"), + (("4.5.0+0.1.1", "MINOR", None, 0, None), "4.5.0+0.2.0"), + (("4.5.0+0.2.0", "MAJOR", None, 0, None), "4.5.0+1.0.0"), +] + +# never bump backwards on pre-releases +linear_prerelease_cases = [ + (("0.1.1b1", None, "alpha", 0, None), "0.1.1-b2"), + (("0.1.1rc0", None, "alpha", 0, None), "0.1.1-rc1"), + (("0.1.1rc0", None, "beta", 0, None), "0.1.1-rc1"), +] + +weird_cases = [ + (("1.1", "PATCH", None, 0, None), "1.1.1"), + (("1", "MINOR", None, 0, None), "1.1.0"), + (("1", "MAJOR", None, 0, None), "2.0.0"), + (("1a0", None, "alpha", 0, None), "1.0.0-a1"), + (("1a0", None, "alpha", 1, None), "1.0.0-a1"), + (("1", None, "beta", 0, None), "1.0.0-b0"), + (("1", None, "beta", 1, None), "1.0.0-b1"), + (("1beta", None, "beta", 0, None), "1.0.0-b1"), + (("1.0.0alpha1", None, "alpha", 0, None), "1.0.0-a2"), + (("1", None, "rc", 0, None), "1.0.0-rc0"), + (("1.0.0rc1+e20d7b57f3eb", "PATCH", None, 0, None), "1.0.0"), +] + +# test driven development +tdd_cases = [ + (("0.1.1", "PATCH", None, 0, None), "0.1.2"), + (("0.1.1", "MINOR", None, 0, None), "0.2.0"), + (("2.1.1", "MAJOR", None, 0, None), "3.0.0"), + (("0.9.0", "PATCH", "alpha", 0, None), "0.9.1-a0"), + (("0.9.0", "MINOR", "alpha", 0, None), "0.10.0-a0"), + (("0.9.0", "MAJOR", "alpha", 0, None), "1.0.0-a0"), + (("0.9.0", "MAJOR", "alpha", 1, None), "1.0.0-a1"), + (("1.0.0a2", None, "beta", 0, None), "1.0.0-b0"), + (("1.0.0a2", None, "beta", 1, None), "1.0.0-b1"), + (("1.0.0beta1", None, "rc", 0, None), "1.0.0-rc0"), + (("1.0.0rc1", None, "rc", 0, None), "1.0.0-rc2"), + (("1.0.0-a0", None, "rc", 0, None), "1.0.0-rc0"), + (("1.0.0-alpha1", None, "alpha", 0, None), "1.0.0-a2"), +] + +excact_cases = [ + (("1.0.0", "PATCH", None, 0, None), "1.0.1"), + (("1.0.0", "MINOR", None, 0, None), "1.1.0"), + # with exact_increment=False: "1.0.0-b0" + (("1.0.0a1", "PATCH", "beta", 0, None), "1.0.1-b0"), + # with exact_increment=False: "1.0.0-b1" + (("1.0.0b0", "PATCH", "beta", 0, None), "1.0.1-b0"), + # with exact_increment=False: "1.0.0-rc0" + (("1.0.0b1", "PATCH", "rc", 0, None), "1.0.1-rc0"), + # with exact_increment=False: "1.0.0-rc1" + (("1.0.0rc0", "PATCH", "rc", 0, None), "1.0.1-rc0"), + # with exact_increment=False: "1.0.0-rc1-dev1" + (("1.0.0rc0", "PATCH", "rc", 0, 1), "1.0.1-rc0-dev1"), + # with exact_increment=False: "1.0.0-b0" + (("1.0.0a1", "MINOR", "beta", 0, None), "1.1.0-b0"), + # with exact_increment=False: "1.0.0-b1" + (("1.0.0b0", "MINOR", "beta", 0, None), "1.1.0-b0"), + # with exact_increment=False: "1.0.0-rc0" + (("1.0.0b1", "MINOR", "rc", 0, None), "1.1.0-rc0"), + # with exact_increment=False: "1.0.0-rc1" + (("1.0.0rc0", "MINOR", "rc", 0, None), "1.1.0-rc0"), + # with exact_increment=False: "1.0.0-rc1-dev1" + (("1.0.0rc0", "MINOR", "rc", 0, 1), "1.1.0-rc0-dev1"), + # with exact_increment=False: "2.0.0" + (("2.0.0b0", "MAJOR", None, 0, None), "3.0.0"), + # with exact_increment=False: "2.0.0" + (("2.0.0b0", "MINOR", None, 0, None), "2.1.0"), + # with exact_increment=False: "2.0.0" + (("2.0.0b0", "PATCH", None, 0, None), "2.0.1"), + # same with exact_increment=False + (("2.0.0b0", "MAJOR", "alpha", 0, None), "3.0.0-a0"), + # with exact_increment=False: "2.0.0b1" + (("2.0.0b0", "MINOR", "alpha", 0, None), "2.1.0-a0"), + # with exact_increment=False: "2.0.0b1" + (("2.0.0b0", "PATCH", "alpha", 0, None), "2.0.1-a0"), +] + + +@pytest.mark.parametrize( + "test_input, expected", + itertools.chain(tdd_cases, weird_cases, simple_flow, linear_prerelease_cases), +) +def test_bump_semver_version(test_input, expected): + current_version = test_input[0] + increment = test_input[1] + prerelease = test_input[2] + prerelease_offset = test_input[3] + devrelease = test_input[4] + assert ( + str( + SemVer(current_version).bump( + increment=increment, + prerelease=prerelease, + prerelease_offset=prerelease_offset, + devrelease=devrelease, + ) + ) + == expected + ) + + +@pytest.mark.parametrize("test_input, expected", excact_cases) +def test_bump_semver_version_force(test_input, expected): + current_version = test_input[0] + increment = test_input[1] + prerelease = test_input[2] + prerelease_offset = test_input[3] + devrelease = test_input[4] + assert ( + str( + SemVer(current_version).bump( + increment=increment, + prerelease=prerelease, + prerelease_offset=prerelease_offset, + devrelease=devrelease, + exact_increment=True, + ) + ) + == expected + ) + + +@pytest.mark.parametrize("test_input,expected", local_versions) +def test_bump_semver_version_local(test_input, expected): + current_version = test_input[0] + increment = test_input[1] + prerelease = test_input[2] + prerelease_offset = test_input[3] + devrelease = test_input[4] + is_local_version = True + assert ( + str( + SemVer(current_version).bump( + increment=increment, + prerelease=prerelease, + prerelease_offset=prerelease_offset, + devrelease=devrelease, + is_local_version=is_local_version, + ) + ) + == expected + ) + + +def test_semver_scheme_property(): + version = SemVer("0.0.1") + assert version.scheme is SemVer + + +def test_semver_implement_version_protocol(): + assert isinstance(SemVer("0.0.1"), VersionProtocol) + + +def test_semver_sortable(): + test_input = [x[0][0] for x in simple_flow] + test_input.extend([x[1] for x in simple_flow]) + # randomize + random_input = [SemVer(x) for x in random.sample(test_input, len(test_input))] + assert len(random_input) == len(test_input) + sorted_result = [str(x) for x in sorted(random_input)] + assert sorted_result == [ + "0.1.0", + "0.1.0", + "0.1.1-dev1", + "0.1.1", + "0.1.1", + "0.2.0", + "0.2.0", + "0.2.0", + "0.3.0-dev1", + "0.3.0", + "0.3.0", + "0.3.0", + "0.3.0", + "0.3.1-a0", + "0.3.1-a0", + "0.3.1-a0", + "0.3.1-a0", + "0.3.1-a1", + "0.3.1-a1", + "0.3.1-a1", + "0.3.1", + "0.3.1", + "0.3.1", + "0.3.2", + "0.4.2", + "1.0.0-a0", + "1.0.0-a0", + "1.0.0-a1", + "1.0.0-a1", + "1.0.0-a1", + "1.0.0-a1", + "1.0.0-a2-dev0", + "1.0.0-a2-dev0", + "1.0.0-a2-dev1", + "1.0.0-a2", + "1.0.0-a3-dev0", + "1.0.0-a3-dev0", + "1.0.0-a3-dev1", + "1.0.0-b0", + "1.0.0-b0", + "1.0.0-b0", + "1.0.0-b1", + "1.0.0-b1", + "1.0.0-rc0", + "1.0.0-rc0", + "1.0.0-rc0", + "1.0.0-rc0", + "1.0.0-rc1-dev1", + "1.0.0-rc1", + "1.0.0", + "1.0.0", + "1.0.1", + "1.0.1", + "1.0.2", + "1.0.2", + "1.1.0", + "1.1.0", + "1.2.0", + "1.2.0", + "1.2.1", + "1.2.1", + "2.0.0", + ] diff --git a/tests/test_version_scheme_semver2.py b/tests/test_version_scheme_semver2.py new file mode 100644 index 0000000..d18a058 --- /dev/null +++ b/tests/test_version_scheme_semver2.py @@ -0,0 +1,211 @@ +import itertools +import random + +import pytest + +from commitizen.version_schemes import SemVer2, VersionProtocol + +simple_flow = [ + (("0.1.0", "PATCH", None, 0, None), "0.1.1"), + (("0.1.0", "PATCH", None, 0, 1), "0.1.1-dev.1"), + (("0.1.1", "MINOR", None, 0, None), "0.2.0"), + (("0.2.0", "MINOR", None, 0, None), "0.3.0"), + (("0.2.0", "MINOR", None, 0, 1), "0.3.0-dev.1"), + (("0.3.0", "PATCH", None, 0, None), "0.3.1"), + (("0.3.0", "PATCH", "alpha", 0, None), "0.3.1-alpha.0"), + (("0.3.1-alpha.0", None, "alpha", 0, None), "0.3.1-alpha.1"), + (("0.3.0", "PATCH", "alpha", 1, None), "0.3.1-alpha.1"), + (("0.3.1-alpha.0", None, "alpha", 1, None), "0.3.1-alpha.1"), + (("0.3.1-alpha.0", None, None, 0, None), "0.3.1"), + (("0.3.1", "PATCH", None, 0, None), "0.3.2"), + (("0.4.2", "MAJOR", "alpha", 0, None), "1.0.0-alpha.0"), + (("1.0.0-alpha.0", None, "alpha", 0, None), "1.0.0-alpha.1"), + (("1.0.0-alpha.1", None, "alpha", 0, None), "1.0.0-alpha.2"), + (("1.0.0-alpha.1", None, "alpha", 0, 1), "1.0.0-alpha.2.dev.1"), + (("1.0.0-alpha.2.dev.0", None, "alpha", 0, 1), "1.0.0-alpha.3.dev.1"), + (("1.0.0-alpha.2.dev.0", None, "alpha", 0, 0), "1.0.0-alpha.3.dev.0"), + (("1.0.0-alpha.1", None, "beta", 0, None), "1.0.0-beta.0"), + (("1.0.0-beta.0", None, "beta", 0, None), "1.0.0-beta.1"), + (("1.0.0-beta.1", None, "rc", 0, None), "1.0.0-rc.0"), + (("1.0.0-rc.0", None, "rc", 0, None), "1.0.0-rc.1"), + (("1.0.0-rc.0", None, "rc", 0, 1), "1.0.0-rc.1.dev.1"), + (("1.0.0-rc.0", "PATCH", None, 0, None), "1.0.0"), + (("1.0.0-alpha.3.dev.0", None, "beta", 0, None), "1.0.0-beta.0"), + (("1.0.0", "PATCH", None, 0, None), "1.0.1"), + (("1.0.1", "PATCH", None, 0, None), "1.0.2"), + (("1.0.2", "MINOR", None, 0, None), "1.1.0"), + (("1.1.0", "MINOR", None, 0, None), "1.2.0"), + (("1.2.0", "PATCH", None, 0, None), "1.2.1"), + (("1.2.1", "MAJOR", None, 0, None), "2.0.0"), +] + +local_versions = [ + (("4.5.0+0.1.0", "PATCH", None, 0, None), "4.5.0+0.1.1"), + (("4.5.0+0.1.1", "MINOR", None, 0, None), "4.5.0+0.2.0"), + (("4.5.0+0.2.0", "MAJOR", None, 0, None), "4.5.0+1.0.0"), +] + +# never bump backwards on pre-releases +linear_prerelease_cases = [ + (("0.1.1-beta.1", None, "alpha", 0, None), "0.1.1-beta.2"), + (("0.1.1-rc.0", None, "alpha", 0, None), "0.1.1-rc.1"), + (("0.1.1-rc.0", None, "beta", 0, None), "0.1.1-rc.1"), +] + +weird_cases = [ + (("1.1", "PATCH", None, 0, None), "1.1.1"), + (("1", "MINOR", None, 0, None), "1.1.0"), + (("1", "MAJOR", None, 0, None), "2.0.0"), + (("1-alpha.0", None, "alpha", 0, None), "1.0.0-alpha.1"), + (("1-alpha.0", None, "alpha", 1, None), "1.0.0-alpha.1"), + (("1", None, "beta", 0, None), "1.0.0-beta.0"), + (("1", None, "beta", 1, None), "1.0.0-beta.1"), + (("1-beta", None, "beta", 0, None), "1.0.0-beta.1"), + (("1.0.0-alpha.1", None, "alpha", 0, None), "1.0.0-alpha.2"), + (("1", None, "rc", 0, None), "1.0.0-rc.0"), + (("1.0.0-rc.1+e20d7b57f3eb", "PATCH", None, 0, None), "1.0.0"), +] + +# test driven development +tdd_cases = [ + (("0.1.1", "PATCH", None, 0, None), "0.1.2"), + (("0.1.1", "MINOR", None, 0, None), "0.2.0"), + (("2.1.1", "MAJOR", None, 0, None), "3.0.0"), + (("0.9.0", "PATCH", "alpha", 0, None), "0.9.1-alpha.0"), + (("0.9.0", "MINOR", "alpha", 0, None), "0.10.0-alpha.0"), + (("0.9.0", "MAJOR", "alpha", 0, None), "1.0.0-alpha.0"), + (("0.9.0", "MAJOR", "alpha", 1, None), "1.0.0-alpha.1"), + (("1.0.0-alpha.2", None, "beta", 0, None), "1.0.0-beta.0"), + (("1.0.0-alpha.2", None, "beta", 1, None), "1.0.0-beta.1"), + (("1.0.0-beta.1", None, "rc", 0, None), "1.0.0-rc.0"), + (("1.0.0-rc.1", None, "rc", 0, None), "1.0.0-rc.2"), + (("1.0.0-alpha.0", None, "rc", 0, None), "1.0.0-rc.0"), + (("1.0.0-alpha.1", None, "alpha", 0, None), "1.0.0-alpha.2"), +] + + +@pytest.mark.parametrize( + "test_input, expected", + itertools.chain(tdd_cases, weird_cases, simple_flow, linear_prerelease_cases), +) +def test_bump_semver_version(test_input, expected): + current_version = test_input[0] + increment = test_input[1] + prerelease = test_input[2] + prerelease_offset = test_input[3] + devrelease = test_input[4] + assert ( + str( + SemVer2(current_version).bump( + increment=increment, + prerelease=prerelease, + prerelease_offset=prerelease_offset, + devrelease=devrelease, + ) + ) + == expected + ) + + +@pytest.mark.parametrize("test_input,expected", local_versions) +def test_bump_semver_version_local(test_input, expected): + current_version = test_input[0] + increment = test_input[1] + prerelease = test_input[2] + prerelease_offset = test_input[3] + devrelease = test_input[4] + is_local_version = True + assert ( + str( + SemVer2(current_version).bump( + increment=increment, + prerelease=prerelease, + prerelease_offset=prerelease_offset, + devrelease=devrelease, + is_local_version=is_local_version, + ) + ) + == expected + ) + + +def test_semver_scheme_property(): + version = SemVer2("0.0.1") + assert version.scheme is SemVer2 + + +def test_semver_implement_version_protocol(): + assert isinstance(SemVer2("0.0.1"), VersionProtocol) + + +def test_semver_sortable(): + test_input = [x[0][0] for x in simple_flow] + test_input.extend([x[1] for x in simple_flow]) + # randomize + random_input = [SemVer2(x) for x in random.sample(test_input, len(test_input))] + assert len(random_input) == len(test_input) + sorted_result = [str(x) for x in sorted(random_input)] + assert sorted_result == [ + "0.1.0", + "0.1.0", + "0.1.1-dev.1", + "0.1.1", + "0.1.1", + "0.2.0", + "0.2.0", + "0.2.0", + "0.3.0-dev.1", + "0.3.0", + "0.3.0", + "0.3.0", + "0.3.0", + "0.3.1-alpha.0", + "0.3.1-alpha.0", + "0.3.1-alpha.0", + "0.3.1-alpha.0", + "0.3.1-alpha.1", + "0.3.1-alpha.1", + "0.3.1-alpha.1", + "0.3.1", + "0.3.1", + "0.3.1", + "0.3.2", + "0.4.2", + "1.0.0-alpha.0", + "1.0.0-alpha.0", + "1.0.0-alpha.1", + "1.0.0-alpha.1", + "1.0.0-alpha.1", + "1.0.0-alpha.1", + "1.0.0-alpha.2.dev.0", + "1.0.0-alpha.2.dev.0", + "1.0.0-alpha.2.dev.1", + "1.0.0-alpha.2", + "1.0.0-alpha.3.dev.0", + "1.0.0-alpha.3.dev.0", + "1.0.0-alpha.3.dev.1", + "1.0.0-beta.0", + "1.0.0-beta.0", + "1.0.0-beta.0", + "1.0.0-beta.1", + "1.0.0-beta.1", + "1.0.0-rc.0", + "1.0.0-rc.0", + "1.0.0-rc.0", + "1.0.0-rc.0", + "1.0.0-rc.1.dev.1", + "1.0.0-rc.1", + "1.0.0", + "1.0.0", + "1.0.1", + "1.0.1", + "1.0.2", + "1.0.2", + "1.1.0", + "1.1.0", + "1.2.0", + "1.2.0", + "1.2.1", + "1.2.1", + "2.0.0", + ] diff --git a/tests/test_version_schemes.py b/tests/test_version_schemes.py new file mode 100644 index 0000000..8e2dae9 --- /dev/null +++ b/tests/test_version_schemes.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import sys + +if sys.version_info >= (3, 10): + from importlib import metadata +else: + import importlib_metadata as metadata + +import pytest +from pytest_mock import MockerFixture + +from commitizen.config.base_config import BaseConfig +from commitizen.exceptions import VersionSchemeUnknown +from commitizen.version_schemes import Pep440, SemVer, get_version_scheme + + +def test_default_version_scheme_is_pep440(config: BaseConfig): + scheme = get_version_scheme(config.settings) + assert scheme is Pep440 + + +def test_version_scheme_from_config(config: BaseConfig): + config.settings["version_scheme"] = "semver" + scheme = get_version_scheme(config.settings) + assert scheme is SemVer + + +def test_version_scheme_from_name(config: BaseConfig): + config.settings["version_scheme"] = "pep440" + scheme = get_version_scheme(config.settings, "semver") + assert scheme is SemVer + + +def test_raise_for_unknown_version_scheme(config: BaseConfig): + with pytest.raises(VersionSchemeUnknown): + get_version_scheme(config.settings, "unknown") + + +def test_version_scheme_from_deprecated_config(config: BaseConfig): + config.settings["version_type"] = "semver" + with pytest.warns(DeprecationWarning): + scheme = get_version_scheme(config.settings) + assert scheme is SemVer + + +def test_version_scheme_from_config_priority(config: BaseConfig): + config.settings["version_scheme"] = "pep440" + config.settings["version_type"] = "semver" + with pytest.warns(DeprecationWarning): + scheme = get_version_scheme(config.settings) + assert scheme is Pep440 + + +def test_warn_if_version_protocol_not_implemented( + config: BaseConfig, mocker: MockerFixture +): + class NotVersionProtocol: + pass + + ep = mocker.Mock() + ep.load.return_value = NotVersionProtocol + mocker.patch.object(metadata, "entry_points", return_value=(ep,)) + + with pytest.warns(match="VersionProtocol"): + get_version_scheme(config.settings, "any") diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..5e26b2d --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import sys +import time +import uuid +from pathlib import Path + +import pytest +from deprecated import deprecated + +from commitizen import cmd, exceptions, git + +skip_below_py_3_10 = pytest.mark.skipif( + sys.version_info < (3, 10), + reason="The output message of argparse is different between Python 3.10 and lower than Python 3.10", +) + +skip_below_py_3_13 = pytest.mark.skipif( + sys.version_info < (3, 13), + reason="The output message of argparse is different between Python 3.13 and lower than Python 3.13", +) + + +class FakeCommand: + def __init__(self, out=None, err=None, return_code=0): + self.out = out + self.err = err + self.return_code = return_code + + +def create_file_and_commit( + message: str, filename: str | None = None, committer_date: str | None = None +): + if not filename: + filename = str(uuid.uuid4()) + + Path(filename).touch() + c = cmd.run("git add .") + if c.return_code != 0: + raise exceptions.CommitError(c.err) + c = git.commit(message, committer_date=committer_date) + if c.return_code != 0: + raise exceptions.CommitError(c.err) + + +def create_branch(name: str): + c = cmd.run(f"git branch {name}") + if c.return_code != 0: + raise exceptions.GitCommandError(c.err) + + +def switch_branch(branch: str): + c = cmd.run(f"git switch {branch}") + if c.return_code != 0: + raise exceptions.GitCommandError(c.err) + + +def merge_branch(branch: str): + c = cmd.run(f"git merge {branch}") + if c.return_code != 0: + raise exceptions.GitCommandError(c.err) + + +def get_current_branch() -> str: + c = cmd.run("git rev-parse --abbrev-ref HEAD") + if c.return_code != 0: + raise exceptions.GitCommandError(c.err) + return c.out + + +def create_tag(tag: str, message: str | None = None) -> None: + c = git.tag(tag, annotated=(message is not None), msg=message) + if c.return_code != 0: + raise exceptions.CommitError(c.err) + + +@deprecated( + reason="\n\ +Prefer using `create_file_and_commit(filename, committer_date={your_date})` to influence the order of tags.\n\ +This is because lightweight tags (like the ones created here) use the commit's creatordate which we can specify \ +with the GIT_COMMITTER_DATE flag, instead of waiting entire seconds during tests." +) +def wait_for_tag(): + """Deprecated -- use `create_file_and_commit(filename, committer_date={your_date})` to order tags instead + + Wait for tag. + + The resolution of timestamps is 1 second, so we need to wait + to create another tag unfortunately. + + This means: + If we create 2 tags under the same second, they might be returned in the wrong order + + See https://stackoverflow.com/questions/28237043/what-is-the-resolution-of-gits-commit-date-or-author-date-timestamps + """ + time.sleep(1.1)