diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 10d782d30b..5eac443f29 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,7 +1,7 @@ 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] +labels: ['type: bug'] body: - type: textarea id: description @@ -48,14 +48,22 @@ body: 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())"` + Please use the following command to retrieve environment information ```bash cz version --report ``` + + If `cz version --report` is not available, please run the following commands to retrieve your environment information: + + ```bash + echo "Commitizen Version: $(cz version)" + echo "Python Version: $(python3 -c 'import sys; print(sys.version)')" + echo "Operating System: $(python3 -c 'import platform; print(platform.system())')" + ``` + placeholder: | + Commitizen Version: 4.0.0 + Python Version: 3.13.3 (main, Apr 8 2025, 13:54:08) [Clang 16.0.0 (clang-1600.0.26.6)] + Operating System: Darwin validations: required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 884fe1663a..e624b37579 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,4 +3,5 @@ blank_issues_enabled: false contact_links: - name: Security Contact + url: https://github.com/woile about: Please report security vulnerabilities to santiwilly@gmail.com diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml index 51d378b747..8fe40136c3 100644 --- a/.github/ISSUE_TEMPLATE/documentation.yml +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -1,7 +1,7 @@ name: 📖 Documentation description: Suggest an improvement for the documentation of this project title: Content to be added or fixed -labels: [documentation] +labels: ['type: documentation'] body: - type: checkboxes id: type diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 7d67eb18af..da3bd5af8a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,7 +1,7 @@ name: 🚀 Feature request description: Suggest an idea for this project title: "" -labels: [feature] +labels: ['type: feature'] body: - type: textarea id: description @@ -26,6 +26,6 @@ body: - type: textarea id: related-issue attributes: - label: Additional context + label: Related issues description: | If applicable, add link to existing issue also help us know better. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 14bee6b434..87189d131c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,21 +9,58 @@ Please fill in the following content to let us know better about this change. ## Checklist +- [ ] I have read the [contributing guidelines](https://commitizen-tools.github.io/commitizen/contributing/contributing) + +### Was generative AI tooling used to co-author this PR? + + + +- [ ] Yes (please specify the tool below) + + + +### Code Changes + - [ ] Add test cases to all the changes you introduce -- [ ] Run `./scripts/format` and `./scripts/test` locally to ensure this change passes linter check and test -- [ ] Test the changes on the local machine manually +- [ ] Run `uv run poe all` locally to ensure this change passes linter check and tests +- [ ] Manually test the changes: + - [ ] Verify the feature/bug fix works as expected in real-world scenarios + - [ ] Test edge cases and error conditions + - [ ] Ensure backward compatibility is maintained + - [ ] Document any manual testing steps performed - [ ] Update the documentation for the changes -## Expected behavior +### Documentation Changes + + + +- [ ] Run `uv run poe doc` locally to ensure the documentation pages renders correctly +- [ ] Check and fix any broken links (internal or external) + + + +## Expected Behavior ## Steps to Test This Pull Request - +3. ... +--> -## Additional context +## Additional Context diff --git a/.github/workflows/bumpversion.yml b/.github/workflows/bumpversion.yml index ed0c8cffce..0ac511afd7 100644 --- a/.github/workflows/bumpversion.yml +++ b/.github/workflows/bumpversion.yml @@ -7,12 +7,12 @@ on: jobs: bump-version: - if: "!startsWith(github.event.head_commit.message, 'bump:')" + if: ${{ github.repository == 'commitizen-tools/commitizen' && !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 + uses: actions/checkout@v6 with: fetch-depth: 0 token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" diff --git a/.github/workflows/docspublish.yml b/.github/workflows/docspublish.yml index f318b86858..a84079fd64 100644 --- a/.github/workflows/docspublish.yml +++ b/.github/workflows/docspublish.yml @@ -4,32 +4,40 @@ on: push: branches: - master + workflow_dispatch: jobs: update-cli-screenshots: + if: ${{ github.repository == 'commitizen-tools/commitizen' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v5 + uses: astral-sh/setup-uv@v7 + - name: Set up Go + uses: actions/setup-go@v6 with: - python-version: "3.x" + go-version: '1.25.8' + - name: Set up VHS + run: | + sudo apt update + sudo apt install -y ffmpeg ttyd + go install github.com/charmbracelet/vhs@latest - name: Install dependencies run: | - python -m pip install -U pip poetry - poetry --version - poetry install + uv --version + uv sync --frozen --group base --group script - name: Update CLI screenshots run: | - poetry run python scripts/gen_cli_help_screenshots.py + uv run --no-sync poe 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 + git add docs/images/cli_help docs/images/cli_interactive if [[ -n "$(git status --porcelain)" ]]; then git commit -m "docs(cli/screenshots): update CLI screenshots" -m "[skip ci]" @@ -39,10 +47,11 @@ jobs: fi publish-documentation: + if: ${{ github.repository == 'commitizen-tools/commitizen' }} runs-on: ubuntu-latest needs: update-cli-screenshots steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" fetch-depth: 0 @@ -50,27 +59,24 @@ jobs: run: | git pull origin master - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" + uses: astral-sh/setup-uv@v7 - name: Install dependencies run: | - python -m pip install -U mkdocs mkdocs-material - - name: Build docs - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - python -m mkdocs build + uv --version + uv sync --frozen --only-group base --only-group documentation - 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 + organization: true + - name: Build docs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + uv run --no-sync poe doc:build + - name: Deploy 🚀 + uses: JamesIves/github-pages-deploy-action@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" + folder: ./site # The folder the action should deploy. + branch: gh-pages diff --git a/.github/workflows/homebrewpublish.yml b/.github/workflows/homebrewpublish.yml index 84c2ca6ca0..3a4d2cd3d2 100644 --- a/.github/workflows/homebrewpublish.yml +++ b/.github/workflows/homebrewpublish.yml @@ -9,24 +9,21 @@ on: jobs: deploy: runs-on: macos-latest - if: ${{ github.event.workflow_run.conclusion == 'success' }} + if: ${{ github.repository == 'commitizen-tools/commitizen' && github.event.workflow_run.conclusion == 'success' }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" + uses: astral-sh/setup-uv@v7 - name: Install dependencies - run: | - python -m pip install -U commitizen + run: uv 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@v3 + uses: dawidd6/action-homebrew-bump-formula@v7 with: - token: ${{secrets.PERSONAL_ACCESS_TOKEN}} + 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 index 45ca450f89..359c50d65a 100644 --- a/.github/workflows/label_issues.yml +++ b/.github/workflows/label_issues.yml @@ -12,12 +12,35 @@ jobs: issues: write runs-on: ubuntu-latest steps: - - uses: actions/github-script@v7 + - uses: actions/github-script@v8 with: script: | - github.rest.issues.addLabels({ + const issue = await github.rest.issues.get({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, - labels: ['issue-status: needs-triage'] - }) + }); + + const body = issue.data.body || ''; + + const osLabels = new Set(); // Use a Set to avoid duplicates + + // Parse the "Environment" section, output of `cz version --report` + if (body.includes('Operating System: Darwin')) { + osLabels.add('os: macOS'); + } + + if (body.includes('Operating System: Linux')) { + osLabels.add('os: Linux'); + } + + if (body.includes('Operating System: Windows')) { + osLabels.add('os: Windows'); + } + + await github.rest.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['issue-status: needs-triage', ...osLabels], + }); diff --git a/.github/workflows/label_pr.yml b/.github/workflows/label_pr.yml index b409c8b757..8e2d674f2b 100644 --- a/.github/workflows/label_pr.yml +++ b/.github/workflows/label_pr.yml @@ -9,11 +9,39 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: sparse-checkout: | .github/labeler.yml sparse-checkout-cone-mode: false - - uses: actions/labeler@v5 + - uses: actions/labeler@v6 with: configuration-path: .github/labeler.yml + - name: Label based on PR title + uses: actions/github-script@v8 + with: + script: | + const title = context.payload.pull_request.title.toLowerCase(); + const labels = []; + + if (title.includes("fix") || title.includes("bug")) { + labels.push("type: bug"); + } + if (title.includes("feat")) { + labels.push("type: feature"); + } + if (title.includes("doc")) { + labels.push("type: documentation"); + } + if (title.includes("refactor")) { + labels.push("type: refactor"); + } + + if (labels.length > 0) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels + }); + } diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml new file mode 100644 index 0000000000..8bd275ffab --- /dev/null +++ b/.github/workflows/links.yml @@ -0,0 +1,41 @@ +name: Links + +on: + repository_dispatch: + workflow_dispatch: + pull_request: + schedule: + - cron: "00 18 * * *" + +jobs: + check-links: + runs-on: ubuntu-latest + permissions: + issues: write # required for Broken Links Report + steps: + - uses: actions/checkout@v6 + + - name: Link Checker + id: lychee + uses: lycheeverse/lychee-action@v2 + with: + args: --config lychee.toml . + fail: false + + - name: Broken Links Report + if: steps.lychee.outputs.exit_code != 0 && github.event_name == 'schedule' + uses: actions/github-script@v8 + with: + script: | + const fs = require('fs'); + + // Read the markdown file + // Ensure the path is correct relative to the workspace root + const reportBody = fs.readFileSync('./lychee/out.md', 'utf8'); + + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: 'Broken Links Report', + body: reportBody + }); diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 3b5d5305a3..0667e255cd 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -1,38 +1,70 @@ name: Python package -on: [workflow_dispatch, pull_request] +on: + push: + branches: + - master + pull_request: + workflow_dispatch: jobs: + detect_changes: + runs-on: ubuntu-latest + outputs: + relevant: ${{ steps.filter.outputs.relevant }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - id: filter + uses: dorny/paths-filter@v4 + with: + filters: | + relevant: + - "commitizen/**" + - "tests/**" + - ".github/workflows/**" python-check: + needs: detect_changes strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - platform: [ubuntu-20.04, macos-latest, windows-latest] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + platform: [ubuntu-22.04, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v4 + - name: No relevant changes, fast-path success + if: ${{ needs.detect_changes.outputs.relevant != 'true' }} + run: | + echo "No relevant file changes; skipping tests and linters." + - uses: actions/checkout@v6 + if: ${{ needs.detect_changes.outputs.relevant == 'true' }} with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: astral-sh/setup-uv@v7 + if: ${{ needs.detect_changes.outputs.relevant == 'true' }} with: python-version: ${{ matrix.python-version }} - name: Install dependencies + if: ${{ needs.detect_changes.outputs.relevant == 'true' }} run: | - python -m pip install -U pip poetry - poetry --version - poetry install + uv --version + uv sync --frozen --group base --group test --group linters - name: Run tests and linters + if: ${{ needs.detect_changes.outputs.relevant == 'true' }} run: | git config --global user.email "action@github.com" git config --global user.name "GitHub Action" - SKIP=no-commit-to-branch,commitizen-branch poetry run pre-commit run --all-files --hook-stage pre-push + uv run --no-sync poe ci shell: bash - name: Upload coverage to Codecov - if: runner.os == 'Linux' - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v6 + if: ${{ needs.detect_changes.outputs.relevant == 'true' }} + with: + token: ${{ secrets.CODECOV_TOKEN }} + - name: Upload test results to Codecov + uses: codecov/codecov-action@v6 + if: ${{ needs.detect_changes.outputs.relevant == 'true' && !cancelled() }} with: token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage.xml - flags: unittests - name: codecov-umbrella + report_type: test_results diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml index e3b3aa6f30..a829ef9470 100644 --- a/.github/workflows/pythonpublish.yml +++ b/.github/workflows/pythonpublish.yml @@ -7,24 +7,19 @@ on: jobs: deploy: + if: ${{ github.repository == 'commitizen-tools/commitizen' }} runs-on: ubuntu-latest + permissions: + id-token: write + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 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 mkdocs mkdocs-material - poetry --version - poetry install - - name: Publish - env: - PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }} - PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - ./scripts/publish + uses: astral-sh/setup-uv@v7 + - name: Build + run: uv build + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index be07cf2d93..fb467da46c 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ coverage.xml *.cover .hypothesis/ .pytest_cache +junit.xml # Translations *.mo diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d0149b5fae..4ec740e2fd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,8 +4,7 @@ default_install_hook_types: - pre-push default_stages: - - commit - - push + - pre-commit repos: - repo: meta @@ -14,30 +13,38 @@ repos: - id: check-useless-excludes - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v5.0.0 hooks: - id: check-vcs-permalinks - id: end-of-file-fixer - exclude: "tests/((commands|data)/|test_).+" + 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-toml # TOML linter (syntax checker) - id: check-yaml args: [ '--unsafe' ] # for mkdocs.yml - id: detect-private-key + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.9.18 + hooks: + - id: uv-lock + - id: uv-sync + args: [ --frozen, --all-groups ] + stages: [ pre-commit, post-checkout, post-merge, post-rewrite ] + - repo: https://github.com/asottile/blacken-docs - rev: 1.13.0 + rev: 1.19.1 hooks: - id: blacken-docs additional_dependencies: [ black~=23.11 ] - repo: https://github.com/codespell-project/codespell - rev: v2.2.4 + rev: v2.4.1 hooks: - id: codespell name: Run codespell to check for common misspellings in files @@ -49,27 +56,30 @@ repos: - tomli - repo: https://github.com/commitizen-tools/commitizen - rev: v3.27.0 # automatically updated by Commitizen + rev: v4.13.9 # automatically updated by Commitizen hooks: - id: commitizen - id: commitizen-branch stages: - post-commit - - push + + - repo: https://github.com/ComPWA/taplo-pre-commit + rev: v0.9.3 + hooks: + - id: taplo-format - repo: local hooks: - id: format - name: format + name: Format Python code language: system pass_filenames: false - entry: ./scripts/format + entry: uv run --no-sync poe format types: [ python ] - id: linter and test - name: linter and test + name: Linters language: system pass_filenames: false - stages: [ push ] - entry: ./scripts/test + entry: uv run --no-sync poe lint types: [ python ] diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 2a3a088484..d57006f153 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -19,9 +19,10 @@ 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] + args: [--rev-range, "$PRE_COMMIT_FROM_REF..$PRE_COMMIT_TO_REF"] always_run: true pass_filenames: false language: python language_version: python3 - minimum_pre_commit_version: "1.4.3" + minimum_pre_commit_version: "3.2.0" + stages: [pre-push] diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 0000000000..36a3453ac9 --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,4 @@ +include = ["pyproject.toml", ".taplo.toml"] + +[formatting] +indent_string = " " diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..c2d0e98ae7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,54 @@ +# AGENTS instructions + +## Purpose + +This file provides **project-specific guidance for AI agents** (and other automated tools) working on the `commitizen` repository. +Follow these instructions in addition to any higher-level system or tool rules. + +## Project Overview + +- **Project**: `commitizen` - a tool to help enforce and automate conventional commits, version bumps, and changelog generation. +- **Primary language**: Python (library + CLI). +- **Key entrypoints**: + - `commitizen/cli.py` - main CLI implementation. + - `commitizen/commands/` - subcommands such as `bump`, `commit`, `changelog`, `check`, etc. + - `commitizen/config/` - configuration discovery and loading. + - `commitizen/providers/` - version providers (e.g., `pep621`, `poetry`, `npm`, `uv`). + +## General Expectations + +- **Preserve public behavior and CLI UX.** +- **Avoid breaking changes** (APIs, CLI flags, exit codes) unless explicitly requested. +- **Keep changes small and focused.** +- **Update or add tests/docs** when you change user-facing behavior. + +## How to Explore and Validate Changes + +- **Code entrypoints**: + - CLI behavior: `commitizen/cli.py` and `commitizen/commands/`. + - Config resolution: `commitizen/config/factory.py` and config modules. + - Bump/changelog/versioning: `commitizen/bump.py`, `commitizen/changelog.py`, `commitizen/version_schemes.py`, `commitizen/providers/`. +- **Docs to consult** (before larger changes): + - `docs/README.md` + - `docs/contributing.md` + - `docs/commands/` and `docs/config/` +- **Tests**: + - Prefer targeted runs, e.g. `uv run pytest tests/test_cli.py` or a specific `tests/commands/` file. + +## Coding Guidelines (for AI tools) + +- **Style**: Follow patterns in nearby code; keep functions focused. +- **Types**: Preserve or improve existing type hints. +- **Errors**: Prefer `commitizen/exceptions.py` error types; keep messages clear for CLI users. +- **Output**: Use `commitizen/out.py`; do not add noisy logging. + +## Common Task Pointers + +- **CLI commands**: edit `commitizen/commands/.py`, wire via `commitizen/cli.py`, and adjust `tests/commands/` + `docs/commands/`. +- **Version bumps / changelog**: use `commitizen/bump.py`, `commitizen/changelog.py`, `commitizen/version_schemes.py`, and `commitizen/providers/` (+ matching tests). +- **Config resolution**: use `commitizen/config/factory.py` and config modules; update `tests/test_conf.py` and related tests. + +## When Unsure + +- Prefer **reading tests and documentation first** to understand the expected behavior. +- When behavior is ambiguous, **assume backward compatibility** with current tests and docs is required. diff --git a/CHANGELOG.md b/CHANGELOG.md index c3a3ecc5e1..31bbd5a29f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,500 @@ +## v4.13.9 (2026-02-25) + +### Fix + +- avoid raising an exception when a change_type is not defined (#1879) + +## v4.13.8 (2026-02-18) + +### Fix + +- **config**: fix contains_commitizen_section failing for completely empty files + +## v4.13.7 (2026-02-09) + +### Fix + +- **provider**: use encoding settings in config (#1857) + +## v4.13.6 (2026-02-07) + +### Fix + +- **bump**: preserve existing changelog header when `changelog_merge_prerelease` is used with `cz bump --changelog` (#1850) + +## v4.13.5 (2026-02-05) + +### Fix + +- **changelog**: add incremental parameter to changelog generation (#1808) + +## v4.13.4 (2026-02-04) + +### Fix + +- **pre-commit-hooks**: correct rev-range syntax in commitizen-branch (#1841) + +## v4.13.3 (2026-02-04) + +### Refactor + +- **version_schemes**: shorten generate_prerelease (#1838) + +## v4.13.2 (2026-02-03) + +### Refactor + +- simplify code with pathlib Path object (#1840) +- **tags**: extract version resolution method (#1839) + +## v4.13.1 (2026-02-03) + +### Refactor + +- **config**: replace is_empty_config with contains_commitizen_section, improve multi config resolution algorithm (#1842) + +## v4.13.0 (2026-02-01) + +### Feat + +- **bump**: add --version-files-only and deprecate --files-only (#1802) +- **version**: add --tag tag to version command (#1819) +- **cli**: add description when choosing a commit rule (#1825) +- **tags**: enable version schemes with less than 3 components (#1705) + +### Fix + +- **config**: include pyproject.toml in multi config file warning (#1803) +- add pytest ruff rule PT and fix missing deprecation warning (#1826) +- **message_length_limit**: align the behavior of message_length_limit (#1813) +- **cli**: capitalize the first characters of help texts and fix minor grammar errors + +### Refactor + +- replace hard-coded string "cz_conventional_commits" with DEFAULT_SETTINGS (#1830) +- **bump**: fix unbounded variable type issue + +## v4.12.1 (2026-01-22) + +### Fix + +- **pre-commit-hooks**: remove magic constants on pre-push hook (#1815) + +## v4.12.0 (2026-01-19) + +### Feat + +- **prek**: supporting prek as an alternative to pre-commit and switching to prek (#1799) + +## v4.11.6 (2026-01-15) + +### Fix + +- **git**: commit bodies with carriage returns are correctly split by … (#1780) + +## v4.11.5 (2026-01-15) + +### Fix + +- **config**: ensure the actually used config file is correct, better test coverage (#1784) + +## v4.11.4 (2026-01-15) + +### Fix + +- **cli**: move sys.excepthook override to correct line, rename 'type' parameter, fix no argv test (#1791) + +## v4.11.3 (2026-01-13) + +### Fix + +- **bump**: fix the issue that changelog_merge_prerelease not working on cz bump + +## v4.11.2 (2026-01-12) + +### Fix + +- **config**: add warning for multiple configuration files and update documentation (#1773) + +## v4.11.1 (2026-01-03) + +### Fix + +- **providers**: normalize package names in uv provider for uv.lock matching + +## v4.11.0 (2025-12-29) + +### Feat + +- Drop support for Python 3.9 as EOL reached and add Python 3.14 support +- add custom validation + +## v4.10.1 (2025-12-11) + +### Fix + +- **version**: fix the behavior of cz version --major +- **cli**: debug and no_raise can be used together in sys.excepthook +- **git**: replace lstrip with strip for compatibility issue +- **bump**: remove NotAllowed related to --get-next option, other related refactoring + +### Refactor + +- **version**: rename class member to align with other classes +- **cargo_provider**: cleanup and get rid of potential type errors +- **bump**: extract option validation and new version resolution to new functions +- **changelog**: raise NotAllow when file_name not passed instead of using assert +- **bump**: rename parameter and variables + +### Perf + +- **ruff**: enable ruff rules TC001~TC006 +- add TYPE_CHECKING to CzQuestion imports + +## v4.10.0 (2025-11-10) + +### Feat + +- add config option for line length warning +- **conventional_commits**: allow exclamation in title on BC +- **version**: add the ability to just print major or minor version +- allow `amend!` prefix as created by `git --fixup=reword:` + +### Fix + +- **commands/version**: add missing return +- **test**: set terminal width for cli tests +- **Init**: raise InitFailedError on keyboard interrupt on pre-commit hook question, simplify logic, remove unreachable code path + +### Refactor + +- **bump**: cleanup related to update_version_file +- **RestructuredTest**: rename variable, fix typo and remove unnecessary string copy +- **TomlConfig**: minor cleanups for DX +- **Commit**: refactor _prompt_commit_questions and fix some type hint +- **hooks**: refactor to improve readability +- **Init**: make project_info a module and remove self.project_info +- **BaseConfig**: update docstring, extract factory method and remove unnecessary variable assignment +- remove self.encoding for better maintainability +- **utils**: make get_backup_file_path to return a path for semantic correctness +- remove unnecessary class member tag_format +- **Bump**: remove use of getattr +- **ConventionalCommitsCz**: rewrite message method to make the pattern more clear +- **cmd**: unnest try except +- **BaseCommitizen**: remove NotImplementedError and make them abstract method +- **BaseCommitizen**: construct Style object directly to get rid of potential type error + +## v4.9.1 (2025-09-10) + +### Fix + +- **dependency**: move deprecated to project.dependencies + +## v4.9.0 (2025-09-09) + +### Feat + +- **check**: add check against default branch + +### Fix + +- **changelog**: mark get_smart_tag_range as deprecated +- **init**: use pre-push as pre-commit stage +- **init**: use pre-push as pre-commit stage +- **init**: make welcome message easier to read +- **Init**: fix a typo in _ask_version_provider options and remove unnecessary filter, use named tuple for options +- **ExitCode**: add from_str in ExitCode and replace parse_no_raise with it +- raise NoVersionSpecifiedError if version is None, and adjust call sites of get_version +- **Changelog**: fix _export_template variable type +- **Bump**: rewrite --get-next NotAllowed error message for consistency + +### Refactor + +- **changelog**: add get_next_tag_name_after_version and test, mark unused for get_smart_tag_range +- **changelog**: simplify logic for get_oldest_and_newest_rev +- **changelog**: shorten generate_tree_from_commits +- **Init**: remove the variable values_to_add and the update_config function for readability +- **Init**: remove unnecessary methods from ProjectInfo and refactor _ask_tag +- **Init**: fix unbounded variable in _ask_tag_format +- **init**: remote extra words +- **process_commit_message**: better type and early return +- **Init**: extract _get_config_data for readability +- **changelog**: shorten condition expression and early return +- **Changelog**: remove unnecessary intermediate variables for better readability +- **bump**: use a loop to shorten a series of similar NotAllowed exceptions +- **Init**: use ternary operator +- **TagRules**: extract tag_formats property and simplify list comprehension +- **git**: remove redundant if branch +- **ScmProvider**: replace sorted with max +- **ExpectedExit**: make the constructor more compact +- **ParseArgs**: simplify __call__ function body + +## v4.8.4 (2025-09-05) + +### Fix + +- members in workspace, use exclude +- cargo workspaces + +### Refactor + +- reduce code indentation + +## v4.8.3 (2025-06-09) + +### Fix + +- **cli**: update description for deprecate warning +- **commit**: emit deprecated warning of cz commit -s +- **Check**: make parameters backward compatible +- **BaseConfig**: mypy error +- **deprecated**: mark deprecate in v5 +- **defaults**: add non-capitalized default constants back and deprecated warning + +### Refactor + +- **jira**: refactor message +- **conventional_commits**: use TypedDict for answers +- **conventional_commits**: make schema_pattern more readable +- do not guess if changelog format is provided +- **check**: compile once and rename variable +- **questions**: type questions with TypedDict +- **bump**: simplify nested if +- **git**: retype get_commits parameter to make it more friendly to call sites +- **git**: simplify tag logic +- **bump**: eliminate similar patterns in code +- **bump**: use any to replace 'or' chain +- remove unnecessary bool() and remove Any type from TypedDict get +- **bump**: improve readability and still bypass mypy check +- **commands**: remove unused args, type version command args +- **commit**: type commit args +- **check**: type CheckArgs arguments +- **check**: remove unused argument +- **changelog**: type untyped arguments +- **bump**: TypedDict for bump argument +- make methods protected, better type +- **conventional_commits**: remove unnecessary checks +- fix mypy output and better type +- **BaseCommitizen**: remove unused process_commit +- remove `TypeError` handling since `Python >=3.9` is required +- add comment clarifying `no_raise` parsing to `list[int]` +- **cli.py**: add type hints +- **mypy**: remove `unused-ignore` +- **changelog**: better typing, yield +- **cli**: early return and improve test coverage +- **git**: extract _create_commit_cmd_string +- misc cleanup +- **bump**: clean up +- **bump**: add type for out, replace function with re escape +- **BaseConfig**: use setter +- **changelog**: minor cleanup +- **git**: refactor get_tag_names +- **EOLType**: add eol enum back and reorganize methods +- **git**: code cleanup and better test coverage +- **commit**: simplify call +- **version_scheme**: cleanup +- improve readability and fix typos + +### Perf + +- **bump**: avoid unnecessary list construction and rename variable to avoid confusion +- **tags**: use set + +## v4.8.2 (2025-05-22) + +### Refactor + +- **check**: simplify code +- **check**: remove unnecessary variable + +## v4.8.1 (2025-05-22) + +### Refactor + +- **customize**: improve code readability + +## v4.8.0 (2025-05-20) + +### Feat + +- **cli**: add --tag-format argument to changelog command + +## v4.7.2 (2025-05-18) + +### Refactor + +- **default**: capitalize all constants and remove unnecessary variable + +## v4.7.1 (2025-05-16) + +### Fix + +- **bump**: don't fail if an invalid version tag is present (fix #1410) (#1418) + +## v4.7.0 (2025-05-10) + +### Feat + +- **providers**: add support for `Cargo.lock` + +### Refactor + +- **tests**: increase verbosity of variables + +## v4.6.3 (2025-05-07) + +### Fix + +- **changelog.py**: cross-platform path handling using os.path.join and modify the path linter and test parameter +- **changelog.py**: modify the CHANGELOG.md generated by cz bump --changelog to the right place + +## v4.6.2 (2025-05-05) + +### Fix + +- **docs**: fix url link and table formatting in the customization docs (#1399) + +## v4.6.1 (2025-05-05) + +### Fix + +- **commit**: use os.unlink to remove temp file + +## 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 diff --git a/commitizen/__init__.py b/commitizen/__init__.py index f16def4441..6db9e6e7db 100644 --- a/commitizen/__init__.py +++ b/commitizen/__init__.py @@ -1,7 +1,7 @@ import logging import logging.config -from colorama import init # type: ignore +from colorama import init from commitizen.cz.base import BaseCommitizen diff --git a/commitizen/__version__.py b/commitizen/__version__.py index 62bfee6c53..c7a3dc0c86 100644 --- a/commitizen/__version__.py +++ b/commitizen/__version__.py @@ -1 +1 @@ -__version__ = "3.27.0" +__version__ = "4.13.9" diff --git a/commitizen/bump.py b/commitizen/bump.py index 2351dbd7ec..cb572d3612 100644 --- a/commitizen/bump.py +++ b/commitizen/bump.py @@ -4,16 +4,23 @@ import re from collections import OrderedDict from glob import iglob +from logging import getLogger from string import Template -from typing import cast +from typing import TYPE_CHECKING, cast -from commitizen.defaults import MAJOR, MINOR, PATCH, bump_message, encoding +from commitizen.defaults import BUMP_MESSAGE, MAJOR, MINOR, PATCH from commitizen.exceptions import CurrentVersionNotFoundError from commitizen.git import GitCommit, smart_open -from commitizen.version_schemes import DEFAULT_SCHEME, Increment, Version, VersionScheme + +if TYPE_CHECKING: + from collections.abc import Generator, Iterable + + 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 @@ -38,22 +45,30 @@ def find_increment( 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) + return cast("Increment", increment) def update_version_in_files( current_version: str, new_version: str, - files: list[str], + version_files: Iterable[str], *, - check_consistency: bool = False, - encoding: str = encoding, + check_consistency: bool, + encoding: str, ) -> list[str]: """Change old version to the new one in every file given. @@ -63,16 +78,22 @@ def update_version_in_files( 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, - ) + updated_files = [] + + for path, pattern in _resolve_files_and_regexes(version_files, current_version): + current_version_found = False + bumped_lines = [] + + with open(path, encoding=encoding) as version_file: + for line in version_file: + bumped_line = ( + line.replace(current_version, new_version) + if pattern.search(line) + else line + ) + + current_version_found = current_version_found or bumped_line != line + bumped_lines.append(bumped_line) if check_consistency and not current_version_found: raise CurrentVersionNotFoundError( @@ -81,82 +102,32 @@ def update_version_in_files( "version_files are possibly inconsistent." ) + bumped_version_file_content = "".join(bumped_lines) + # Write the file out again with smart_open(path, "w", encoding=encoding) as file: - file.write(version_file) - updated.append(path) - return updated + file.write(bumped_version_file_content) + updated_files.append(path) + + return updated_files -def files_and_regexs(patterns: list[str], version: str) -> list[tuple[str, str]]: +def _resolve_files_and_regexes( + patterns: Iterable[str], version: str +) -> Generator[tuple[str, re.Pattern], None, None]: """ Resolve all distinct files with their regexp from a list of glob patterns with optional regexp """ - out = [] + filepath_set: set[tuple[str, str]] = set() 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 normalize_tag( - version: Version | str, - tag_format: str, - scheme: VersionScheme | 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 | - """ - scheme = scheme or DEFAULT_SCHEME - version = scheme(version) if isinstance(version, str) else version + regex = regex or re.escape(version) - major, minor, patch = version.release - prerelease = version.prerelease or "" + filepath_set.update((path, regex) for path in iglob(filepath)) - t = Template(tag_format) - return t.safe_substitute( - version=version, major=major, minor=minor, patch=patch, prerelease=prerelease - ) + return ((path, re.compile(regex)) for path, regex in sorted(filepath_set)) def create_commit_message( @@ -165,6 +136,6 @@ def create_commit_message( message_template: str | None = None, ) -> str: if message_template is None: - message_template = bump_message + 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 index 12d52f7b08..d8b8acccae 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -31,8 +31,10 @@ from collections import OrderedDict, defaultdict from dataclasses import dataclass from datetime import date -from typing import TYPE_CHECKING, Iterable +from itertools import chain +from typing import TYPE_CHECKING, Any +from deprecated import deprecated from jinja2 import ( BaseLoader, ChoiceLoader, @@ -41,20 +43,14 @@ Template, ) -from commitizen import out -from commitizen.bump import normalize_tag -from commitizen.cz.base import ChangelogReleaseHook from commitizen.exceptions import InvalidConfigurationError, NoCommitsFoundError -from commitizen.git import GitCommit, GitTag -from commitizen.version_schemes import ( - DEFAULT_SCHEME, - BaseVersion, - InvalidVersion, -) +from commitizen.tags import TagRules if TYPE_CHECKING: - from commitizen.cz.base import MessageBuilderHook - from commitizen.version_schemes import VersionScheme + from collections.abc import Generator, Iterable, Mapping, MutableMapping, Sequence + + from commitizen.cz.base import ChangelogReleaseHook, MessageBuilderHook + from commitizen.git import GitCommit, GitTag @dataclass @@ -67,43 +63,30 @@ class Metadata: unreleased_end: int | None = None latest_version: str | None = None latest_version_position: int | None = None + latest_version_tag: str | None = None - -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 __post_init__(self) -> None: + 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 tag_included_in_changelog( - tag: GitTag, - used_tags: list, - merge_prerelease: bool, - scheme: VersionScheme = DEFAULT_SCHEME, -) -> bool: - if tag in used_tags: - return False - - try: - version = scheme(tag.name) - except InvalidVersion: - return False - - if merge_prerelease and version.is_prerelease: - return False +@dataclass +class IncrementalMergeInfo: + """ + Information regarding the last non-pre-release, parsed from the changelog. - return True + Required to merge pre-releases on bump. + Separate from Metadata to not mess with the interface. + """ + name: str | None = None + index: int | None = None -def get_version_tags(scheme: type[BaseVersion], tags: list[GitTag]) -> list[GitTag]: - valid_tags: list[GitTag] = [] - for tag in tags: - try: - scheme(tag.name) - except InvalidVersion: - out.warn(f"InvalidVersion {tag}") - else: - valid_tags.append(tag) - return valid_tags +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( @@ -115,36 +98,40 @@ def generate_tree_from_commits( change_type_map: dict[str, str] | None = None, changelog_message_builder_hook: MessageBuilderHook | None = None, changelog_release_hook: ChangelogReleaseHook | None = None, - merge_prerelease: bool = False, - scheme: VersionScheme = DEFAULT_SCHEME, -) -> Iterable[dict]: + rules: TagRules | None = None, + during_version_bump: bool = False, +) -> Generator[dict[str, Any], None, None]: 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 + if during_version_bump and rules.merge_prereleases: + current_tag = None + else: + current_tag = get_commit_tag(commits[0], tags) if commits else None + current_tag_name = unreleased_version or "Unreleased" + current_tag_date = ( + date.today().isoformat() if unreleased_version is not None else "" + ) + + used_tags: set[GitTag] = set() + if current_tag: + used_tags.add(current_tag) + if current_tag.name: + current_tag_name = current_tag.name + current_tag_date = current_tag.date + commit_tag: GitTag | None = None changes: dict = defaultdict(list) - used_tags: list = [current_tag] for commit in commits: - commit_tag = get_commit_tag(commit, tags) - - if commit_tag is not None and tag_included_in_changelog( - commit_tag, used_tags, merge_prerelease, scheme=scheme + if ( + (commit_tag := get_commit_tag(commit, tags)) + and commit_tag not in used_tags + and rules.include_in_changelog(commit_tag) ): - used_tags.append(commit_tag) + used_tags.add(commit_tag) release = { "version": current_tag_name, "date": current_tag_date, @@ -157,24 +144,15 @@ def generate_tree_from_commits( current_tag_date = commit_tag.date changes = defaultdict(list) - matches = pat.match(commit.message) - if not matches: + if not pat.match(commit.message): 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 subject and body from commit message + for message in chain( + [map_pat.match(commit.message)], + (body_map_pat.match(block) for block in commit.body.split("\n\n")), + ): + if message: process_commit_message( changelog_message_builder_hook, message, @@ -197,43 +175,50 @@ 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 = { + ref_changes: MutableMapping[str | None, list], + change_type_map: Mapping[str, str] | None = None, +) -> None: + message: dict[str, Any] = { "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) + processed_msg = hook(message, commit) if hook else message + if not processed_msg: + return + + messages = [processed_msg] if isinstance(processed_msg, dict) else processed_msg + for msg in messages: + change_type = msg.pop("change_type", None) + if change_type_map and change_type: + change_type = change_type_map.get(change_type, change_type) + ref_changes[change_type].append(msg) -def order_changelog_tree(tree: Iterable, change_type_order: list[str]) -> Iterable: +def generate_ordered_changelog_tree( + tree: Iterable[Mapping[str, Any]], change_type_order: list[str] +) -> Generator[dict[str, Any], None, None]: if len(set(change_type_order)) != len(change_type_order): raise InvalidConfigurationError( - f"Change types contain duplicates types ({change_type_order})" + f"Change types contain duplicated 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 + yield { + **entry, + "changes": _calculate_sorted_changes(change_type_order, entry["changes"]), + } + + +def _calculate_sorted_changes( + change_type_order: list[str], changes: Mapping[str, Any] +) -> OrderedDict[str, Any]: + remaining_change_types = set(changes.keys()) - set(change_type_order) + sorted_change_types = change_type_order + sorted(remaining_change_types) + return OrderedDict((ct, changes[ct]) for ct in sorted_change_types if ct in changes) def get_changelog_template(loader: BaseLoader, template: str) -> Template: @@ -251,7 +236,7 @@ def render_changelog( tree: Iterable, loader: BaseLoader, template: str, - **kwargs, + **kwargs: Any, ) -> str: jinja_template = get_changelog_template(loader, template) changelog: str = jinja_template.render(tree=tree, **kwargs) @@ -277,6 +262,7 @@ def incremental_build( 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): @@ -286,9 +272,7 @@ def incremental_build( 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 + or latest_version_position > unreleased_end ): continue @@ -297,18 +281,34 @@ def incremental_build( 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) + + if latest_version_position is not None: + return output_lines + + 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_next_tag_name_after_version(tags: Iterable[GitTag], version: str) -> str | None: + it = iter(tag.name for tag in tags) + for name in it: + if name == version: + return next(it, None) + + raise NoCommitsFoundError(f"Could not find a valid revision range. {version=}") + + +@deprecated( + reason="This function is unused and will be removed in v5", + version="5.0.0", + category=DeprecationWarning, +) def get_smart_tag_range( - tags: list[GitTag], newest: str, oldest: str | None = None + tags: Sequence[GitTag], newest: str, oldest: str | None = None ) -> list[GitTag]: """Smart because it finds the N+1 tag. @@ -334,45 +334,36 @@ def get_smart_tag_range( def get_oldest_and_newest_rev( - tags: list[GitTag], + tags: Iterable[GitTag], version: str, - tag_format: str, - scheme: VersionScheme | None = None, -) -> tuple[str | None, str | None]: + rules: TagRules, +) -> tuple[str | None, str]: """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 - - newest_tag = normalize_tag(newest, tag_format=tag_format, scheme=scheme) - - oldest_tag = None - if oldest: - oldest_tag = normalize_tag(oldest, tag_format=tag_format, scheme=scheme) - - tags_range = get_smart_tag_range(tags, newest=newest_tag, oldest=oldest_tag) - if not tags_range: + oldest_version, sep, newest_version = version.partition("..") + if not sep: + newest_version = version + oldest_version = "" + + def get_tag_name(v: str) -> str: + if tag := rules.find_tag_for(tags, v): + return tag.name raise NoCommitsFoundError("Could not find a valid revision range.") - oldest_rev: str | None = tags_range[-1].name - newest_rev = newest_tag + newest_tag_name = get_tag_name(newest_version) + oldest_tag_name = get_tag_name(oldest_version) if oldest_version else None - # 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: - 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 + oldest_rev = get_next_tag_name_after_version( + tags, oldest_tag_name or newest_tag_name + ) - return oldest_rev, newest_rev + # Return None for oldest_rev if: + # 1. The oldest tag is the last tag in the list and matches the requested oldest tag + # 2. The oldest and the newest tag are the same + if oldest_rev == newest_tag_name: + return None, newest_tag_name + return oldest_rev, newest_tag_name diff --git a/commitizen/changelog_formats/__init__.py b/commitizen/changelog_formats/__init__.py index 782bfb24cb..26e697cadd 100644 --- a/commitizen/changelog_formats/__init__.py +++ b/commitizen/changelog_formats/__init__.py @@ -1,17 +1,17 @@ from __future__ import annotations -import sys -from typing import ClassVar, Protocol +import warnings +from importlib import metadata +from typing import TYPE_CHECKING, 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 +if TYPE_CHECKING: + from collections.abc import Callable + + from commitizen.changelog import IncrementalMergeInfo, Metadata + from commitizen.config.base_config import BaseConfig + CHANGELOG_FORMAT_ENTRYPOINT = "commitizen.changelog_format" TEMPLATE_EXTENSION = "j2" @@ -25,7 +25,7 @@ class ChangelogFormat(Protocol): config: BaseConfig - def __init__(self, config: BaseConfig): + def __init__(self, config: BaseConfig) -> None: self.config = config @property @@ -48,6 +48,12 @@ def get_metadata(self, filepath: str) -> Metadata: """ raise NotImplementedError + def get_latest_full_release(self, filepath: str) -> IncrementalMergeInfo: + """ + Extract metadata for the last non-pre-release. + """ + raise NotImplementedError + KNOWN_CHANGELOG_FORMATS: dict[str, type[ChangelogFormat]] = { ep.name: ep.load() @@ -64,10 +70,9 @@ def get_changelog_format( :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] + format = ( + name and KNOWN_CHANGELOG_FORMATS.get(name) or _guess_changelog_format(filename) + ) if not format: raise ChangelogFormatUnknown(f"Unknown changelog format '{name}'") @@ -75,7 +80,7 @@ def get_changelog_format( return format(config) -def guess_changelog_format(filename: str | None) -> type[ChangelogFormat] | None: +def _guess_changelog_format(filename: str | None) -> type[ChangelogFormat] | None: """ Try guessing the file format from the filename. @@ -91,3 +96,15 @@ def guess_changelog_format(filename: str | None) -> type[ChangelogFormat] | None if filename.endswith(f".{alt_extension}"): return format return None + + +def __getattr__(name: str) -> Callable[[str], type[ChangelogFormat] | None]: + if name == "guess_changelog_format": + warnings.warn( + "guess_changelog_format is deprecated and will be removed in v5. " + "Use _guess_changelog_format instead.", + DeprecationWarning, + stacklevel=2, + ) + return _guess_changelog_format + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/commitizen/changelog_formats/asciidoc.py b/commitizen/changelog_formats/asciidoc.py index d738926f6e..ed3e8607bd 100644 --- a/commitizen/changelog_formats/asciidoc.py +++ b/commitizen/changelog_formats/asciidoc.py @@ -1,24 +1,25 @@ 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) -> str | None: + 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 - matches = list(re.finditer(self.version_parser, m.group("title"))) - if not matches: - return None - return matches[-1].group("version") + 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) diff --git a/commitizen/changelog_formats/base.py b/commitizen/changelog_formats/base.py index 7c802d63d4..dcddb5d7e0 100644 --- a/commitizen/changelog_formats/base.py +++ b/commitizen/changelog_formats/base.py @@ -1,16 +1,20 @@ from __future__ import annotations -import os from abc import ABCMeta -from re import Pattern -from typing import IO, Any, ClassVar +from pathlib import Path +from typing import IO, TYPE_CHECKING, Any, ClassVar -from commitizen.changelog import Metadata +from commitizen.changelog import IncrementalMergeInfo, Metadata from commitizen.config.base_config import BaseConfig +from commitizen.git import GitTag +from commitizen.tags import TagRules, VersionTag from commitizen.version_schemes import get_version_scheme from . import ChangelogFormat +if TYPE_CHECKING: + from commitizen.config.base_config import BaseConfig + class BaseFormat(ChangelogFormat, metaclass=ABCMeta): """ @@ -20,21 +24,23 @@ class BaseFormat(ChangelogFormat, metaclass=ABCMeta): extension: ClassVar[str] = "" alternative_extensions: ClassVar[set[str]] = set() - def __init__(self, config: BaseConfig): + def __init__(self, config: BaseConfig) -> None: # 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"] - - @property - def version_parser(self) -> Pattern: - return get_version_scheme(self.config).parser + self.tag_rules = TagRules( + scheme=get_version_scheme(self.config.settings), + tag_format=self.config.settings["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): + file = Path(filepath) + if not file.is_file(): return Metadata() - with open(filepath, encoding=self.encoding) as changelog_file: + with file.open(encoding=self.config.settings["encoding"]) as changelog_file: return self.get_metadata_from_file(changelog_file) def get_metadata_from_file(self, file: IO[Any]) -> Metadata: @@ -55,9 +61,10 @@ def get_metadata_from_file(self, file: IO[Any]) -> Metadata: meta.unreleased_end = index # Try to find the latest release done - version = self.parse_version_from_title(line) - if version: - meta.latest_version = version + parsed_version = self.parse_version_from_title(line) + if parsed_version: + meta.latest_version = parsed_version.version + meta.latest_version_tag = parsed_version.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: @@ -65,7 +72,31 @@ def get_metadata_from_file(self, file: IO[Any]) -> Metadata: return meta - def parse_version_from_title(self, line: str) -> str | None: + def get_latest_full_release(self, filepath: str) -> IncrementalMergeInfo: + file = Path(filepath) + if not file.is_file(): + return IncrementalMergeInfo() + + with file.open(encoding=self.config.settings["encoding"]) as changelog_file: + return self.get_latest_full_release_from_file(changelog_file) + + def get_latest_full_release_from_file(self, file: IO[Any]) -> IncrementalMergeInfo: + latest_version_index: int | None = None + for index, line in enumerate(file): + latest_version_index = index + line = line.strip().lower() + + parsed_version = self.parse_version_from_title(line) + if ( + parsed_version + and not self.tag_rules.extract_version( + GitTag(parsed_version.tag, "", "") + ).is_prerelease + ): + return IncrementalMergeInfo(name=parsed_version.tag, index=index) + return IncrementalMergeInfo(index=latest_version_index) + + def parse_version_from_title(self, line: str) -> VersionTag | None: """ Extract the version from a title line if any """ diff --git a/commitizen/changelog_formats/markdown.py b/commitizen/changelog_formats/markdown.py index a5a0f42de3..e3d30fe174 100644 --- a/commitizen/changelog_formats/markdown.py +++ b/commitizen/changelog_formats/markdown.py @@ -1,9 +1,13 @@ 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" @@ -12,14 +16,11 @@ class Markdown(BaseFormat): RE_TITLE = re.compile(r"^(?P<level>#+) (?P<title>.*)$") - def parse_version_from_title(self, line: str) -> str | None: + def parse_version_from_title(self, line: str) -> VersionTag | None: m = self.RE_TITLE.match(line) if not m: return None - m = re.search(self.version_parser, m.group("title")) - if not m: - return None - return m.group("version") + return self.tag_rules.search_version(m.group("title")) def parse_title_level(self, line: str) -> int | None: m = self.RE_TITLE.match(line) diff --git a/commitizen/changelog_formats/restructuredtext.py b/commitizen/changelog_formats/restructuredtext.py index 37acf81ef3..e8ce411e80 100644 --- a/commitizen/changelog_formats/restructuredtext.py +++ b/commitizen/changelog_formats/restructuredtext.py @@ -1,95 +1,88 @@ from __future__ import annotations -import re -import sys from itertools import zip_longest -from typing import IO, TYPE_CHECKING, Any, Tuple, Union +from typing import IO 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: + def get_metadata_from_file(self, file: IO[str]) -> 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. + but determined by 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() + out_metadata = Metadata() + unreleased_title_kind: str | tuple[str, str] | None = None + is_overlined_title = False + lines = [line.strip().lower() for line in 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): + kind: str | tuple[str, str] | None = None + if _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): + is_overlined_title = True + elif not is_overlined_title and _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 - m = re.search(self.version_parser, title) - if m: - version = m.group("version") - meta.latest_version = version - 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 = ( - meta.latest_version_position if meta.latest_version else index + 1 + is_overlined_title = False + + if not title: + continue + + if "unreleased" in title: + unreleased_title_kind = kind + out_metadata.unreleased_start = index + continue + + if unreleased_title_kind and unreleased_title_kind == kind: + out_metadata.unreleased_end = index + # Try to find the latest release done + if version := self.tag_rules.search_version(title): + out_metadata.latest_version = version[0] + out_metadata.latest_version_tag = version[1] + out_metadata.latest_version_position = index + break + + if ( + out_metadata.unreleased_start is not None + and out_metadata.unreleased_end is None + ): + out_metadata.unreleased_end = ( + out_metadata.latest_version_position + if out_metadata.latest_version + else len(lines) ) - 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:]) - ) + return out_metadata + + +def _is_overlined_title(first: str, second: str, third: str) -> bool: + return ( + len(first) == len(third) >= len(second) + and first[0] == third[0] + and all(char == first[0] for char in first) + and _is_underlined_title(second, third) + ) + + +def _is_underlined_title(first: str, second: str) -> bool: + return ( + len(second) >= len(first) + and not second.isalnum() + and all(char == second[0] for char in second) + ) diff --git a/commitizen/changelog_formats/textile.py b/commitizen/changelog_formats/textile.py index 80118cdb3c..6693e5e002 100644 --- a/commitizen/changelog_formats/textile.py +++ b/commitizen/changelog_formats/textile.py @@ -1,22 +1,23 @@ 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) -> str | None: + def parse_version_from_title(self, line: str) -> VersionTag | None: if not self.RE_TITLE.match(line): return None - m = re.search(self.version_parser, line) - if not m: - return None - return m.group("version") + return self.tag_rules.search_version(line) def parse_title_level(self, line: str) -> int | None: m = self.RE_TITLE.match(line) diff --git a/commitizen/cli.py b/commitizen/cli.py index 1d1ebe1f68..79988fb5cb 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -7,12 +7,13 @@ from functools import partial from pathlib import Path from types import TracebackType -from typing import Any, Sequence +from typing import TYPE_CHECKING, cast import argcomplete from decli import cli from commitizen import commands, config, out, version_schemes +from commitizen.defaults import DEFAULT_SETTINGS from commitizen.exceptions import ( CommitizenException, ExitCode, @@ -47,21 +48,18 @@ def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, - kwarg: str | Sequence[Any] | None, + values: object, option_string: str | None = None, - ): - if not isinstance(kwarg, str): + ) -> None: + if not isinstance(values, str): return - if "=" not in kwarg: + + key, sep, value = values.partition("=") + if not key or not sep: 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) @@ -70,8 +68,7 @@ def __call__( { "name": ["--template", "-t"], "help": ( - "changelog template file name " - "(relative to the current working directory)" + "Changelog template file name (relative to the current working directory)." ), }, { @@ -79,33 +76,32 @@ def __call__( "action": ParseKwargs, "dest": "extras", "metavar": "EXTRA", - "help": "a changelog extra variable (in the form 'key=value')", + "help": "Changelog extra variables (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/" + "Commitizen is a powerful release management tool that helps teams maintain consistent and meaningful commit messages while automating version management.\n" + "For more information, please visit https://commitizen-tools.github.io/commitizen" ), "formatter_class": argparse.RawDescriptionHelpFormatter, "arguments": [ { "name": "--config", - "help": "the path of configuration file", + "help": "The path to the configuration file.", }, - {"name": "--debug", "action": "store_true", "help": "use debug mode"}, + {"name": "--debug", "action": "store_true", "help": "Use debug mode."}, { "name": ["-n", "--name"], - "help": "use the given commitizen (default: cz_conventional_commits)", + "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/", + "help": "Comma-separated error codes that won't raise error, e.g., cz -nr 1,2,3 bump. See codes at https://commitizen-tools.github.io/commitizen/exit_codes/", }, ], "subcommands": { @@ -114,146 +110,162 @@ def __call__( "commands": [ { "name": ["init"], - "description": "init commitizen configuration", - "help": "init commitizen configuration", + "description": "Initialize commitizen configuration", + "help": "Initialize commitizen configuration.", "func": commands.Init, }, { "name": ["commit", "c"], - "description": "create new commit", - "help": "create new commit", + "description": "Create new commit", + "help": "Create new commit.", "func": commands.Commit, "arguments": [ { "name": ["--retry"], "action": "store_true", - "help": "retry last commit", + "help": "Retry the last commit.", }, { "name": ["--no-retry"], "action": "store_true", "default": False, - "help": "skip retry if retry_after_failure is set to true", + "help": "Skip retry if --retry or `retry_after_failure` is set to true.", }, { "name": "--dry-run", "action": "store_true", - "help": "show output to stdout, no commit, no modified files", + "help": "Perform a dry run, without committing or modifying files.", }, { "name": "--write-message-to-file", "type": Path, "metavar": "FILE_PATH", - "help": "write message to file before committing (can be combined with --dry-run)", + "help": "Write message to FILE_PATH before committing (can be used with --dry-run).", }, { "name": ["-s", "--signoff"], "action": "store_true", - "help": "sign off the commit", + "help": "Deprecated, use `cz commit -- -s` instead.", }, { "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.", + # The help text aligns with the description of git commit --all + "help": "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", + "help": "Set the 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", + "description": "Show available Commitizens", + "help": "Show available Commitizens.", "func": commands.ListCz, }, { "name": "example", - "description": "show commit example", - "help": "show commit 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", + "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", + "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", + "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", + "help": "Perform a dry run, without committing or modifying files.", + }, + { + "name": "--files-only", # TODO: rename to --version-files-only + "action": "store_true", + "help": "Bump version in the `version_files` specified in the configuration file only(deprecated; use --version-files-only instead).", }, { - "name": "--files-only", + "name": "--version-files-only", "action": "store_true", - "help": "bump version in the files from the config", + "help": "Bump version in the files from the config", }, { "name": "--local-version", "action": "store_true", - "help": "bump only the local version portion", + "help": "Bump version only the local version portion (ignoring the public version).", }, { "name": ["--changelog", "-ch"], "action": "store_true", "default": False, - "help": "generate the changelog for the newest version", + "help": "Generate the changelog for the latest version.", }, { "name": ["--no-verify"], "action": "store_true", "default": False, - "help": "this option bypasses the pre-commit and commit-msg hooks", + # The help text aligns with the description of git commit --no-verify + "help": "Bypass the pre-commit and commit-msg hooks.", }, { "name": "--yes", "action": "store_true", - "help": "accept automatically questions done", + "help": "Accept automatically answered questions.", }, { "name": "--tag-format", "help": ( - "the format used to tag the commit and read it, " - "use it in existing projects, " - "wrap around simple quotes" + "The format used to tag the commit and read it. " + "Use it in existing projects, and wrap around simple quotes." ), }, { "name": "--bump-message", "help": ( - "template used to create the release commit, " - "useful when working with CI" + "Template used to create the release commit, useful when working with CI." ), }, { "name": ["--prerelease", "-pr"], - "help": "choose type of prerelease", + "help": "Type of prerelease.", "choices": ["alpha", "beta", "rc"], }, { "name": ["--devrelease", "-d"], - "help": "specify non-negative integer for dev. release", + "help": "Specify non-negative integer for dev release.", "type": int, }, { "name": ["--increment"], - "help": "manually specify the desired increment", + "help": "Specify the desired increment.", "choices": ["MAJOR", "MINOR", "PATCH"], "type": str.upper, }, @@ -262,35 +274,34 @@ def __call__( "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 " + "Set the method by which the new version is chosen. " + "'linear' (default) resolves the next version based on typical linear version progression, " + "where 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" + "without interpretation, ensuring the increment and pre-release are always honored." ), }, { "name": ["--check-consistency", "-cc"], "help": ( - "check consistency among versions defined in " - "commitizen configuration and version_files" + "Check consistency among versions defined in Commitizen configuration file and `version_files`." ), "action": "store_true", }, { "name": ["--annotated-tag", "-at"], - "help": "create annotated tag instead of lightweight one", + "help": "Create annotated tag instead of lightweight one.", "action": "store_true", }, { "name": ["--annotated-tag-message", "-atm"], - "help": "create annotated tag message", + "help": "Create annotated tag message.", "type": str, }, { "name": ["--gpg-sign", "-s"], - "help": "sign tag instead of lightweight one", + "help": "Sign tag instead of lightweight one.", "default": False, "action": "store_true", }, @@ -298,46 +309,46 @@ def __call__( "name": ["--changelog-to-stdout"], "action": "store_true", "default": False, - "help": "Output changelog to the stdout", + "help": "Output changelog to stdout.", }, { "name": ["--git-output-to-stderr"], "action": "store_true", "default": False, - "help": "Redirect git output to stderr", + "help": "Redirect git output to stderr.", }, { "name": ["--retry"], "action": "store_true", "default": False, - "help": "retry commit if it fails the 1st time", + "help": "Retry commit if it fails for the first time.", }, { "name": ["--major-version-zero"], "action": "store_true", "default": None, - "help": "keep major version at zero, even for breaking changes", + "help": "Keep major version at zero, even for breaking changes.", }, *deepcopy(tpl_arguments), { "name": "--file-name", - "help": "file name of changelog (default: 'CHANGELOG.md')", + "help": "File name of changelog (default: 'CHANGELOG.md').", }, { "name": ["--prerelease-offset"], "type": int, "default": None, - "help": "start pre-releases with this offset", + "help": "Start pre-releases with this offset.", }, { "name": ["--version-scheme"], - "help": "choose version scheme", + "help": "Choose version scheme.", "default": None, "choices": version_schemes.KNOWN_SCHEMES, }, { "name": ["--version-type"], - "help": "Deprecated, use --version-scheme", + "help": "Deprecated, use `--version-scheme` instead.", "default": None, "choices": version_schemes.KNOWN_SCHEMES, }, @@ -345,23 +356,35 @@ def __call__( "name": "manual_version", "type": str, "nargs": "?", - "help": "bump to the given version (e.g: 1.5.3)", + "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", + "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)" + "Generate changelog (note that it will overwrite existing files)" ), "help": ( - "generate changelog (note that it will overwrite existing file)" + "Generate changelog (note that it will overwrite existing files)." ), "func": commands.Changelog, "arguments": [ @@ -369,17 +392,17 @@ def __call__( "name": "--dry-run", "action": "store_true", "default": False, - "help": "show changelog to stdout", + "help": "Show changelog to stdout.", }, { "name": "--file-name", - "help": "file name of changelog (default: 'CHANGELOG.md')", + "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" + "Set the value for the new version (use the tag value), " + "instead of using unreleased versions." ), }, { @@ -387,22 +410,22 @@ def __call__( "action": "store_true", "default": False, "help": ( - "generates changelog from last created version, " - "useful if the changelog has been manually modified" + "Generate changelog from the 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)", + "help": "Generate 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" + "Start rev of the changelog. " + "If not set, it will generate changelog from the beginning." ), }, { @@ -410,134 +433,158 @@ def __call__( "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" + "Collect all changes from prereleases into the next non-prerelease. " + "If not set, it will include prereleases in the changelog." ), }, { "name": ["--version-scheme"], - "help": "choose 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", + "help": "Export the changelog template into this file instead of rendering it.", }, *deepcopy(tpl_arguments), + { + "name": "--tag-format", + "help": "The format of the tag, wrap around simple quotes.", + }, ], }, { "name": ["check"], - "description": "validates that a commit message matches the commitizen schema", - "help": "validates that a commit message matches the commitizen schema", + "description": "Validate that a commit message matches the commitizen schema", + "help": "Validate 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" + "Ask for the name of the temporary file that contains the commit message. " + "Use 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", + "help": "Validate the commits in the given range of git rev, e.g., master..HEAD.", + "exclusive_group": "group1", + }, + { + "name": ["-d", "--use-default-range"], + "action": "store_true", + "default": False, + "help": "Validate the commits from the default branch to HEAD, e.g., refs/remotes/origin/master..HEAD.", "exclusive_group": "group1", }, { "name": ["-m", "--message"], - "help": "commit message that needs to be checked", + "help": "Validate the given commit message.", "exclusive_group": "group1", }, { "name": ["--allow-abort"], "action": "store_true", "default": False, - "help": "allow empty commit messages, which typically abort a commit", + "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", + "help": "Skip validation for commit messages that start with the specified prefixes.", + }, + { + "name": ["-l", "--message-length-limit"], + "type": int, + "help": "Restrict the length of the **first line** 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)" + "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)" + "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", + "help": "Output the system information for reporting bugs.", "action": "store_true", "exclusive_group": "group1", }, { "name": ["-p", "--project"], - "help": "get the version of the current project", + "help": "Output the version of the current project.", "action": "store_true", "exclusive_group": "group1", }, { "name": ["-c", "--commitizen"], - "help": "get the version of the installed commitizen", + "help": "Output 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" + "Output the version of both the installed commitizen and the current project." ), "action": "store_true", "exclusive_group": "group1", }, + { + "name": ["--major"], + "help": "Output just the major version. Must be used with --project or --verbose.", + "action": "store_true", + "exclusive_group": "group2", + }, + { + "name": ["--minor"], + "help": "Output just the minor version. Must be used with --project or --verbose.", + "action": "store_true", + "exclusive_group": "group2", + }, + { + "name": ["--tag"], + "help": "get the version with tag prefix. Need to be used with --project or --verbose.", + "action": "store_true", + "exclusive_group": "group2", + }, ], }, ], }, } -original_excepthook = sys.excepthook - def commitizen_excepthook( - type, value, traceback, debug=False, no_raise: list[int] | None = None -): + exctype: type[BaseException], + value: BaseException, + traceback: TracebackType | None, + debug: bool = False, + no_raise: list[int] | None = 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 = 0 - sys.exit(exit_code) - else: - original_excepthook(type, value, traceback) - + if not isinstance(value, CommitizenException): + sys.__excepthook__(exctype, value, traceback) + return -commitizen_debug_excepthook = partial(commitizen_excepthook, debug=True) - -sys.excepthook = commitizen_excepthook + if value.message: + value.output_method(value.message) + if debug: + sys.__excepthook__(exctype, value, traceback) + exit_code = value.exit_code + if no_raise is not None and exit_code in no_raise: + sys.exit(ExitCode.EXPECTED_EXIT) + sys.exit(exit_code) def parse_no_raise(comma_separated_no_raise: str) -> list[int]: @@ -546,24 +593,50 @@ def parse_no_raise(comma_separated_no_raise: str) -> list[int]: 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) + def exit_code_from_str_or_skip(s: str) -> ExitCode | None: + try: + return ExitCode.from_str(s) + except (KeyError, ValueError): + out.warn(f"WARN: no_raise value `{s}` is not a valid exit code. Skipping.") + return None + + return [ + code.value + for s in comma_separated_no_raise.split(",") + if (code := exit_code_from_str_or_skip(s)) is not None + ] + + +if TYPE_CHECKING: + + class Args(argparse.Namespace): + config: str | None = None + debug: bool = False + name: str | None = None + no_raise: str | None = None # comma-separated string, later parsed as list[int] + report: bool = False + project: bool = False + commitizen: bool = False + verbose: bool = False + func: type[ + commands.Init # init + | commands.Commit # commit (c) + | commands.ListCz # ls + | commands.Example # example + | commands.Info # info + | commands.Schema # schema + | commands.Bump # bump + | commands.Changelog # changelog (ch) + | commands.Check # check + | commands.Version # version + ] + + +def main() -> None: + sys.excepthook = commitizen_excepthook + + parser: argparse.ArgumentParser = cli(data) argcomplete.autocomplete(parser) # Show help if no arg provided if len(sys.argv) == 1: @@ -573,11 +646,8 @@ def main(): # 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): + except SystemExit as e: + if e.code == 2: raise NoCommandFoundError() raise e @@ -603,27 +673,20 @@ def main(): 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() - + conf = config.read_cfg(args.config) + args = cast("Args", args) if args.name: conf.update({"name": args.name}) - elif not args.name and not conf.path: - conf.update({"name": "cz_conventional_commits"}) + elif not conf.path: + conf.update({"name": DEFAULT_SETTINGS["name"]}) 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)() + sys.excepthook = partial(sys.excepthook, debug=True) + if args.no_raise: + sys.excepthook = partial(sys.excepthook, no_raise=parse_no_raise(args.no_raise)) + + args.func(conf, arguments)() # type: ignore[arg-type] if __name__ == "__main__": diff --git a/commitizen/cmd.py b/commitizen/cmd.py index ba48ac7881..fe70da9c9b 100644 --- a/commitizen/cmd.py +++ b/commitizen/cmd.py @@ -1,11 +1,16 @@ +from __future__ import annotations + import os import subprocess -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from charset_normalizer import from_bytes from commitizen.exceptions import CharacterSetDecodeError +if TYPE_CHECKING: + from collections.abc import Mapping + class Command(NamedTuple): out: str @@ -19,16 +24,18 @@ 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 + pass + + 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: +def run(cmd: str, env: Mapping[str, str] | None = None) -> Command: if env is not None: env = {**os.environ, **env} process = subprocess.Popen( diff --git a/commitizen/commands/__init__.py b/commitizen/commands/__init__.py index 806e384522..58b18297dc 100644 --- a/commitizen/commands/__init__.py +++ b/commitizen/commands/__init__.py @@ -11,13 +11,13 @@ __all__ = ( "Bump", + "Changelog", "Check", "Commit", - "Changelog", "Example", "Info", + "Init", "ListCz", "Schema", "Version", - "Init", ) diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index 8497384665..6084c8c151 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -2,13 +2,14 @@ import warnings from logging import getLogger +from typing import TYPE_CHECKING, 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, @@ -20,54 +21,82 @@ NoPatternMapError, NotAGitProjectError, NotAllowed, - NoVersionSpecifiedError, ) from commitizen.providers import get_provider +from commitizen.tags import TagRules from commitizen.version_schemes import ( Increment, InvalidVersion, Prerelease, + VersionProtocol, get_version_scheme, ) +if TYPE_CHECKING: + from commitizen.config import BaseConfig + logger = getLogger("commitizen") +class BumpArgs(Settings, total=False): + allow_no_commit: bool | None + annotated_tag_message: str | None + build_metadata: str | None + changelog_to_stdout: bool + changelog: bool + check_consistency: bool + devrelease: int | None + dry_run: bool + file_name: str + files_only: bool | None + version_files_only: bool | None + get_next: bool # TODO: maybe rename to `next_version_to_stdout` + git_output_to_stderr: bool + increment_mode: str + increment: Increment | None + local_version: bool + manual_version: str | None + no_verify: bool + prerelease: Prerelease | None + retry: bool + yes: bool + + class Bump: """Show prompt for the user to create a guided commit.""" - def __init__(self, config: BaseConfig, arguments: dict): + def __init__(self, config: BaseConfig, arguments: BumpArgs) -> None: 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.arguments = arguments + self.bump_settings = cast( + "BumpArgs", + { + **config.settings, + **{ + k: v + for k in ( + "annotated_tag_message", + "annotated_tag", + "bump_message", + "file_name", + "gpg_sign", + "increment_mode", + "increment", + "major_version_zero", + "prerelease_offset", + "prerelease", + "tag_format", + "template", + ) + if (v := arguments.get(k)) is not None + }, }, - } - self.cz = factory.commiter_factory(self.config) - self.changelog = arguments["changelog"] or self.config.settings.get( - "update_changelog_on_bump" ) + self.cz = factory.committer_factory(self.config) + self.changelog_flag = arguments["changelog"] self.changelog_to_stdout = arguments["changelog_to_stdout"] self.git_output_to_stderr = arguments["git_output_to_stderr"] self.no_verify = arguments["no_verify"] @@ -79,12 +108,12 @@ def __init__(self, config: BaseConfig, arguments: dict): if deprecated_version_type: warnings.warn( DeprecationWarning( - "`--version-type` parameter is deprecated and will be removed in commitizen 4. " + "`--version-type` parameter is deprecated and will be removed in v5. " "Please use `--version-scheme` instead" ) ) self.scheme = get_version_scheme( - self.config, arguments["version_scheme"] or deprecated_version_type + self.config.settings, arguments["version_scheme"] or deprecated_version_type ) self.file_name = arguments["file_name"] or self.config.settings.get( "changelog_file" @@ -98,29 +127,29 @@ def __init__(self, config: BaseConfig, arguments: dict): ) self.extras = arguments["extras"] - def is_initial_tag(self, current_tag_version: str, is_yes: bool = False) -> bool: + 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 git.tag_exist(current_tag_version): - if is_yes: - is_initial = True - else: - out.info(f"Tag {current_tag_version} could not be found. ") - out.info( - "Possible causes:\n" - "- version in configuration is not the current version\n" - "- tag_format is missing, check them using 'git tag --list'\n" - ) - is_initial = questionary.confirm("Is this the first tag created?").ask() - return is_initial + if current_tag: + return False + if is_yes: + return True + + out.info("No tag matching configuration could 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" + ) + return bool(questionary.confirm("Is this the first tag created?").ask()) - def find_increment(self, commits: list[git.GitCommit]) -> Increment | None: + 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 + if self.bump_settings["major_version_zero"] else self.cz.bump_map ) bump_pattern = self.cz.bump_pattern @@ -129,145 +158,138 @@ def find_increment(self, commits: list[git.GitCommit]) -> Increment | None: 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() - - tag_format: str = self.bump_settings["tag_format"] - 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"] - - 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 prerelease_offset: - raise NotAllowed( - "--prerelease-offset 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" - ) - - current_tag_version: str = bump.normalize_tag( - current_version, - tag_format=tag_format, - scheme=self.scheme, - ) - - is_initial = self.is_initial_tag(current_tag_version, is_yes) + return bump.find_increment(commits, regex=bump_pattern, increments_map=bump_map) + + def _validate_arguments(self, current_version: VersionProtocol) -> None: + errors: list[str] = [] + if self.arguments["manual_version"]: + for val, option in ( + (self.arguments["increment"], "--increment"), + (self.arguments["prerelease"], "--prerelease"), + (self.arguments["devrelease"] is not None, "--devrelease"), + (self.arguments["local_version"], "--local-version"), + (self.arguments["build_metadata"], "--build-metadata"), + (self.arguments["major_version_zero"], "--major-version-zero"), + ): + if val: + errors.append(f"{option} cannot be combined with MANUAL_VERSION") + + if self.arguments["major_version_zero"] and current_version.release[0]: + errors.append( + f"--major-version-zero is meaningless for current version {current_version}" + ) + if self.arguments["build_metadata"] and self.arguments["local_version"]: + errors.append("--local-version cannot be combined with --build-metadata") - # If user specified changelog_to_stdout, they probably want the - # changelog to be generated as well, this is the most intuitive solution - self.changelog = self.changelog or bool(self.changelog_to_stdout) + if errors: + raise NotAllowed("\n".join(errors)) - if manual_version: + def _resolve_increment_and_new_version( + self, current_version: VersionProtocol, current_tag: git.GitTag | None + ) -> tuple[Increment | None, VersionProtocol]: + increment = self.arguments["increment"] + if manual_version := self.arguments["manual_version"]: try: - new_version = self.scheme(manual_version) + return increment, 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 is_initial: - commits = git.get_commits() - else: - commits = git.get_commits(current_tag_version) - - # 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: - raise NoCommitsFoundError( - "[NO_COMMITS_FOUND]\n" "No new commits found." - ) - increment = self.find_increment(commits) + if increment is None: + commits = git.get_commits(current_tag.name if current_tag else None) + + # 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 self.arguments["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 ( + self.arguments["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`" + ) - # 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 self.arguments["allow_no_commit"]: + increment = "PATCH" + + return increment, current_version.bump( + increment, + prerelease=self.arguments["prerelease"], + prerelease_offset=self.bump_settings["prerelease_offset"], + devrelease=self.arguments["devrelease"], + is_local_version=self.arguments["local_version"], + build_metadata=self.arguments["build_metadata"], + exact_increment=self.arguments["increment_mode"] == "exact", + ) + + def __call__(self) -> None: + """Steps executed to bump.""" + provider = get_provider(self.config) + current_version = self.scheme(provider.get_version()) + self._validate_arguments(current_version) + + next_version_to_stdout = self.arguments["get_next"] + if next_version_to_stdout: + for value, option in ( + (self.changelog_flag, "--changelog"), + (self.changelog_to_stdout, "--changelog-to-stdout"), + ): + if value: + warnings.warn(f"{option} has no effect when used with --get-next") - 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", + # 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 = any( + ( + self.changelog_flag, + self.changelog_to_stdout, + self.config.settings.get("update_changelog_on_bump"), ) + ) - new_tag_version = bump.normalize_tag( - new_version, - tag_format=tag_format, - scheme=self.scheme, + rules = TagRules.from_settings(cast("Settings", self.bump_settings)) + current_tag = rules.find_tag_for(git.get_tags(), current_version) + current_tag_version = ( + current_tag.name if current_tag else rules.normalize_tag(current_version) ) - message = bump.create_commit_message( - current_version, new_version, bump_commit_message + + is_initial = self._is_initial_tag(current_tag, self.arguments["yes"]) + + increment, new_version = self._resolve_increment_and_new_version( + current_version, current_tag ) + new_tag_version = rules.normalize_tag(new_version) + if next_version_to_stdout: + 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 DryRunExit() + + message = bump.create_commit_message( + current_version, new_version, self.bump_settings["bump_message"] + ) # Report found information - information = f"{message}\n" f"tag to create: {new_tag_version}\n" + information = f"{message}\ntag to create: {new_tag_version}\n" if increment: information += f"increment detected: {increment}\n" @@ -281,42 +303,50 @@ def __call__(self) -> None: # noqa: C901 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" + "[NO_COMMITS_TO_BUMP]\nThe commits found are not eligible to be bumped" ) - files: list[str] = [] - if self.changelog: - args = { + updated_files: list[str] = [] + changelog_file_name = None + dry_run = self.arguments["dry_run"] + if self.changelog_flag: + changelog_args = { "unreleased_version": new_tag_version, "template": self.template, "extras": self.extras, "incremental": True, "dry_run": dry_run, + # governs logic for merge_prerelease + "during_version_bump": self.arguments["prerelease"] is None, } if self.changelog_to_stdout: - changelog_cmd = Changelog(self.config, {**args, "dry_run": True}) try: - changelog_cmd() + Changelog( + self.config, + {**changelog_args, "dry_run": True}, # type: ignore[typeddict-item] + )() except DryRunExit: pass - args["file_name"] = self.file_name - changelog_cmd = Changelog(self.config, args) + changelog_cmd = Changelog( + self.config, + {**changelog_args, "file_name": self.file_name}, # type: ignore[typeddict-item] + ) changelog_cmd() - files.append(changelog_cmd.file_name) + changelog_file_name = changelog_cmd.file_name + updated_files.append(changelog_file_name) # Do not perform operations over files or git. if dry_run: raise DryRunExit() - files.extend( + updated_files.extend( bump.update_version_in_files( str(current_version), str(new_version), - version_files, + self.bump_settings["version_files"], check_consistency=self.check_consistency, - encoding=self.encoding, + encoding=self.config.settings["encoding"], ) ) @@ -333,43 +363,52 @@ def __call__(self) -> None: # noqa: C901 new_tag_version=new_tag_version, message=message, increment=increment, - changelog_file_name=changelog_cmd.file_name if self.changelog else None, + changelog_file_name=changelog_file_name, ) - if is_files_only: + if self.arguments.get("files_only"): + warnings.warn( + "--files-only is deprecated and will be removed in v5. Use --version-files-only instead.", + DeprecationWarning, + ) + raise ExpectedExit() + + if self.arguments.get("version_files_only"): raise ExpectedExit() # FIXME: check if any changes have been staged - git.add(*files) + git.add(*updated_files) c = git.commit(message, args=self._get_commit_args()) - if self.retry and c.return_code != 0 and self.changelog: + 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) + git.add(*updated_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) + for msg in (c.out, c.err): + if msg: + out_func = out.diagnostic if self.git_output_to_stderr else out.write + out_func(msg) 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)), + signed=any( + ( + self.bump_settings.get("gpg_sign"), + self.config.settings.get("gpg_sign"), + ) + ), + annotated=any( + ( + self.bump_settings.get("annotated_tag"), + self.config.settings.get("annotated_tag"), + self.bump_settings.get("annotated_tag_message"), + ) + ), msg=self.bump_settings.get("annotated_tag_message", None), # TODO: also get from self.config.settings? ) @@ -387,7 +426,7 @@ def __call__(self) -> None: # noqa: C901 current_tag_version=new_tag_version, message=message, increment=increment, - changelog_file_name=changelog_cmd.file_name if self.changelog else None, + changelog_file_name=changelog_file_name, ) # TODO: For v3 output this only as diagnostic and remove this if diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index bda7a1844f..5093ed9f29 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -1,15 +1,12 @@ 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 typing import TYPE_CHECKING, Any, TypedDict, cast -from commitizen import bump, changelog, defaults, factory, git, out +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, @@ -20,72 +17,115 @@ NotAllowed, ) from commitizen.git import GitTag, smart_open +from commitizen.tags import TagRules from commitizen.version_schemes import get_version_scheme +if TYPE_CHECKING: + from collections.abc import Generator, Iterable + + from commitizen.config import BaseConfig + + +class ChangelogArgs(TypedDict, total=False): + change_type_map: dict[str, str] + change_type_order: list[str] + current_version: str + dry_run: bool + file_name: str + incremental: bool + merge_prerelease: bool + rev_range: str + start_rev: str + tag_format: str + unreleased_version: str | None + version_scheme: str + template: str + extras: dict[str, Any] + export_template: str + during_version_bump: bool | None + class Changelog: """Generate a changelog based on the commit history.""" - def __init__(self, config: BaseConfig, args): + def __init__(self, config: BaseConfig, arguments: ChangelogArgs) -> None: 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.config = config - self.start_rev = args.get("start_rev") or self.config.settings.get( - "changelog_start_rev" + changelog_file_name = arguments.get("file_name") or self.config.settings.get( + "changelog_file" ) - self.file_name = args.get("file_name") or cast( - str, self.config.settings.get("changelog_file") - ) - if not isinstance(self.file_name, str): + if not isinstance(changelog_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.file_name = ( + Path(self.config.path.parent, changelog_file_name).as_posix() + if self.config.path is not None + else changelog_file_name + ) + + self.cz = factory.committer_factory(self.config) + + self.start_rev = arguments.get("start_rev") or self.config.settings.get( + "changelog_start_rev" + ) + self.changelog_format = get_changelog_format(self.config, self.file_name) - self.incremental = args["incremental"] or self.config.settings.get( - "changelog_incremental" + self.incremental = bool( + arguments.get("incremental") + or self.config.settings.get("changelog_incremental") ) - self.dry_run = args["dry_run"] + self.dry_run = bool(arguments.get("dry_run")) - self.scheme = get_version_scheme(self.config, args.get("version_scheme")) + self.scheme = get_version_scheme( + self.config.settings, arguments.get("version_scheme") + ) current_version = ( - args.get("current_version", config.settings.get("version")) or "" + arguments.get("current_version") + or self.config.settings.get("version") + or "" ) self.current_version = self.scheme(current_version) if current_version else None - self.unreleased_version = args["unreleased_version"] + self.unreleased_version = arguments["unreleased_version"] self.change_type_map = ( self.config.settings.get("change_type_map") or self.cz.change_type_map ) - self.change_type_order = ( + self.change_type_order = cast( + "list[str]", self.config.settings.get("change_type_order") or self.cz.change_type_order - or defaults.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.rev_range = arguments.get("rev_range") + self.tag_rules = TagRules( + scheme=self.scheme, + tag_format=arguments.get("tag_format") + or self.config.settings["tag_format"], + legacy_tag_formats=self.config.settings["legacy_tag_formats"], + ignored_tag_formats=self.config.settings["ignored_tag_formats"], + merge_prereleases=arguments.get("merge_prerelease") + or self.config.settings["changelog_merge_prerelease"], ) - self.merge_prerelease = args.get( - "merge_prerelease" - ) or self.config.settings.get("changelog_merge_prerelease") self.template = ( - args.get("template") + arguments.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") + self.extras = arguments.get("extras") or {} + self.export_template_to = arguments.get("export_template") + + self.during_version_bump: bool = arguments.get("during_version_bump") or False - def _find_incremental_rev(self, latest_version: str, tags: list[GitTag]) -> str: + def _find_incremental_rev(self, latest_version: str, tags: Iterable[GitTag]) -> str: """Try to find the 'start_rev'. We use a similarity approach. We know how to parse the version from the markdown @@ -98,29 +138,31 @@ def _find_incremental_rev(self, latest_version: str, tags: list[GitTag]) -> str: on our experience. """ SIMILARITY_THRESHOLD = 0.89 - tag_ratio = map( - lambda tag: ( - SequenceMatcher( + scores_and_tag_names: Generator[tuple[float, str]] = ( + ( + score, + tag.name, + ) + for tag in tags + if ( + score := SequenceMatcher( None, latest_version, strip_local_version(tag.name) - ).ratio(), - tag, - ), - tags, + ).ratio() + ) + >= SIMILARITY_THRESHOLD ) try: - score, tag = max(tag_ratio, key=itemgetter(0)) + _, start_rev = max(scores_and_tag_names, key=itemgetter(0)) except ValueError: raise NoRevisionError() - if score < SIMILARITY_THRESHOLD: - raise NoRevisionError() - start_rev = tag.name return start_rev - def write_changelog( + 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: + ) -> None: + with smart_open( + self.file_name, "w", encoding=self.config.settings["encoding"] + ) as changelog_file: partial_changelog: str | None = None if self.incremental: new_lines = changelog.incremental_build( @@ -129,33 +171,28 @@ def write_changelog( changelog_out = "".join(new_lines) partial_changelog = changelog_out - if changelog_hook: - changelog_out = changelog_hook(changelog_out, partial_changelog) + if self.cz.changelog_hook: + changelog_out = self.cz.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 _export_template(self, dist: str) -> None: + filename = changelog.get_changelog_template( + self.cz.template_loader, self.template + ).filename + if filename is None: + raise NotAllowed("Template filename is not set") + + text = Path(filename).read_text() + Path(dist).write_text(text) - def __call__(self): + def __call__(self) -> None: 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 - ) - merge_prerelease = self.merge_prerelease if self.export_template_to: - return self.export_template() + return self._export_template(self.export_template_to) if not changelog_pattern or not commit_parser: raise NoPatternMapError( @@ -166,30 +203,56 @@ def __call__(self): raise NotAllowed("--incremental cannot be combined with a rev_range") # Don't continue if no `file_name` specified. - assert self.file_name - - tags = changelog.get_version_tags(self.scheme, git.get_tags()) or [] + if not self.file_name: + raise NotAllowed("filename is required.") - end_rev = "" + tags = self.tag_rules.get_version_tags(git.get_tags(), warn=True) + changelog_meta = changelog.Metadata() if self.incremental: changelog_meta = self.changelog_format.get_metadata(self.file_name) if changelog_meta.latest_version: - latest_tag_version: str = bump.normalize_tag( - changelog_meta.latest_version, - tag_format=self.tag_format, - scheme=self.scheme, - ) start_rev = self._find_incremental_rev( - strip_local_version(latest_tag_version), tags + strip_local_version(changelog_meta.latest_version_tag or ""), tags ) + end_rev = "" if self.rev_range: start_rev, end_rev = changelog.get_oldest_and_newest_rev( tags, - version=self.rev_range, - tag_format=self.tag_format, - scheme=self.scheme, + self.rev_range, + self.tag_rules, + ) + + if self.during_version_bump and self.tag_rules.merge_prereleases: + latest_full_release_info = self.changelog_format.get_latest_full_release( + self.file_name ) + # Determine if there are prereleases to merge: + # - Only prereleases in changelog (no full release found), OR + # - First version in changelog is before first full release (prereleases exist) + if latest_full_release_info.index is not None and ( + latest_full_release_info.name is None + or ( + changelog_meta.latest_version_position is not None + and changelog_meta.latest_version_position + < latest_full_release_info.index + ) + ): + # Use the existing unreleased_start if available (from get_metadata()). + # Otherwise, use the position of the first version entry (prerelease) + # to preserve the changelog header. + if changelog_meta.unreleased_start is None: + changelog_meta.unreleased_start = ( + changelog_meta.latest_version_position + ) + changelog_meta.latest_version_position = latest_full_release_info.index + changelog_meta.unreleased_end = latest_full_release_info.index - 1 + + start_rev = latest_full_release_info.name or "" + if not start_rev: + # Only pre-releases in changelog + changelog_meta.latest_version_position = None + changelog_meta.unreleased_end = latest_full_release_info.index + 1 commits = git.get_commits(start=start_rev, end=end_rev, args="--topo-order") if not commits and ( @@ -202,35 +265,43 @@ def __call__(self): 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, - merge_prerelease=merge_prerelease, - scheme=self.scheme, + self.unreleased_version, + change_type_map=self.change_type_map, + changelog_message_builder_hook=self.cz.changelog_message_builder_hook, + changelog_release_hook=self.cz.changelog_release_hook, + rules=self.tag_rules, + during_version_bump=self.during_version_bump, ) if self.change_type_order: - tree = changelog.order_changelog_tree(tree, self.change_type_order) + tree = changelog.generate_ordered_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") + tree, + self.cz.template_loader, + self.template, + **{ + "incremental": self.incremental, # extra variable for the template + **self.cz.template_extras, + **self.config.settings["extras"], + **self.extras, + }, + ).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, "") + if self.cz.changelog_hook: + changelog_out = self.cz.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: + changelog_path = Path(self.file_name) + if self.incremental and changelog_path.is_file(): + with changelog_path.open( + encoding=self.config.settings["encoding"] + ) as changelog_file: lines = changelog_file.readlines() - self.write_changelog(changelog_out, lines, changelog_meta) + self._write_changelog(changelog_out, lines, changelog_meta) diff --git a/commitizen/commands/check.py b/commitizen/commands/check.py index 6e98f8cb3f..ab5e671d67 100644 --- a/commitizen/commands/check.py +++ b/commitizen/commands/check.py @@ -1,23 +1,36 @@ from __future__ import annotations -import os import re import sys -from typing import Any +from pathlib import Path +from typing import TYPE_CHECKING, TypedDict from commitizen import factory, git, out -from commitizen.config import BaseConfig from commitizen.exceptions import ( InvalidCommandArgumentError, InvalidCommitMessageError, NoCommitsFoundError, ) +if TYPE_CHECKING: + from commitizen.config import BaseConfig + + +class CheckArgs(TypedDict, total=False): + commit_msg_file: str + commit_msg: str + rev_range: str + allow_abort: bool + message_length_limit: int + allowed_prefixes: list[str] + message: str + use_default_range: bool + class Check: """Check if the current commit msg matches the commitizen format.""" - def __init__(self, config: BaseConfig, arguments: dict[str, Any], cwd=os.getcwd()): + def __init__(self, config: BaseConfig, arguments: CheckArgs, *args: object) -> None: """Initial check command. Args: @@ -25,15 +38,19 @@ def __init__(self, config: BaseConfig, arguments: dict[str, Any], cwd=os.getcwd( 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( + self.commit_msg_file = arguments.get("commit_msg_file") + self.commit_msg = arguments.get("message") + self.rev_range = arguments.get("rev_range") + self.allow_abort = bool( arguments.get("allow_abort", config.settings["allow_abort"]) ) - # we need to distinguish between None and [], which is a valid value + self.use_default_range = bool(arguments.get("use_default_range")) + self.max_msg_length = arguments.get( + "message_length_limit", config.settings.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 @@ -41,72 +58,79 @@ def __init__(self, config: BaseConfig, arguments: dict[str, Any], cwd=os.getcwd( 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) + 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: str | None = sys.stdin.read() - elif num_exclusive_args_provided != 1: + + if 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): + if num_exclusive_args_provided == 0 and not sys.stdin.isatty(): + self.commit_msg = sys.stdin.read() + + self.config: BaseConfig = config + self.cz = factory.committer_factory(self.config) + + def __call__(self) -> None: """Validate if commit messages follows the conventional pattern. Raises: - InvalidCommitMessageError: if the commit provided not follows the conventional pattern + InvalidCommitMessageError: if the commit provided does not follow the conventional pattern + NoCommitsFoundError: if no commit is found with the given range """ 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 + pattern = re.compile(self.cz.schema_pattern()) + invalid_commits = [ + (commit, check.errors) for commit in commits - if not self.validate_commit_message(commit.message, pattern) + if not ( + check := self.cz.validate_commit_message( + commit_msg=commit.message, + pattern=pattern, + allow_abort=self.allow_abort, + allowed_prefixes=self.allowed_prefixes, + max_msg_length=self.max_msg_length, + commit_hash=commit.rev, + ) + ).is_valid ] - displayed_msgs_content = "\n".join( - [ - f'commit "{commit.rev}": "{commit.message}"' - for commit in ill_formated_commits - ] - ) - if displayed_msgs_content: + + if invalid_commits: raise InvalidCommitMessageError( - "commit validation: failed!\n" - "please enter a commit message in the commitizen format.\n" - f"{displayed_msgs_content}\n" - f"pattern: {pattern}" + self.cz.format_exception_message(invalid_commits) ) out.success("Commit validation: successful!") - def _get_commits(self): - msg = None + def _get_commit_message(self) -> str | None: + if self.commit_msg_file is None: + # Get commit message from command line (--message) + return self.commit_msg + # 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)] + return Path(self.commit_msg_file).read_text( + encoding=self.config.settings["encoding"] + ) + + def _get_commits(self) -> list[git.GitCommit]: + if (msg := self._get_commit_message()) is not None: + return [git.GitCommit(rev="", title="", body=self._filter_comments(msg))] # Get commit messages from git log (--rev-range) - return git.get_commits(end=self.rev_range) + return git.get_commits( + git.get_default_branch() if self.use_default_range else None, + self.rev_range, + ) @staticmethod def _filter_comments(msg: str) -> str: @@ -131,18 +155,10 @@ def _filter_comments(msg: str) -> str: The filtered commit message without comments. """ - lines = [] + lines: list[str] = [] 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 - return bool(re.match(pattern, commit_msg)) diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index 0816b2e508..6668c0d65b 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -2,11 +2,15 @@ import contextlib import os +import shutil +import subprocess +import tempfile +from pathlib import Path +from typing import TYPE_CHECKING, TypedDict 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 ( @@ -22,122 +26,167 @@ ) from commitizen.git import smart_open +if TYPE_CHECKING: + from commitizen.config import BaseConfig + + +class CommitArgs(TypedDict, total=False): + all: bool + dry_run: bool + edit: bool + extra_cli_args: str + message_length_limit: int + no_retry: bool + signoff: bool + write_message_to_file: Path | None + retry: bool + class Commit: """Show prompt for the user to create a guided commit.""" - def __init__(self, config: BaseConfig, arguments: dict): + def __init__(self, config: BaseConfig, arguments: CommitArgs) -> None: 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.cz = factory.committer_factory(self.config) self.arguments = arguments - self.temp_file: str = get_backup_file_path() + self.backup_file_path = get_backup_file_path() - def read_backup_message(self) -> str | None: + def _read_backup_message(self) -> str | None: # Check the commit backup file exists - if not os.path.isfile(self.temp_file): + if not self.backup_file_path.is_file(): return None # Read commit message from backup - with open(self.temp_file, encoding=self.encoding) as f: - return f.read().strip() + return self.backup_file_path.read_text( + encoding=self.config.settings["encoding"] + ).strip() - def prompt_commit_questions(self) -> str: + def _get_message_by_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): + questions = self.cz.questions() + for question in (q for q in questions if q["type"] == "list"): question["use_shortcuts"] = self.config.settings["use_shortcuts"] try: - answers = questionary.prompt(questions, style=cz.style) + answers = questionary.prompt(questions, style=self.cz.style) except ValueError as err: root_err = err.__context__ if isinstance(root_err, CzException): - raise CustomError(root_err.__str__()) + raise CustomError(str(root_err)) 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: + message = self.cz.message(answers) + self._validate_subject_length(message) + return message + + def _validate_subject_length(self, message: str) -> None: + message_length_limit = self.arguments.get( + "message_length_limit", self.config.settings.get("message_length_limit", 0) + ) + # By the contract, message_length_limit is set to 0 for no limit + if ( + message_length_limit is None or message_length_limit <= 0 + ): # do nothing for no limit + return + + subject = message.partition("\n")[0].strip() + if len(subject) > message_length_limit: raise CommitMessageLengthExceededError( - f"Length of commit message exceeds limit ({message_len}/{message_length_limit})" + f"Length of commit message exceeds limit ({len(subject)}/{message_length_limit}), subject: '{subject}'" ) + 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) + message = Path(file_path).read_text().strip() + os.unlink(file.name) return message - def __call__(self): - dry_run: bool = self.arguments.get("dry_run") - write_message_to_file: bool = self.arguments.get("write_message_to_file") + def _get_message(self) -> str: + if self.arguments.get("retry"): + commit_message = self._read_backup_message() + if commit_message is None: + raise NoCommitBackupError() + return commit_message + + if ( + self.config.settings.get("retry_after_failure") + and not self.arguments.get("no_retry") + and (backup_message := self._read_backup_message()) + ): + return backup_message + return self._get_message_by_prompt_commit_questions() + + def __call__(self) -> None: + extra_args = self.arguments.get("extra_cli_args", "") + dry_run = bool(self.arguments.get("dry_run")) + write_message_to_file = self.arguments.get("write_message_to_file") + signoff = bool(self.arguments.get("signoff")) + + if signoff: + out.warn( + "Deprecated warning: `cz commit -s` is deprecated and will be removed in v5, please use `cz commit -- -s` instead." + ) - is_all: bool = self.arguments.get("all") - if is_all: - c = git.add("-u") + if self.arguments.get("all"): + git.add("-u") - if git.is_staging_clean() and not dry_run: + if git.is_staging_clean() and not (dry_run or "--allow-empty" in extra_args): 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() + commit_message = self._get_message() + if self.arguments.get("edit"): + commit_message = self.manual_edit(commit_message) - out.info(f"\n{m}\n") + out.info(f"\n{commit_message}\n") if write_message_to_file: - with smart_open(write_message_to_file, "w", encoding=self.encoding) as file: - file.write(m) + with smart_open( + write_message_to_file, "w", encoding=self.config.settings["encoding"] + ) as file: + file.write(commit_message) if dry_run: raise DryRunExit() - signoff: bool = ( - self.arguments.get("signoff") or self.config.settings["always_signoff"] - ) - - if signoff: - out.warn( - "signoff mechanic is deprecated, please use `cz commit -- -s` instead." - ) - extra_args = self.arguments.get("extra_cli_args", "--") + " -s" - else: - extra_args = self.arguments.get("extra_cli_args", "") - - c = git.commit(m, args=extra_args) + if self.config.settings["always_signoff"] or signoff: + extra_args = f"{extra_args} -s".strip() + c = git.commit(commit_message, 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) + with smart_open( + self.backup_file_path, "w", encoding=self.config.settings["encoding"] + ) as f: + f.write(commit_message) raise CommitError() - if "nothing added" in c.out or "no changes added to commit" in c.out: + if any(s in c.out for s in ("nothing added", "no changes added to commit")): 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!") + return + + with contextlib.suppress(FileNotFoundError): + self.backup_file_path.unlink() + out.write(c.err) + out.write(c.out) + out.success("Commit successful!") diff --git a/commitizen/commands/example.py b/commitizen/commands/example.py index e7abe7b318..ba9f34adc4 100644 --- a/commitizen/commands/example.py +++ b/commitizen/commands/example.py @@ -5,9 +5,9 @@ class Example: """Show an example so people understands the rules.""" - def __init__(self, config: BaseConfig, *args): + def __init__(self, config: BaseConfig, *args: object) -> None: self.config: BaseConfig = config - self.cz = factory.commiter_factory(self.config) + self.cz = factory.committer_factory(self.config) - def __call__(self): + def __call__(self) -> None: out.write(self.cz.example()) diff --git a/commitizen/commands/info.py b/commitizen/commands/info.py index afac9797e4..5ea8227313 100644 --- a/commitizen/commands/info.py +++ b/commitizen/commands/info.py @@ -5,9 +5,9 @@ class Info: """Show in depth explanation of your rules.""" - def __init__(self, config: BaseConfig, *args): + def __init__(self, config: BaseConfig, *args: object) -> None: self.config: BaseConfig = config - self.cz = factory.commiter_factory(self.config) + self.cz = factory.committer_factory(self.config) - def __call__(self): + def __call__(self) -> None: out.write(self.cz.info()) diff --git a/commitizen/commands/init.py b/commitizen/commands/init.py index ffc5e3eb3b..d0f4b686ac 100644 --- a/commitizen/commands/init.py +++ b/commitizen/commands/init.py @@ -1,91 +1,94 @@ from __future__ import annotations -import os -import shutil -from typing import Any +from pathlib import Path +from typing import TYPE_CHECKING, Any, NamedTuple import questionary import yaml -from commitizen import cmd, factory, out +from commitizen import cmd, factory, out, project_info from commitizen.__version__ import __version__ -from commitizen.config import BaseConfig, JsonConfig, TomlConfig, YAMLConfig +from commitizen.config.factory import create_config from commitizen.cz import registry -from commitizen.defaults import DEFAULT_SETTINGS, config_files -from commitizen.exceptions import InitFailedError, NoAnswersError +from commitizen.defaults import CONFIG_FILES, DEFAULT_SETTINGS +from commitizen.exceptions import ( + InitFailedError, + MissingCzCustomizeConfigError, + 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 +if TYPE_CHECKING: + from commitizen.config import ( + BaseConfig, + ) -class ProjectInfo: - """Discover information about the current folder.""" - @property - def has_pyproject(self) -> bool: - return os.path.isfile("pyproject.toml") - - @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_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() +class _VersionProviderOption(NamedTuple): + provider_name: str + description: str @property - def is_pre_commit_installed(self) -> bool: - return bool(shutil.which("pre-commit")) + def title(self) -> str: + return f"{self.provider_name}: {self.description}" + + +_VERSION_PROVIDER_CHOICES = tuple( + questionary.Choice(title=option.title, value=option.provider_name) + for option in ( + _VersionProviderOption( + provider_name="commitizen", + description="Fetch and set version in commitizen config (default)", + ), + _VersionProviderOption( + provider_name="cargo", + description="Get and set version from Cargo.toml:project.version field", + ), + _VersionProviderOption( + provider_name="composer", + description="Get and set version from composer.json:project.version field", + ), + _VersionProviderOption( + provider_name="npm", + description="Get and set version from package.json:project.version field", + ), + _VersionProviderOption( + provider_name="pep621", + description="Get and set version from pyproject.toml:project.version field", + ), + _VersionProviderOption( + provider_name="poetry", + description="Get and set version from pyproject.toml:tool.poetry.version field", + ), + _VersionProviderOption( + provider_name="uv", + description="Get and set version from pyproject.toml and uv.lock", + ), + _VersionProviderOption( + provider_name="scm", + description="Fetch the version from git and does not need to set it back", + ), + ) +) class Init: - def __init__(self, config: BaseConfig, *args): + _PRE_COMMIT_CONFIG_PATH = ".pre-commit-config.yaml" + + def __init__(self, config: BaseConfig, *args: object) -> None: self.config: BaseConfig = config - self.encoding = config.settings["encoding"] - self.cz = factory.commiter_factory(self.config) - self.project_info = ProjectInfo() + self.cz = factory.committer_factory(self.config) - def __call__(self): + def __call__(self) -> None: 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" + "Answer the following questions to configure your project.\n" + "For further configuration, visit:\n" "\n" "https://commitizen-tools.github.io/commitizen/config/" "\n" @@ -98,176 +101,167 @@ def __call__(self): 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, version_scheme)(tag) + 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 + hook_types: list[str] | None = 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() 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}") + config_data = self._get_config_data() + with smart_open( + self._PRE_COMMIT_CONFIG_PATH, + "w", + encoding=self.config.settings["encoding"], + ) as config_file: + yaml.safe_dump(config_data, stream=config_file) + + if not project_info.is_pre_commit_installed(): + raise InitFailedError( + "Failed to install pre-commit hook.\n" + "pre-commit is not installed in current environment." + ) - # Create and initialize config - self.config.init_empty_config_content() - self._update_config_file(values_to_add) + cmd_str = "pre-commit install " + " ".join( + f"--hook-type {ty}" for ty in hook_types + ) + c = cmd.run(cmd_str) + if c.return_code != 0: + raise InitFailedError( + "Failed to install pre-commit hook.\n" + f"Error running {cmd_str}." + "Outputs are attached below:\n" + f"stdout: {c.out}\n" + f"stderr: {c.err}" + ) + out.write("commitizen pre-commit hook is now installed in your '.git'\n") + + _write_config_to_file( + path=config_path, + cz_name=cz_name, + version_provider=version_provider, + version_scheme=version_scheme, + version=version, + tag_format=tag_format, + update_changelog_on_bump=update_changelog_on_bump, + major_version_zero=major_version_zero, + ) 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( + def _ask_config_path(self) -> Path: + filename: str = questionary.select( "Please choose a supported config file: ", - choices=config_files, - default=default_path, + choices=CONFIG_FILES, + default=project_info.get_default_config_filename(), style=self.cz.style, ).unsafe_ask() - return name + return Path(filename) 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", + f"Please choose a cz (commit rule): (default: {DEFAULT_SETTINGS['name']})", + choices=self._construct_name_choices_from_registry(), + default=DEFAULT_SETTINGS["name"], style=self.cz.style, ).unsafe_ask() return name + def _construct_name_choices_from_registry(self) -> list[questionary.Choice]: + """ + Construct questionary choices of cz names from registry. + """ + choices = [] + for cz_name, cz_class in registry.items(): + try: + cz_obj = cz_class(self.config) + except MissingCzCustomizeConfigError: + # Fallback if description is not available + choices.append(questionary.Choice(title=cz_name, value=cz_name)) + continue + + # Get the first line of the schema as the description + # TODO(bearomorphism): schema is a workaround. Add a description method to the cz class. + description = cz_obj.schema().partition("\n")[0] + choices.append( + questionary.Choice( + title=cz_name, value=cz_name, description=description + ) + ) + return choices + def _ask_tag(self) -> str: - latest_tag = self.project_info.latest_tag + latest_tag = get_latest_tag_name() if not latest_tag: out.error("No Existing Tag. Set tag to v0.0.1") return "0.0.1" - is_correct_tag = questionary.confirm( + if questionary.confirm( f"Is {latest_tag} the latest tag?", style=self.cz.style, default=False + ).unsafe_ask(): + return latest_tag + + existing_tags = get_tag_names() + if not existing_tags: + out.error("No Existing Tag. Set tag to v0.0.1") + return "0.0.1" + + answer: str = questionary.select( + "Please choose the latest tag: ", + # The latest tag is most likely with the largest number. + # Thus, listing the existing_tags in reverse order makes more sense. + choices=sorted(existing_tags, reverse=True), + style=self.cz.style, ).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 + if not answer: + raise NoAnswersError("Tag is required!") + return answer - def _ask_tag_format(self, latest_tag) -> str: - is_correct_format = False + def _ask_tag_format(self, latest_tag: str) -> str: 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() + v_tag_format = r"v$version" + if questionary.confirm( + f'Is "{v_tag_format}" the correct tag format?', style=self.cz.style + ).unsafe_ask(): + return v_tag_format 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() + tag_format: str = 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 + return tag_format or default_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", - "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" - 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, + choices=_VERSION_PROVIDER_CHOICES, style=self.cz.style, - default=default, + default=project_info.get_default_version_provider(), ).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), + choices=KNOWN_SCHEMES, style=self.cz.style, - default=default, + default=project_info.get_default_version_scheme(), ).unsafe_ask() return scheme @@ -283,7 +277,7 @@ def _ask_major_version_zero(self, version: Version) -> bool: return major_version_zero def _ask_update_changelog_on_bump(self) -> bool: - "Ask for setting: update_changelog_on_bump" + """Ask for setting: update_changelog_on_bump""" update_changelog_on_bump: bool = questionary.confirm( "Create changelog automatically on bump", default=True, @@ -291,73 +285,60 @@ def _ask_update_changelog_on_bump(self) -> bool: ).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 = { + def _get_config_data(self) -> dict[str, Any]: + CZ_HOOK_CONFIG = { "repo": "https://github.com/commitizen-tools/commitizen", "rev": f"v{__version__}", "hooks": [ {"id": "commitizen"}, - {"id": "commitizen-branch", "stages": ["push"]}, + {"id": "commitizen-branch", "stages": ["pre-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 + pre_commit_config_path = Path(self._PRE_COMMIT_CONFIG_PATH) + if not pre_commit_config_path.is_file(): + return {"repos": [CZ_HOOK_CONFIG]} + + with pre_commit_config_path.open( + encoding=self.config.settings["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) + config_data: dict[str, Any] = yaml.safe_load(config_file) or {} + + if not isinstance(repos := config_data.get("repos"), list): + # .pre-commit-config.yaml exists but there's no "repos" key + config_data["repos"] = [CZ_HOOK_CONFIG] + return config_data + + # Check if commitizen pre-commit hook is already in the config + if any("commitizen" in hook_config["repo"] for hook_config in repos): + out.write("commitizen already in pre-commit config") + else: + repos.append(CZ_HOOK_CONFIG) + return config_data + + +def _write_config_to_file( + *, + path: Path, + cz_name: str, + version_provider: str, + version_scheme: str, + version: Version, + tag_format: str, + update_changelog_on_bump: bool, + major_version_zero: bool, +) -> None: + out_config = create_config(path=path) + out_config.init_empty_config_content() + + out_config.set_key("name", cz_name) + out_config.set_key("tag_format", tag_format) + out_config.set_key("version_scheme", version_scheme) + if version_provider == "commitizen": + out_config.set_key("version", version.public) + else: + out_config.set_key("version_provider", version_provider) + if update_changelog_on_bump: + out_config.set_key("update_changelog_on_bump", update_changelog_on_bump) + if major_version_zero: + out_config.set_key("major_version_zero", major_version_zero) diff --git a/commitizen/commands/list_cz.py b/commitizen/commands/list_cz.py index 99701865af..412266f6c3 100644 --- a/commitizen/commands/list_cz.py +++ b/commitizen/commands/list_cz.py @@ -6,8 +6,8 @@ class ListCz: """List currently installed rules.""" - def __init__(self, config: BaseConfig, *args): + def __init__(self, config: BaseConfig, *args: object) -> None: self.config: BaseConfig = config - def __call__(self): + def __call__(self) -> None: out.write("\n".join(registry.keys())) diff --git a/commitizen/commands/schema.py b/commitizen/commands/schema.py index 0940648cde..a7aeb53569 100644 --- a/commitizen/commands/schema.py +++ b/commitizen/commands/schema.py @@ -5,9 +5,9 @@ class Schema: """Show structure of the rule.""" - def __init__(self, config: BaseConfig, *args): + def __init__(self, config: BaseConfig, *args: object) -> None: self.config: BaseConfig = config - self.cz = factory.commiter_factory(self.config) + self.cz = factory.committer_factory(self.config) - def __call__(self): + def __call__(self) -> None: out.write(self.cz.schema()) diff --git a/commitizen/commands/version.py b/commitizen/commands/version.py index 45d553c710..c8a76fe27f 100644 --- a/commitizen/commands/version.py +++ b/commitizen/commands/version.py @@ -1,39 +1,86 @@ import platform import sys +from typing import TypedDict from commitizen import out from commitizen.__version__ import __version__ from commitizen.config import BaseConfig +from commitizen.exceptions import NoVersionSpecifiedError, VersionSchemeUnknown from commitizen.providers import get_provider +from commitizen.tags import TagRules +from commitizen.version_schemes import get_version_scheme + + +class VersionArgs(TypedDict, total=False): + commitizen: bool + report: bool + project: bool + verbose: bool + major: bool + minor: bool + tag: bool class Version: - """Get the version of the installed commitizen or the current project.""" + """Get the version of the installed commitizen or the current project. + Precedence: + 1. report + 2. commitizen + 3. verbose, project + """ - def __init__(self, config: BaseConfig, *args): + def __init__(self, config: BaseConfig, arguments: VersionArgs) -> None: self.config: BaseConfig = config - self.parameter = args[0] - self.operating_system = platform.system() - self.python_version = sys.version + self.arguments = arguments - def __call__(self): - if self.parameter.get("report"): + def __call__(self) -> None: + if self.arguments.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"Python Version: {sys.version}") + out.write(f"Operating System: {platform.system()}") + return + + if self.arguments.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: + + if not self.arguments.get("commitizen") and ( + self.arguments.get("project") or self.arguments.get("verbose") + ): + try: + version = get_provider(self.config).get_version() + except NoVersionSpecifiedError: out.error("No project information in this project.") - else: - # if no argument is given, show installed commitizen version - out.write(f"{__version__}") + return + try: + version_scheme = get_version_scheme(self.config.settings)(version) + except VersionSchemeUnknown: + out.error("Unknown version scheme.") + return + + if self.arguments.get("major"): + version = f"{version_scheme.major}" + elif self.arguments.get("minor"): + version = f"{version_scheme.minor}" + elif self.arguments.get("tag"): + tag_rules = TagRules.from_settings(self.config.settings) + version = tag_rules.normalize_tag(version_scheme) + + out.write( + f"Project Version: {version}" + if self.arguments.get("verbose") + else version + ) + return + + if self.arguments.get("major") or self.arguments.get("minor"): + out.error( + "Major or minor version can only be used with --project or --verbose." + ) + return + + if self.arguments.get("tag"): + out.error("Tag can only be used with --project or --verbose.") + return + + # If no arguments are provided, just show the installed commitizen version + out.write(__version__) diff --git a/commitizen/config/__init__.py b/commitizen/config/__init__.py index f3720bb1b3..08dc7ffd7e 100644 --- a/commitizen/config/__init__.py +++ b/commitizen/config/__init__.py @@ -2,57 +2,50 @@ from pathlib import Path -from commitizen import defaults, git +from commitizen import defaults, git, out +from commitizen.config.factory import create_config 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() +def _resolve_config_candidates() -> list[BaseConfig]: + git_project_root = git.find_git_project_root() + cfg_search_paths = [Path(".")] - 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 - ) + if git_project_root and cfg_search_paths[0].resolve() != git_project_root.resolve(): + cfg_search_paths.append(git_project_root) - for filename in cfg_paths: - if not filename.exists(): - continue + candidates: list[BaseConfig] = [] + for dir in cfg_search_paths: + for filename in defaults.CONFIG_FILES: + out_path = dir / filename + if out_path.is_file(): + conf = _create_config_from_path(out_path) + if conf.contains_commitizen_section(): + candidates.append(conf) + return candidates - _conf: TomlConfig | JsonConfig | YAMLConfig - with open(filename, "rb") as f: - data: bytes = f.read() +def _create_config_from_path(path: Path) -> BaseConfig: + return create_config(data=path.read_bytes(), path=path) - 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: +def read_cfg(filepath: str | None = None) -> BaseConfig: + if filepath is not None: + conf_path = Path(filepath) + if not conf_path.is_file(): + raise ConfigFileNotFound() + conf = _create_config_from_path(conf_path) + if not conf.contains_commitizen_section(): raise ConfigFileIsEmpty() - elif _conf.is_empty_config: - continue - else: - conf = _conf - break + return conf + + config_candidates = _resolve_config_candidates() + if len(config_candidates) > 1: + out.warn( + f"Multiple config files detected: {', '.join(str(conf.path) for conf in config_candidates)}. " + f"Using config file: '{config_candidates[0].path}'." + ) - return conf + return config_candidates[0] if config_candidates else BaseConfig() diff --git a/commitizen/config/base_config.py b/commitizen/config/base_config.py index 478691aa14..f100cf9953 100644 --- a/commitizen/config/base_config.py +++ b/commitizen/config/base_config.py @@ -1,37 +1,60 @@ from __future__ import annotations from pathlib import Path +from typing import TYPE_CHECKING from commitizen.defaults import DEFAULT_SETTINGS, Settings +if TYPE_CHECKING: + import sys + + # 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 BaseConfig: - def __init__(self): + def __init__(self) -> None: self._settings: Settings = DEFAULT_SETTINGS.copy() - self.encoding = self.settings["encoding"] self._path: Path | None = None + def contains_commitizen_section(self) -> bool: + """Check if the config file contains a commitizen section. + + The implementation is different for each config file type. + """ + raise NotImplementedError() + @property def settings(self) -> Settings: return self._settings @property - def path(self) -> Path | None: - return self._path + def path(self) -> Path: + return self._path # type: ignore[return-value] + + @path.setter + def path(self, path: Path) -> None: + self._path = Path(path) - def set_key(self, key, value): - """Set or update a key in the conf. + def set_key(self, key: str, value: object) -> Self: + """Set or update a key in the config file. - For now only strings are supported. - We use to update the version number. + Currently, only strings are supported for the parameter key. """ 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() + + def init_empty_config_content(self) -> None: + """Create a config file with the empty config content. + + The implementation is different for each config file type. + """ + raise NotImplementedError() diff --git a/commitizen/config/factory.py b/commitizen/config/factory.py new file mode 100644 index 0000000000..d0a212b73f --- /dev/null +++ b/commitizen/config/factory.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from commitizen.config.json_config import JsonConfig +from commitizen.config.toml_config import TomlConfig +from commitizen.config.yaml_config import YAMLConfig + +if TYPE_CHECKING: + from pathlib import Path + + from commitizen.config.base_config import BaseConfig + + +def create_config(*, data: bytes | str | None = None, path: Path) -> BaseConfig: + if "toml" in path.suffix: + return TomlConfig(data=data or "", path=path) + if "json" in path.suffix: + return JsonConfig(data=data or "{}", path=path) + if "yaml" in path.suffix: + return YAMLConfig(data=data or "", path=path) + + # Should be unreachable. See the constant CONFIG_FILES. + raise ValueError( + f"Unsupported config file: {path.name} due to unknown file extension" + ) diff --git a/commitizen/config/json_config.py b/commitizen/config/json_config.py index b6a07f4ced..688a6b9fec 100644 --- a/commitizen/config/json_config.py +++ b/commitizen/config/json_config.py @@ -1,37 +1,51 @@ from __future__ import annotations import json -from pathlib import Path +from typing import TYPE_CHECKING from commitizen.exceptions import InvalidConfigurationError from commitizen.git import smart_open from .base_config import BaseConfig +if TYPE_CHECKING: + import sys + from pathlib import Path + + # 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 JsonConfig(BaseConfig): - def __init__(self, *, data: bytes | str, path: Path | str): + def __init__(self, *, data: bytes | str, path: Path) -> None: super().__init__() - self.is_empty_config = False - self.add_path(path) + self.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 contains_commitizen_section(self) -> bool: + with self.path.open("rb") as json_file: + try: + config_doc = json.load(json_file) + except json.JSONDecodeError: + return False + return config_doc.get("commitizen") is not None - def set_key(self, key, value): - """Set or update a key in the conf. + def init_empty_config_content(self) -> None: + with smart_open( + self.path, "a", encoding=self._settings["encoding"] + ) as json_file: + json.dump({"commitizen": {}}, json_file) - For now only strings are supported. - We use to update the version number. - """ - with open(self.path, "rb") as f: - parser = json.load(f) + def set_key(self, key: str, value: object) -> Self: + with self.path.open("rb") as f: + config_doc = json.load(f) - parser["commitizen"][key] = value - with smart_open(self.path, "w", encoding=self.encoding) as f: - json.dump(parser, f, indent=2) + config_doc["commitizen"][key] = value + with smart_open(self.path, "w", encoding=self._settings["encoding"]) as f: + json.dump(config_doc, f, indent=2) return self def _parse_setting(self, data: bytes | str) -> None: @@ -53,4 +67,4 @@ def _parse_setting(self, data: bytes | str) -> None: try: self.settings.update(doc["commitizen"]) except KeyError: - self.is_empty_config = True + pass diff --git a/commitizen/config/toml_config.py b/commitizen/config/toml_config.py index 813389cbcf..28c05aaa52 100644 --- a/commitizen/config/toml_config.py +++ b/commitizen/config/toml_config.py @@ -1,47 +1,54 @@ from __future__ import annotations -import os -from pathlib import Path +from typing import TYPE_CHECKING -from tomlkit import exceptions, parse, table +from tomlkit import TOMLDocument, exceptions, parse, table from commitizen.exceptions import InvalidConfigurationError from .base_config import BaseConfig +if TYPE_CHECKING: + import sys + from pathlib import Path + + # 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 TomlConfig(BaseConfig): - def __init__(self, *, data: bytes | str, path: Path | str): + def __init__(self, *, data: bytes | str, path: Path) -> None: super().__init__() - self.is_empty_config = False - self.add_path(path) + self.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("") + def contains_commitizen_section(self) -> bool: + config_doc = parse(self.path.read_bytes()) + return config_doc.get("tool", {}).get("commitizen") is not None - 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 init_empty_config_content(self) -> None: + config_doc = TOMLDocument() + if self.path.is_file(): + config_doc = parse(self.path.read_bytes()) - def set_key(self, key, value): - """Set or update a key in the conf. + if config_doc.get("tool") is None: + config_doc["tool"] = table() + config_doc["tool"]["commitizen"] = table() # type: ignore[index] - For now only strings are supported. - We use to update the version number. - """ - with open(self.path, "rb") as f: - parser = parse(f.read()) + with self.path.open("wb") as output_toml_file: + output_toml_file.write( + config_doc.as_string().encode(self._settings["encoding"]) + ) + + def set_key(self, key: str, value: object) -> Self: + config_doc = parse(self.path.read_bytes()) + + config_doc["tool"]["commitizen"][key] = value # type: ignore[index] + self.path.write_bytes(config_doc.as_string().encode(self._settings["encoding"])) - 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: @@ -58,6 +65,6 @@ def _parse_setting(self, data: bytes | str) -> None: raise InvalidConfigurationError(f"Failed to parse {self.path}: {e}") try: - self.settings.update(doc["tool"]["commitizen"]) # type: ignore + self.settings.update(doc["tool"]["commitizen"]) # type: ignore[index,typeddict-item] # TODO: fix this except exceptions.NonExistentKey: - self.is_empty_config = True + pass diff --git a/commitizen/config/yaml_config.py b/commitizen/config/yaml_config.py index 2bb6fe3af8..1e9610e17a 100644 --- a/commitizen/config/yaml_config.py +++ b/commitizen/config/yaml_config.py @@ -1,6 +1,6 @@ from __future__ import annotations -from pathlib import Path +from typing import TYPE_CHECKING import yaml @@ -9,18 +9,34 @@ from .base_config import BaseConfig +if TYPE_CHECKING: + import sys + from pathlib import Path + + # 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 YAMLConfig(BaseConfig): - def __init__(self, *, data: bytes | str, path: Path | str): + def __init__(self, *, data: bytes | str, path: Path) -> None: super().__init__() - self.is_empty_config = False - self.add_path(path) + self.path = path self._parse_setting(data) - def init_empty_config_content(self): - with smart_open(self.path, "a", encoding=self.encoding) as json_file: + def init_empty_config_content(self) -> None: + with smart_open( + self.path, "a", encoding=self._settings["encoding"] + ) as json_file: yaml.dump({"commitizen": {}}, json_file, explicit_start=True) + def contains_commitizen_section(self) -> bool: + with self.path.open("rb") as yaml_file: + config_doc = yaml.load(yaml_file, Loader=yaml.FullLoader) + return config_doc is not None and config_doc.get("commitizen") is not None + def _parse_setting(self, data: bytes | str) -> None: """We expect to have a section in cz.yaml looking like @@ -29,8 +45,6 @@ def _parse_setting(self, data: bytes | str) -> None: name: cz_conventional_commits ``` """ - import yaml.scanner - try: doc = yaml.safe_load(data) except yaml.YAMLError as e: @@ -39,19 +53,16 @@ def _parse_setting(self, data: bytes | str) -> None: 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. + pass - 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) + def set_key(self, key: str, value: object) -> Self: + with self.path.open("rb") as yaml_file: + config_doc = 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) + config_doc["commitizen"][key] = value + with smart_open( + self.path, "w", encoding=self._settings["encoding"] + ) as yaml_file: + yaml.dump(config_doc, yaml_file, explicit_start=True) return self diff --git a/commitizen/cz/__init__.py b/commitizen/cz/__init__.py index 04603a9ec4..6b7c1c887a 100644 --- a/commitizen/cz/__init__.py +++ b/commitizen/cz/__init__.py @@ -2,16 +2,15 @@ import importlib import pkgutil -import sys import warnings -from typing import Iterable +from collections.abc import Iterable +from importlib import metadata +from typing import TYPE_CHECKING -if sys.version_info >= (3, 10): - from importlib import metadata -else: - import importlib_metadata as metadata +if TYPE_CHECKING: + from collections.abc import Iterable -from commitizen.cz.base import BaseCommitizen + from commitizen.cz.base import BaseCommitizen def discover_plugins( diff --git a/commitizen/cz/base.py b/commitizen/cz/base.py index bd116ceb02..5e7f2663ca 100644 --- a/commitizen/cz/base.py +++ b/commitizen/cz/base.py @@ -1,14 +1,21 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod -from typing import Any, Callable, Iterable, Protocol +from collections.abc import Iterable, Mapping +from typing import TYPE_CHECKING, Any, NamedTuple, Protocol from jinja2 import BaseLoader, PackageLoader -from prompt_toolkit.styles import Style, merge_styles +from prompt_toolkit.styles import Style -from commitizen import git -from commitizen.config.base_config import BaseConfig -from commitizen.defaults import Questions +from commitizen.exceptions import CommitMessageLengthExceededError + +if TYPE_CHECKING: + import re + from collections.abc import Callable, Iterable, Mapping + + from commitizen import git + from commitizen.config.base_config import BaseConfig + from commitizen.question import CzQuestion class MessageBuilderHook(Protocol): @@ -23,6 +30,11 @@ def __call__( ) -> dict[str, Any]: ... +class ValidationResult(NamedTuple): + is_valid: bool + errors: list + + class BaseCommitizen(metaclass=ABCMeta): bump_pattern: str | None = None bump_map: dict[str, str] | None = None @@ -40,7 +52,7 @@ class BaseCommitizen(metaclass=ABCMeta): ("disabled", "fg:#858585 italic"), ] - # The whole subject will be parsed as message by default + # The whole subject will be parsed as a message by default # This allows supporting changelog for any rule system. # It can be modified per rule commit_parser: str | None = r"(?P<message>.*)" @@ -61,47 +73,92 @@ class BaseCommitizen(metaclass=ABCMeta): template_loader: BaseLoader = PackageLoader("commitizen", "templates") template_extras: dict[str, Any] = {} - def __init__(self, config: BaseConfig): + 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: + def questions(self) -> list[CzQuestion]: """Questions regarding the commit message.""" @abstractmethod - def message(self, answers: dict) -> str: + def message(self, answers: Mapping[str, Any]) -> str: """Format your git message.""" @property - def style(self): - return merge_styles( + def style(self) -> Style: + return Style( [ - Style(BaseCommitizen.default_style_config), - Style(self.config.settings["style"]), + *BaseCommitizen.default_style_config, + *self.config.settings["style"], ] ) - def example(self) -> str | None: + @abstractmethod + def example(self) -> str: """Example of the commit message.""" - raise NotImplementedError("Not Implemented yet") - def schema(self) -> str | None: + @abstractmethod + def schema(self) -> str: """Schema definition of the commit message.""" - raise NotImplementedError("Not Implemented yet") - def schema_pattern(self) -> str | None: + @abstractmethod + def schema_pattern(self) -> str: """Regex matching the schema used for message validation.""" - raise NotImplementedError("Not Implemented yet") - def info(self) -> str | None: + @abstractmethod + 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. + def validate_commit_message( + self, + *, + commit_msg: str, + pattern: re.Pattern[str], + allow_abort: bool, + allowed_prefixes: list[str], + max_msg_length: int | None, + commit_hash: str, + ) -> ValidationResult: + """Validate commit message against the pattern.""" + if not commit_msg: + return ValidationResult( + allow_abort, [] if allow_abort else ["commit message is empty"] + ) + + if any(map(commit_msg.startswith, allowed_prefixes)): + return ValidationResult(True, []) + + if max_msg_length is not None and max_msg_length > 0: + msg_len = len(commit_msg.partition("\n")[0].strip()) + if msg_len > max_msg_length: + # TODO: capitalize the first letter of the error message for consistency in v5 + raise CommitMessageLengthExceededError( + f"commit validation: failed!\n" + f"commit message length exceeds the limit.\n" + f'commit "{commit_hash}": "{commit_msg}"\n' + f"message length limit: {max_msg_length} (actual: {msg_len})" + ) + + return ValidationResult( + bool(pattern.match(commit_msg)), + [f"pattern: {pattern.pattern}"], + ) - If not overwritten, it returns the first line of commit. - """ - return commit.split("\n")[0] + def format_exception_message( + self, invalid_commits: list[tuple[git.GitCommit, list]] + ) -> str: + """Format commit errors.""" + displayed_msgs_content = "\n".join( + [ + f'commit "{commit.rev}": "{commit.message}\n"' + "\n".join(errors) + for commit, errors in invalid_commits + ] + ) + # TODO: capitalize the first letter of the error message for consistency in v5 + return ( + "commit validation: failed!\n" + "please enter a commit message in the commitizen format.\n" + f"{displayed_msgs_content}" + ) diff --git a/commitizen/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py index dcfd7bab89..31c329595a 100644 --- a/commitizen/cz/conventional_commits/conventional_commits.py +++ b/commitizen/cz/conventional_commits/conventional_commits.py @@ -1,47 +1,50 @@ -import os -import re +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, TypedDict 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"] +if TYPE_CHECKING: + from commitizen.question import CzQuestion +__all__ = ["ConventionalCommitsCz"] -def parse_scope(text): - if not text: - return "" - scope = text.strip().split() - if len(scope) == 1: - return scope[0] +def _parse_scope(text: str) -> str: + return "-".join(text.strip().split()) - return "-".join(scope) +def _parse_subject(text: str) -> str: + return required_validator(text.strip(".").strip(), msg="Subject is required.") -def parse_subject(text): - if isinstance(text, str): - text = text.strip(".").strip() - return required_validator(text, msg="Subject is required.") +class ConventionalCommitsAnswers(TypedDict): + prefix: str + scope: str + subject: str + body: str + footer: str + is_breaking_change: bool 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 + 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>.*)?" change_type_map = { "feat": "Feat", "fix": "Fix", "refactor": "Refactor", "perf": "Perf", } - changelog_pattern = defaults.bump_pattern + changelog_pattern = defaults.BUMP_PATTERN - def questions(self) -> Questions: - questions: Questions = [ + def questions(self) -> list[CzQuestion]: + return [ { "type": "list", "name": "prefix", @@ -86,9 +89,7 @@ def questions(self) -> Questions: }, { "value": "test", - "name": ( - "test: Adding missing or correcting " "existing tests" - ), + "name": ("test: Adding missing or correcting existing tests"), "key": "t", }, { @@ -115,12 +116,12 @@ def questions(self) -> Questions: "message": ( "What is the scope of this change? (class or file name): (press [enter] to skip)\n" ), - "filter": parse_scope, + "filter": _parse_scope, }, { "type": "input", "name": "subject", - "filter": parse_subject, + "filter": _parse_subject, "message": ( "Write a short and imperative summary of the code changes: (lower case and no period)\n" ), @@ -135,8 +136,8 @@ def questions(self) -> Questions: }, { "type": "confirm", - "message": "Is this a BREAKING CHANGE? Correlates with MAJOR in SemVer", "name": "is_breaking_change", + "message": "Is this a BREAKING CHANGE? Correlates with MAJOR in SemVer", "default": False, }, { @@ -148,9 +149,8 @@ def questions(self) -> Questions: ), }, ] - return questions - def message(self, answers: dict) -> str: + def message(self, answers: ConventionalCommitsAnswers) -> str: # type: ignore[override] prefix = answers["prefix"] scope = answers["scope"] subject = answers["subject"] @@ -158,18 +158,17 @@ def message(self, answers: dict) -> str: footer = answers["footer"] is_breaking_change = answers["is_breaking_change"] - if scope: - scope = f"({scope})" - if body: - body = f"\n\n{body}" + formatted_scope = f"({scope})" if scope else "" + title = f"{prefix}{formatted_scope}" if is_breaking_change: + if self.config.settings.get("breaking_change_exclamation_in_title", False): + title = f"{title}!" footer = f"BREAKING CHANGE: {footer}" - if footer: - footer = f"\n\n{footer}" - message = f"{prefix}{scope}: {subject}{body}{footer}" + formatted_body = f"\n\n{body}" if body else "" + formatted_footter = f"\n\n{footer}" if footer else "" - return message + return f"{title}: {subject}{formatted_body}{formatted_footter}" def example(self) -> str: return ( @@ -190,25 +189,30 @@ def schema(self) -> str: ) def schema_pattern(self) -> str: - PATTERN = ( + change_types = ( + "build", + "bump", + "chore", + "ci", + "docs", + "feat", + "fix", + "perf", + "refactor", + "revert", + "style", + "test", + ) + return ( 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"(" + "|".join(change_types) + r")" # type + r"(\(\S+\))?" # scope + r"!?" + r": " + 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() + filepath = Path(__file__).parent / "conventional_commits_info.txt" + return filepath.read_text(encoding=self.config.settings["encoding"]) diff --git a/commitizen/cz/customize/__init__.py b/commitizen/cz/customize/__init__.py index c5af001a79..0aedb9337a 100644 --- a/commitizen/cz/customize/__init__.py +++ b/commitizen/cz/customize/__init__.py @@ -1 +1 @@ -from .customize import CustomizeCommitsCz # noqa +from .customize import CustomizeCommitsCz # noqa: F401 diff --git a/commitizen/cz/customize/customize.py b/commitizen/cz/customize/customize.py index 5c3b4e76b4..8fcc63fac3 100644 --- a/commitizen/cz/customize/customize.py +++ b/commitizen/cz/customize/customize.py @@ -1,89 +1,73 @@ from __future__ import annotations -try: +from pathlib import Path +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Mapping + from jinja2 import Template -except ImportError: - from string import Template # type: ignore + + from commitizen.config import BaseConfig + from commitizen.question import CzQuestion +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 + 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): + def __init__(self, config: BaseConfig) -> None: 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 + for attr_name in [ + "bump_pattern", + "bump_map", + "bump_map_major_version_zero", + "change_type_order", + "commit_parser", + "changelog_pattern", + "change_type_map", + ]: + if value := self.custom_settings.get(attr_name): + setattr(self, attr_name, value) + + def questions(self) -> list[CzQuestion]: + return self.custom_settings.get("questions", [{}]) # type: ignore[return-value] + + def message(self, answers: Mapping[str, Any]) -> str: + message_template = Template(self.custom_settings.get("message_template", "")) + if getattr(Template, "substitute", None): + return message_template.substitute(**answers) # type: ignore[attr-defined,no-any-return] # pragma: no cover # TODO: check if we can fix this + return message_template.render(**answers) - changelog_pattern = self.custom_settings.get("changelog_pattern") - if changelog_pattern: - self.changelog_pattern = changelog_pattern + def example(self) -> str: + return self.custom_settings.get("example") or "" - change_type_map = self.custom_settings.get("change_type_map") - if change_type_map: - self.change_type_map = change_type_map + def schema_pattern(self) -> str: + return self.custom_settings.get("schema_pattern") or "" - def questions(self) -> Questions: - return self.custom_settings.get("questions", [{}]) + def schema(self) -> str: + return self.custom_settings.get("schema") or "" - 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 | None: - return self.custom_settings.get("example") - - def schema_pattern(self) -> str | None: - return self.custom_settings.get("schema_pattern") - - def schema(self) -> str | None: - return self.custom_settings.get("schema") - - def info(self) -> str | None: - 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 None + def info(self) -> str: + if info_path := self.custom_settings.get("info_path"): + return Path(info_path).read_text(encoding=self.config.settings["encoding"]) + return self.custom_settings.get("info") or "" diff --git a/commitizen/cz/jira/jira.py b/commitizen/cz/jira/jira.py index b8fd056a71..07189d15d3 100644 --- a/commitizen/cz/jira/jira.py +++ b/commitizen/cz/jira/jira.py @@ -1,14 +1,21 @@ -import os +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING from commitizen.cz.base import BaseCommitizen -from commitizen.defaults import Questions + +if TYPE_CHECKING: + from collections.abc import Mapping + + from commitizen.question import CzQuestion __all__ = ["JiraSmartCz"] class JiraSmartCz(BaseCommitizen): - def questions(self) -> Questions: - questions = [ + def questions(self) -> list[CzQuestion]: + return [ { "type": "input", "name": "message", @@ -42,20 +49,12 @@ def questions(self) -> Questions: "filter": lambda x: "#comment " + x if x else "", }, ] - return questions - def message(self, answers: dict) -> str: + def message(self, answers: Mapping[str, str]) -> str: return " ".join( - filter( - bool, - [ - answers["message"], - answers["issues"], - answers["workflow"], - answers["time"], - answers["comment"], - ], - ) + x + for k in ("message", "issues", "workflow", "time", "comment") + if (x := answers.get(k)) ) def example(self) -> str: @@ -68,14 +67,11 @@ def example(self) -> str: ) def schema(self) -> str: - return "<ignored text> <ISSUE_KEY> <ignored text> #<COMMAND> <optional COMMAND_ARGUMENTS>" # noqa + return "<ignored text> <ISSUE_KEY> <ignored text> #<COMMAND> <optional COMMAND_ARGUMENTS>" 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 + filepath = Path(__file__).parent / "jira_info.txt" + return filepath.read_text(encoding=self.config.settings["encoding"]) diff --git a/commitizen/cz/utils.py b/commitizen/cz/utils.py index 7bc89673c6..ba5eace44a 100644 --- a/commitizen/cz/utils.py +++ b/commitizen/cz/utils.py @@ -1,32 +1,31 @@ import os import re import tempfile +from pathlib import Path from commitizen import git from commitizen.cz import exceptions +_RE_LOCAL_VERSION = re.compile(r"\+.+") -def required_validator(answer, msg=None): + +def required_validator(answer: str, msg: object = None) -> str: if not answer: raise exceptions.AnswerRequiredError(msg) return answer -def multiple_line_breaker(answer, sep="|"): +def multiple_line_breaker(answer: str, sep: str = "|") -> str: 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) + return _RE_LOCAL_VERSION.sub("", version) -def get_backup_file_path() -> str: +def get_backup_file_path() -> Path: project_root = git.find_git_project_root() - - if project_root is None: - project = "" - else: - project = project_root.as_posix().replace("/", "%") + project = project_root.as_posix().replace("/", "%") if project_root else "" user = os.environ.get("USER", "") - return os.path.join(tempfile.gettempdir(), f"cz.commit%{user}%{project}.backup") + return Path(tempfile.gettempdir(), f"cz.commit%{user}%{project}.backup") diff --git a/commitizen/defaults.py b/commitizen/defaults.py index e4363f4ab0..4865ccc188 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -1,11 +1,14 @@ from __future__ import annotations -import pathlib +import warnings from collections import OrderedDict -from typing import Any, Iterable, MutableMapping, TypedDict +from collections.abc import Iterable, MutableMapping, Sequence +from typing import TYPE_CHECKING, Any, TypedDict -# Type -Questions = Iterable[MutableMapping[str, Any]] +if TYPE_CHECKING: + import pathlib + + from commitizen.question import CzQuestion class CzSettings(TypedDict, total=False): @@ -14,7 +17,7 @@ class CzSettings(TypedDict, total=False): bump_map_major_version_zero: OrderedDict[str, str] change_type_order: list[str] - questions: Questions + questions: Iterable[CzQuestion] example: str | None schema_pattern: str | None schema: str | None @@ -27,55 +30,63 @@ class CzSettings(TypedDict, total=False): 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 - bump_message: str | None - retry_after_failure: bool allow_abort: bool allowed_prefixes: list[str] + always_signoff: bool + annotated_tag: bool + bump_message: str | None + change_type_map: dict[str, 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]] | None + changelog_start_rev: str | None customize: CzSettings + encoding: str + extras: dict[str, Any] + gpg_sign: bool + ignored_tag_formats: Sequence[str] + legacy_tag_formats: Sequence[str] major_version_zero: bool - pre_bump_hooks: list[str] | None + message_length_limit: int + name: str post_bump_hooks: list[str] | None + pre_bump_hooks: list[str] | None prerelease_offset: int - encoding: str - always_signoff: bool + retry_after_failure: bool + style: list[tuple[str, str]] + tag_format: str template: str | None - extras: dict[str, Any] + update_changelog_on_bump: bool + use_shortcuts: bool + version_files: list[str] + version_provider: str | None + version_scheme: str | None + version_type: str | None + version: str | None + breaking_change_exclamation_in_title: bool -name: str = "cz_conventional_commits" -config_files: list[str] = [ - "pyproject.toml", +CONFIG_FILES: tuple[str, ...] = ( ".cz.toml", + "cz.toml", ".cz.json", "cz.json", ".cz.yaml", "cz.yaml", - "cz.toml", -] -encoding: str = "utf-8" + "pyproject.toml", +) +ENCODING = "utf-8" DEFAULT_SETTINGS: Settings = { - "name": name, + "name": "cz_conventional_commits", "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, @@ -85,6 +96,7 @@ class Settings(TypedDict, total=False): "Pull request", "fixup!", "squash!", + "amend!", ], "changelog_file": "CHANGELOG.md", "changelog_format": None, # default guessed from changelog_file @@ -97,10 +109,12 @@ class Settings(TypedDict, total=False): "pre_bump_hooks": [], "post_bump_hooks": [], "prerelease_offset": 0, - "encoding": encoding, + "encoding": ENCODING, "always_signoff": False, "template": None, # default provided by plugin "extras": {}, + "breaking_change_exclamation_in_title": False, + "message_length_limit": 0, # 0 for no limit } MAJOR = "MAJOR" @@ -109,8 +123,8 @@ class Settings(TypedDict, total=False): CHANGELOG_FORMAT = "markdown" -bump_pattern = r"^((BREAKING[\-\ ]CHANGE|\w+)(\(.+\))?!?):" -bump_map = OrderedDict( +BUMP_PATTERN = r"^((BREAKING[\-\ ]CHANGE|\w+)(\(.+\))?!?):" +BUMP_MAP = OrderedDict( ( (r"^.+!$", MAJOR), (r"^BREAKING[\-\ ]CHANGE", MAJOR), @@ -120,7 +134,7 @@ class Settings(TypedDict, total=False): (r"^perf", PATCH), ) ) -bump_map_major_version_zero = OrderedDict( +BUMP_MAP_MAJOR_VERSION_ZERO = OrderedDict( ( (r"^.+!$", MINOR), (r"^BREAKING[\-\ ]CHANGE", MINOR), @@ -130,5 +144,55 @@ class Settings(TypedDict, total=False): (r"^perf", PATCH), ) ) -change_type_order = ["BREAKING CHANGE", "Feat", "Fix", "Refactor", "Perf"] -bump_message = "bump: version $current_version → $new_version" +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]: + regexes = { + "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 regexes.items()}, + **{f"${{{k}}}": v for k, v in regexes.items()}, + } + + +# Type +Questions = Iterable[MutableMapping[str, Any]] # TODO: remove this in v5 + + +def __getattr__(name: str) -> Any: + # PEP-562: deprecate module-level variable + + # {"deprecated key": (value, "new key")} + deprecated_vars = { + "bump_pattern": (BUMP_PATTERN, "BUMP_PATTERN"), + "bump_map": (BUMP_MAP, "BUMP_MAP"), + "bump_map_major_version_zero": ( + BUMP_MAP_MAJOR_VERSION_ZERO, + "BUMP_MAP_MAJOR_VERSION_ZERO", + ), + "bump_message": (BUMP_MESSAGE, "BUMP_MESSAGE"), + "change_type_order": (CHANGE_TYPE_ORDER, "CHANGE_TYPE_ORDER"), + "encoding": (ENCODING, "ENCODING"), + "name": (DEFAULT_SETTINGS["name"], "DEFAULT_SETTINGS['name']"), + "Questions": (Questions, "Iterable[CzQuestion]"), + } + if name in deprecated_vars: + value, replacement = deprecated_vars[name] + warnings.warn( + f"{name} is deprecated and will be removed in v5. " + f"Use {replacement} instead.", + DeprecationWarning, + stacklevel=2, + ) + return value + raise AttributeError(f"{name} is not an attribute of {__name__}") diff --git a/commitizen/exceptions.py b/commitizen/exceptions.py index b0fc4e382d..52193e6ccc 100644 --- a/commitizen/exceptions.py +++ b/commitizen/exceptions.py @@ -1,9 +1,12 @@ -import enum +from __future__ import annotations + +from enum import IntEnum +from typing import Any from commitizen import out -class ExitCode(enum.IntEnum): +class ExitCode(IntEnum): EXPECTED_EXIT = 0 NO_COMMITIZEN_FOUND = 1 NOT_A_GIT_PROJECT = 2 @@ -26,7 +29,7 @@ class ExitCode(enum.IntEnum): INVALID_CONFIGURATION = 19 NOT_ALLOWED = 20 NO_INCREMENT = 21 - UNRECOGNIZED_CHARACTERSET_ENCODING = 22 + CHARACTER_SET_DECODE_ERROR = 22 GIT_COMMAND_ERROR = 23 INVALID_MANUAL_VERSION = 24 INIT_FAILED = 25 @@ -38,11 +41,17 @@ class ExitCode(enum.IntEnum): CONFIG_FILE_IS_EMPTY = 31 COMMIT_MESSAGE_LENGTH_LIMIT_EXCEEDED = 32 + @classmethod + def from_str(cls, value: str) -> ExitCode: + if value.isdecimal(): + return cls(int(value)) + return cls[value.strip()] + class CommitizenException(Exception): - def __init__(self, *args, **kwargs): + def __init__(self, *args: str, **kwargs: Any) -> None: self.output_method = kwargs.get("output_method") or out.error - self.exit_code = self.__class__.exit_code + self.exit_code: ExitCode = self.__class__.exit_code if args: self.message = args[0] elif hasattr(self.__class__, "message"): @@ -50,46 +59,57 @@ def __init__(self, *args, **kwargs): else: self.message = "" - def __str__(self): + def __str__(self) -> str: 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 + def __init__(self, *args: str, **kwargs: Any) -> None: + kwargs["output_method"] = kwargs.get("output_method") or out.write super().__init__(*args, **kwargs) class DryRunExit(ExpectedExit): - pass + """Exit due to passing `--dry-run` option""" class NoneIncrementExit(CommitizenException): + """The commits found are not eligible to be bumped""" + exit_code = ExitCode.NO_INCREMENT class NoCommitizenFoundException(CommitizenException): + """Using a cz (e.g., `cz_jira`) that cannot be found in your system""" + exit_code = ExitCode.NO_COMMITIZEN_FOUND class NotAGitProjectError(CommitizenException): + """Not in a git project""" + exit_code = ExitCode.NOT_A_GIT_PROJECT message = "fatal: not a git repository (or any of the parent directories): .git" class MissingCzCustomizeConfigError(CommitizenException): + """Configuration is missing for `cz_customize`""" + exit_code = ExitCode.MISSING_CZ_CUSTOMIZE_CONFIG message = "fatal: customize is not set in configuration file." class NoCommitsFoundError(CommitizenException): + """No commits found""" + exit_code = ExitCode.NO_COMMITS_FOUND class NoVersionSpecifiedError(CommitizenException): + """Version is not specified in configuration file""" + exit_code = ExitCode.NO_VERSION_SPECIFIED message = ( "[NO_VERSION_SPECIFIED]\n" @@ -99,111 +119,166 @@ class NoVersionSpecifiedError(CommitizenException): class NoPatternMapError(CommitizenException): + """bump / changelog pattern or map can not be found in configuration file""" + exit_code = ExitCode.NO_PATTERN_MAP class BumpCommitFailedError(CommitizenException): + """Commit failed when bumping version""" + exit_code = ExitCode.BUMP_COMMIT_FAILED class BumpTagFailedError(CommitizenException): + """Tag failed when bumping version""" + exit_code = ExitCode.BUMP_TAG_FAILED class CurrentVersionNotFoundError(CommitizenException): + """Current version cannot be found in `version_files`""" + exit_code = ExitCode.CURRENT_VERSION_NOT_FOUND class NoAnswersError(CommitizenException): + """No user response given""" + exit_code = ExitCode.NO_ANSWERS class CommitError(CommitizenException): + """git commit error""" + exit_code = ExitCode.COMMIT_ERROR class NoCommitBackupError(CommitizenException): + """Commit backup file is not found""" + exit_code = ExitCode.NO_COMMIT_BACKUP message = "No commit backup found" class NothingToCommitError(CommitizenException): + """Nothing in staging to be committed""" + exit_code = ExitCode.NOTHING_TO_COMMIT class CustomError(CommitizenException): + """`CzException` raised""" + exit_code = ExitCode.CUSTOM_ERROR class InvalidCommitMessageError(CommitizenException): + """The commit message does not pass `cz check`""" + exit_code = ExitCode.INVALID_COMMIT_MSG class NoRevisionError(CommitizenException): + """No revision found""" + exit_code = ExitCode.NO_REVISION message = "No tag found to do an incremental changelog" class NoCommandFoundError(CommitizenException): + """No command found when running Commitizen cli (e.g., `cz --debug`)""" + exit_code = ExitCode.NO_COMMAND_FOUND message = "Command is required" class InvalidCommandArgumentError(CommitizenException): + """The argument provided to the command is invalid (e.g. `cz check -commit-msg-file filename --rev-range master..`)""" + exit_code = ExitCode.INVALID_COMMAND_ARGUMENT class InvalidConfigurationError(CommitizenException): + """An error was found in the Commitizen Configuration, such as duplicates in `change_type_order`""" + exit_code = ExitCode.INVALID_CONFIGURATION class NotAllowed(CommitizenException): + """`--incremental` cannot be combined with a `rev_range`""" + exit_code = ExitCode.NOT_ALLOWED class CharacterSetDecodeError(CommitizenException): - exit_code = ExitCode.UNRECOGNIZED_CHARACTERSET_ENCODING + """The character encoding of the command output could not be determined""" + + exit_code = ExitCode.CHARACTER_SET_DECODE_ERROR class GitCommandError(CommitizenException): + """Unexpected failure while calling a git command""" + exit_code = ExitCode.GIT_COMMAND_ERROR class InvalidManualVersion(CommitizenException): + """Manually provided version is invalid""" + exit_code = ExitCode.INVALID_MANUAL_VERSION class InitFailedError(CommitizenException): + """Failed to initialize pre-commit""" + exit_code = ExitCode.INIT_FAILED class RunHookError(CommitizenException): + """An error occurred during a hook execution""" + exit_code = ExitCode.RUN_HOOK_FAILED class VersionProviderUnknown(CommitizenException): + """Unknown `version_provider`""" + exit_code = ExitCode.VERSION_PROVIDER_UNKNOWN class VersionSchemeUnknown(CommitizenException): + """Unknown `version_scheme`""" + exit_code = ExitCode.VERSION_SCHEME_UNKNOWN class ChangelogFormatUnknown(CommitizenException): + """Unknown `changelog_format` or cannot be determined by the file extension""" + exit_code = ExitCode.CHANGELOG_FORMAT_UNKNOWN message = "Unknown changelog format identifier" class ConfigFileNotFound(CommitizenException): + """The configuration file is not found""" + exit_code = ExitCode.CONFIG_FILE_NOT_FOUND message = "Cannot found the config file, please check your file path again." class ConfigFileIsEmpty(CommitizenException): + """The configuration file is empty""" + exit_code = ExitCode.CONFIG_FILE_IS_EMPTY message = "Config file is empty, please check your file path again." class CommitMessageLengthExceededError(CommitizenException): + """The commit message length exceeds the given limit.""" + exit_code = ExitCode.COMMIT_MESSAGE_LENGTH_LIMIT_EXCEEDED message = "Length of commit message exceeds the given limit." + + +# When adding / updating a new exit code, please update the documentation of the exit codes in docs/exit_codes.md diff --git a/commitizen/factory.py b/commitizen/factory.py index 09af5fd0f7..d9e99fb771 100644 --- a/commitizen/factory.py +++ b/commitizen/factory.py @@ -4,16 +4,14 @@ from commitizen.exceptions import NoCommitizenFoundException -def commiter_factory(config: BaseConfig) -> BaseCommitizen: +def committer_factory(config: BaseConfig) -> BaseCommitizen: """Return the correct commitizen existing in the registry.""" name: str = config.settings["name"] try: - _cz = registry[name](config) + return 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 index 1f758889ed..dc26038986 100644 --- a/commitizen/git.py +++ b/commitizen/git.py @@ -2,77 +2,155 @@ import os from enum import Enum -from os import linesep +from functools import lru_cache 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): +class EOLType(Enum): """The EOL type from `git config core.eol`.""" LF = "lf" CRLF = "crlf" NATIVE = "native" - def get_eol_for_open(self) -> str: + @classmethod + def for_open(cls) -> str: + c = cmd.run("git config core.eol") + eol = c.out.strip().upper() + return cls._char_for_open()[cls._safe_cast(eol)] + + @classmethod + def _safe_cast(cls, eol: str) -> EOLType: + try: + return cls[eol] + except KeyError: + return cls.NATIVE + + @classmethod + @lru_cache + def _char_for_open(cls) -> dict[EOLType, str]: """Get the EOL character for `open()`.""" - map = { - EOLTypes.CRLF: WINDOWS_EOL, - EOLTypes.LF: UNIX_EOL, - EOLTypes.NATIVE: linesep, + return { + cls.LF: "\n", + cls.CRLF: "\r\n", + cls.NATIVE: os.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 + def __eq__(self, other: object) -> bool: + return hasattr(other, "rev") and self.rev == other.rev + + def __hash__(self) -> int: + return hash(self.rev) class GitCommit(GitObject): def __init__( - self, rev, title, body: str = "", author: str = "", author_email: str = "" - ): + self, + rev: str, + title: str, + body: str = "", + author: str = "", + author_email: str = "", + parents: list[str] | None = 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): + def message(self) -> str: return f"{self.title}\n\n{self.body}".strip() - def __repr__(self): + @classmethod + def from_rev_and_commit(cls, rev_and_commit: str) -> GitCommit: + """Create a GitCommit instance from a formatted commit string. + + This method parses a multi-line string containing commit information in the following format: + ``` + <rev> + <parents> + <title> + <author> + <author_email> + <body_line_1> + <body_line_2> + ... + ``` + + Args: + rev_and_commit (str): A string containing commit information with fields separated by newlines. + - rev: The commit hash/revision + - parents: Space-separated list of parent commit hashes + - title: The commit title/message + - author: The commit author's name + - author_email: The commit author's email + - body: Optional multi-line commit body + + Returns: + GitCommit: A new GitCommit instance with the parsed information. + + Example: + >>> commit_str = '''abc123 + ... def456 ghi789 + ... feat: add new feature + ... John Doe + ... john@example.com + ... This is a detailed description + ... of the new feature''' + >>> commit = GitCommit.from_rev_and_commit(commit_str) + >>> commit.rev + 'abc123' + >>> commit.title + 'feat: add new feature' + >>> commit.parents + ['def456', 'ghi789'] + """ + rev, parents, title, author, author_email, *body_list = ( + rev_and_commit.splitlines() + ) + return cls( + 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], + ) + + def __repr__(self) -> str: return f"{self.title} ({self.rev})" class GitTag(GitObject): - def __init__(self, name, rev, date): + def __init__(self, name: str, rev: str, date: str) -> None: self.rev = rev.strip() self.name = name.strip() self._date = date.strip() - def __repr__(self): + def __repr__(self) -> str: return f"GitTag('{self.name}', '{self.rev}', '{self.date}')" @property - def date(self): + def date(self) -> str: return self._date + @date.setter + def date(self, value: str) -> None: + self._date = value + @classmethod def from_line(cls, line: str, inner_delimiter: str) -> GitTag: name, objectname, date, obj = line.split(inner_delimiter) @@ -85,22 +163,18 @@ def from_line(cls, line: str, inner_delimiter: str) -> GitTag: 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" + if not annotated and not signed: + return cmd.run(f"git tag {tag}") # 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 + option = "-s" if signed else "-a" # The else case is for annotated tags + return cmd.run(f'git tag {option} {tag} -m "{msg or tag}"') def add(*args: str) -> cmd.Command: - c = cmd.run(f"git add {' '.join(args)}") - return c + return cmd.run(f"git add {' '.join(args)}") def commit( @@ -112,45 +186,40 @@ def commit( 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}" - + command = _create_commit_cmd_string(args, committer_date, f.name) c = cmd.run(command) os.unlink(f.name) return c +def _create_commit_cmd_string(args: str, committer_date: str | None, name: str) -> str: + command = f'git commit {args} -F "{name}"' + if not committer_date: + return command + if os.name != "nt": + return f"GIT_COMMITTER_DATE={committer_date} {command}" + # Using `cmd /v /c "{command}"` sets environment variables only for that command + return f'cmd /v /c "set GIT_COMMITTER_DATE={committer_date}&& {command}"' + + def get_commits( start: str | None = None, - end: str = "HEAD", + end: str | None = None, *, args: str = "", ) -> list[GitCommit]: """Get the commits between start and end.""" + if end is None: + end = "HEAD" 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, 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, - ) - git_commits.append(git_commit) - return git_commits - - -def get_filenames_in_commit(git_reference: str = ""): + return [ + GitCommit.from_rev_and_commit(rev_and_commit) + for rev_and_commit in git_log_entries + if rev_and_commit + ] + + +def get_filenames_in_commit(git_reference: str = "") -> list[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 @@ -160,8 +229,7 @@ def get_filenames_in_commit(git_reference: str = ""): 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) + raise GitCommandError(c.err) def get_tags( @@ -169,7 +237,7 @@ def get_tags( ) -> list[GitTag]: inner_delimiter = "---inner_delimiter---" formatter = ( - f'"%(refname:lstrip=2){inner_delimiter}' + f'"%(refname:strip=2){inner_delimiter}' f"%(objectname){inner_delimiter}" f"%(creatordate:format:{dateformat}){inner_delimiter}" f'%(object)"' @@ -187,16 +255,11 @@ def get_tags( if c.err: out.warn(f"Attempting to proceed after: {c.err}") - if not c.out: - return [] - - git_tags = [ + return [ 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}") @@ -221,18 +284,18 @@ def get_tag_message(tag: str) -> str | None: return c.out.strip() -def get_tag_names() -> list[str | None]: +def get_tag_names() -> list[str]: c = cmd.run("git tag --list") if c.err: return [] - return [tag.strip() for tag in c.out.split("\n") if tag.strip()] + return [tag for raw in c.out.split("\n") if (tag := raw.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 + if c.err: + return None + return Path(c.out.strip()) def is_staging_clean() -> bool: @@ -243,53 +306,36 @@ def is_staging_clean() -> bool: 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 smart_open(*args, **kargs): + return c.out.strip() == "true" + + +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, **kwargs): # type: ignore[no-untyped-def,unused-ignore] # noqa: ANN201 """Open a file with the EOL style determined from Git.""" - return open(*args, newline=get_eol_style().get_eol_for_open(), **kargs) + return open(*args, newline=EOLType.for_open(), **kwargs) 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%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}" + log_format: str = "%H%n%P%n%s%n%an%n%ae%n%b" + command_range = f"{start}..{end}" if start else end + command = f"git -c log.showSignature=False log --pretty={log_format}{delimiter} {args} {command_range}" + 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") + + +def get_default_branch() -> str: + c = cmd.run("git symbolic-ref refs/remotes/origin/HEAD") + if c.return_code != 0: + raise GitCommandError(c.err) + return c.out.strip() diff --git a/commitizen/hooks.py b/commitizen/hooks.py index f5505d0e82..10560d5eae 100644 --- a/commitizen/hooks.py +++ b/commitizen/hooks.py @@ -1,12 +1,16 @@ from __future__ import annotations import os +from typing import TYPE_CHECKING from commitizen import cmd, out from commitizen.exceptions import RunHookError +if TYPE_CHECKING: + from collections.abc import Mapping -def run(hooks, _env_prefix="CZ_", **env): + +def run(hooks: str | list[str], _env_prefix: str = "CZ_", **env: object) -> None: if isinstance(hooks, str): hooks = [hooks] @@ -24,7 +28,7 @@ def run(hooks, _env_prefix="CZ_", **env): raise RunHookError(f"Running hook '{hook}' failed") -def _format_env(prefix: str, env: dict[str, str]) -> dict[str, str]: +def _format_env(prefix: str, env: Mapping[str, object]) -> 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) diff --git a/commitizen/out.py b/commitizen/out.py index 40342e9de5..1bbfe4329d 100644 --- a/commitizen/out.py +++ b/commitizen/out.py @@ -1,5 +1,6 @@ import io import sys +from typing import Any from termcolor import colored @@ -8,12 +9,12 @@ sys.stdout.reconfigure(encoding="utf-8") -def write(value: str, *args) -> None: +def write(value: str, *args: object) -> None: """Intended to be used when value is multiline.""" print(value, *args) -def line(value: str, *args, **kwargs) -> None: +def line(value: str, *args: object, **kwargs: Any) -> None: """Wrapper in case I want to do something different later.""" print(value, *args, **kwargs) @@ -33,7 +34,7 @@ def info(value: str) -> None: line(message) -def diagnostic(value: str): +def diagnostic(value: str) -> None: line(value, file=sys.stderr) diff --git a/commitizen/project_info.py b/commitizen/project_info.py new file mode 100644 index 0000000000..381629304e --- /dev/null +++ b/commitizen/project_info.py @@ -0,0 +1,47 @@ +"""Resolves project information about the current working directory.""" + +import shutil +from pathlib import Path +from typing import Literal + + +def is_pre_commit_installed() -> bool: + return any(shutil.which(tool) for tool in ("pre-commit", "prek")) + + +def get_default_version_provider() -> Literal[ + "commitizen", "cargo", "composer", "npm", "pep621", "poetry", "uv" +]: + pyproject_path = Path("pyproject.toml") + if pyproject_path.is_file(): + if "[tool.poetry]" in pyproject_path.read_text(): + return "poetry" + if Path("uv.lock").is_file(): + return "uv" + return "pep621" + + if Path("setup.py").is_file(): + return "pep621" + + if Path("Cargo.toml").is_file(): + return "cargo" + + if Path("package.json").is_file(): + return "npm" + + if Path("composer.json").is_file(): + return "composer" + + return "commitizen" + + +def get_default_config_filename() -> Literal["pyproject.toml", ".cz.toml"]: + return "pyproject.toml" if Path("pyproject.toml").is_file() else ".cz.toml" + + +def get_default_version_scheme() -> Literal["pep440", "semver"]: + return ( + "pep440" + if Path("pyproject.toml").is_file() or Path("setup.py").is_file() + else "semver" + ) diff --git a/commitizen/providers/__init__.py b/commitizen/providers/__init__.py index 3fd4ab1bfd..6bec8f1567 100644 --- a/commitizen/providers/__init__.py +++ b/commitizen/providers/__init__.py @@ -1,16 +1,10 @@ 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 importlib import metadata +from typing import TYPE_CHECKING, cast 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 @@ -18,9 +12,13 @@ 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 + +if TYPE_CHECKING: + from commitizen.config.base_config import BaseConfig + from commitizen.providers.base_provider import VersionProvider __all__ = [ - "get_provider", "CargoProvider", "CommitizenProvider", "ComposerProvider", @@ -28,6 +26,8 @@ "Pep621Provider", "PoetryProvider", "ScmProvider", + "UvProvider", + "get_provider", ] PROVIDER_ENTRYPOINT = "commitizen.provider" @@ -46,4 +46,4 @@ def get_provider(config: BaseConfig) -> VersionProvider: except ValueError: raise VersionProviderUnknown(f'Version Provider "{provider_name}" unknown.') provider_cls = ep.load() - return cast(VersionProvider, provider_cls(config)) + return cast("VersionProvider", provider_cls(config)) diff --git a/commitizen/providers/base_provider.py b/commitizen/providers/base_provider.py index 34048318e2..b77d86394c 100644 --- a/commitizen/providers/base_provider.py +++ b/commitizen/providers/base_provider.py @@ -3,11 +3,14 @@ import json from abc import ABC, abstractmethod from pathlib import Path -from typing import Any, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar import tomlkit -from commitizen.config.base_config import BaseConfig +if TYPE_CHECKING: + from collections.abc import Mapping + + from commitizen.config.base_config import BaseConfig class VersionProvider(ABC): @@ -19,7 +22,7 @@ class VersionProvider(ABC): config: BaseConfig - def __init__(self, config: BaseConfig): + def __init__(self, config: BaseConfig) -> None: self.config = config @abstractmethod @@ -29,7 +32,7 @@ def get_version(self) -> str: """ @abstractmethod - def set_version(self, version: str): + def set_version(self, version: str) -> None: """ Set the new current version """ @@ -46,6 +49,9 @@ class FileProvider(VersionProvider): def file(self) -> Path: return Path() / self.filename + def _get_encoding(self) -> str: + return self.config.settings["encoding"] + class JsonProvider(FileProvider): """ @@ -55,18 +61,21 @@ class JsonProvider(FileProvider): indent: ClassVar[int] = 2 def get_version(self) -> str: - document = json.loads(self.file.read_text()) + document = json.loads(self.file.read_text(encoding=self._get_encoding())) return self.get(document) - def set_version(self, version: str): - document = json.loads(self.file.read_text()) + def set_version(self, version: str) -> None: + document = json.loads(self.file.read_text(encoding=self._get_encoding())) self.set(document, version) - self.file.write_text(json.dumps(document, indent=self.indent) + "\n") + self.file.write_text( + json.dumps(document, indent=self.indent) + "\n", + encoding=self._get_encoding(), + ) - def get(self, document: dict[str, Any]) -> str: - return document["version"] # type: ignore + def get(self, document: Mapping[str, str]) -> str: + return document["version"] - def set(self, document: dict[str, Any], version: str): + def set(self, document: dict[str, Any], version: str) -> None: document["version"] = version @@ -76,16 +85,16 @@ class TomlProvider(FileProvider): """ def get_version(self) -> str: - document = tomlkit.parse(self.file.read_text()) + document = tomlkit.parse(self.file.read_text(encoding=self._get_encoding())) return self.get(document) - def set_version(self, version: str): - document = tomlkit.parse(self.file.read_text()) + def set_version(self, version: str) -> None: + document = tomlkit.parse(self.file.read_text(encoding=self._get_encoding())) self.set(document, version) - self.file.write_text(tomlkit.dumps(document)) + self.file.write_text(tomlkit.dumps(document), encoding=self._get_encoding()) def get(self, document: tomlkit.TOMLDocument) -> str: - return document["project"]["version"] # type: ignore + return document["project"]["version"] # type: ignore[index,return-value] - def set(self, document: tomlkit.TOMLDocument, version: str): - document["project"]["version"] = version # type: ignore + def set(self, document: tomlkit.TOMLDocument, version: str) -> None: + document["project"]["version"] = version # type: ignore[index] diff --git a/commitizen/providers/cargo_provider.py b/commitizen/providers/cargo_provider.py index cee687c15b..235d4a110c 100644 --- a/commitizen/providers/cargo_provider.py +++ b/commitizen/providers/cargo_provider.py @@ -1,9 +1,18 @@ from __future__ import annotations -import tomlkit +import fnmatch +import glob +from pathlib import Path +from typing import TYPE_CHECKING + +from tomlkit import TOMLDocument, dumps, parse +from tomlkit.exceptions import NonExistentKey from commitizen.providers.base_provider import TomlProvider +if TYPE_CHECKING: + from tomlkit.items import AoT + class CargoProvider(TomlProvider): """ @@ -13,18 +22,90 @@ class CargoProvider(TomlProvider): """ filename = "Cargo.toml" + lock_filename = "Cargo.lock" - 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 + @property + def lock_file(self) -> Path: + return Path() / self.lock_filename + + def get(self, document: TOMLDocument) -> str: + out = _try_get_workspace(document)["package"]["version"] + if TYPE_CHECKING: + assert isinstance(out, str) + return out + + def set(self, document: TOMLDocument, version: str) -> None: + _try_get_workspace(document)["package"]["version"] = version + + def set_version(self, version: str) -> None: + super().set_version(version) + if self.lock_file.is_file(): + self.set_lock_version(version) + + def set_lock_version(self, version: str) -> None: + cargo_toml_content = parse(self.file.read_text(encoding=self._get_encoding())) + cargo_lock_content = parse( + self.lock_file.read_text(encoding=self._get_encoding()) + ) + packages = cargo_lock_content["package"] + + if TYPE_CHECKING: + assert isinstance(packages, AoT) - 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 + cargo_package_name = cargo_toml_content["package"]["name"] # type: ignore[index] + if TYPE_CHECKING: + assert isinstance(cargo_package_name, str) + for i, package in enumerate(packages): + if package["name"] == cargo_package_name: + cargo_lock_content["package"][i]["version"] = version # type: ignore[index] + break + except NonExistentKey: + workspace = cargo_toml_content.get("workspace", {}) + if TYPE_CHECKING: + assert isinstance(workspace, dict) + workspace_members = workspace.get("members", []) + excluded_workspace_members = workspace.get("exclude", []) + members_inheriting: list[str] = [] + + for member in workspace_members: + for path in glob.glob(member, recursive=True): + if any( + fnmatch.fnmatch(path, pattern) + for pattern in excluded_workspace_members + ): + continue + + cargo_file = Path(path) / "Cargo.toml" + package_content = parse( + cargo_file.read_text(encoding=self._get_encoding()) + ).get("package", {}) + if TYPE_CHECKING: + assert isinstance(package_content, dict) + try: + version_workspace = package_content["version"]["workspace"] + if version_workspace is True: + package_name = package_content["name"] + if TYPE_CHECKING: + assert isinstance(package_name, str) + members_inheriting.append(package_name) + except NonExistentKey: + pass + + for i, package in enumerate(packages): + if package["name"] in members_inheriting: + cargo_lock_content["package"][i]["version"] = version # type: ignore[index] + + self.lock_file.write_text( + dumps(cargo_lock_content), encoding=self._get_encoding() + ) + + +def _try_get_workspace(document: TOMLDocument) -> dict: + try: + workspace = document["workspace"] + if TYPE_CHECKING: + assert isinstance(workspace, dict) + return workspace + except NonExistentKey: + return document diff --git a/commitizen/providers/commitizen_provider.py b/commitizen/providers/commitizen_provider.py index a1da25ff72..f90d7d9b6e 100644 --- a/commitizen/providers/commitizen_provider.py +++ b/commitizen/providers/commitizen_provider.py @@ -1,5 +1,6 @@ from __future__ import annotations +from commitizen.exceptions import NoVersionSpecifiedError from commitizen.providers.base_provider import VersionProvider @@ -9,7 +10,9 @@ class CommitizenProvider(VersionProvider): """ def get_version(self) -> str: - return self.config.settings["version"] # type: ignore + if ret := self.config.settings["version"]: + return ret + raise NoVersionSpecifiedError() - def set_version(self, version: str): + def set_version(self, version: str) -> None: self.config.set_key("version", version) diff --git a/commitizen/providers/npm_provider.py b/commitizen/providers/npm_provider.py index 12900ff7de..6794f6a714 100644 --- a/commitizen/providers/npm_provider.py +++ b/commitizen/providers/npm_provider.py @@ -2,12 +2,15 @@ import json from pathlib import Path -from typing import Any, ClassVar +from typing import TYPE_CHECKING, Any, ClassVar -from commitizen.providers.base_provider import VersionProvider +from commitizen.providers.base_provider import JsonProvider +if TYPE_CHECKING: + from collections.abc import Mapping -class NpmProvider(VersionProvider): + +class NpmProvider(JsonProvider): """ npm package.json and package-lock.json version management """ @@ -33,33 +36,43 @@ def get_version(self) -> str: """ Get the current version from package.json """ - package_document = json.loads(self.package_file.read_text()) + package_document = json.loads( + self.package_file.read_text(encoding=self._get_encoding()) + ) 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 + json.loads(self.package_file.read_text(encoding=self._get_encoding())), + version, ) self.package_file.write_text( - json.dumps(package_document, indent=self.indent) + "\n" + json.dumps(package_document, indent=self.indent) + "\n", + encoding=self._get_encoding(), ) - if self.lock_file.exists(): + if self.lock_file.is_file(): lock_document = self.set_lock_version( - json.loads(self.lock_file.read_text()), version + json.loads(self.lock_file.read_text(encoding=self._get_encoding())), + version, ) self.lock_file.write_text( - json.dumps(lock_document, indent=self.indent) + "\n" + json.dumps(lock_document, indent=self.indent) + "\n", + encoding=self._get_encoding(), ) - if self.shrinkwrap_file.exists(): + if self.shrinkwrap_file.is_file(): shrinkwrap_document = self.set_shrinkwrap_version( - json.loads(self.shrinkwrap_file.read_text()), version + json.loads( + self.shrinkwrap_file.read_text(encoding=self._get_encoding()) + ), + version, ) self.shrinkwrap_file.write_text( - json.dumps(shrinkwrap_document, indent=self.indent) + "\n" + json.dumps(shrinkwrap_document, indent=self.indent) + "\n", + encoding=self._get_encoding(), ) - def get_package_version(self, document: dict[str, Any]) -> str: - return document["version"] # type: ignore + def get_package_version(self, document: Mapping[str, str]) -> str: + return document["version"] def set_package_version( self, document: dict[str, Any], version: str diff --git a/commitizen/providers/poetry_provider.py b/commitizen/providers/poetry_provider.py index 7aa28f56d9..d9a174a0a4 100644 --- a/commitizen/providers/poetry_provider.py +++ b/commitizen/providers/poetry_provider.py @@ -1,9 +1,12 @@ from __future__ import annotations -import tomlkit +from typing import TYPE_CHECKING from commitizen.providers.base_provider import TomlProvider +if TYPE_CHECKING: + import tomlkit + class PoetryProvider(TomlProvider): """ @@ -13,7 +16,7 @@ class PoetryProvider(TomlProvider): filename = "pyproject.toml" def get(self, pyproject: tomlkit.TOMLDocument) -> str: - return pyproject["tool"]["poetry"]["version"] # type: ignore + return pyproject["tool"]["poetry"]["version"] # type: ignore[index,return-value] - def set(self, pyproject: tomlkit.TOMLDocument, version: str): - pyproject["tool"]["poetry"]["version"] = version # type: ignore + def set(self, pyproject: tomlkit.TOMLDocument, version: str) -> None: + pyproject["tool"]["poetry"]["version"] = version # type: ignore[index] diff --git a/commitizen/providers/scm_provider.py b/commitizen/providers/scm_provider.py index 00df3e4153..687775da30 100644 --- a/commitizen/providers/scm_provider.py +++ b/commitizen/providers/scm_provider.py @@ -1,16 +1,8 @@ from __future__ import annotations -import re -from typing import Callable - from commitizen.git import get_tags from commitizen.providers.base_provider import VersionProvider -from commitizen.version_schemes import ( - InvalidVersion, - Version, - VersionProtocol, - get_version_scheme, -) +from commitizen.tags import TagRules class ScmProvider(VersionProvider): @@ -22,65 +14,13 @@ class ScmProvider(VersionProvider): It is meant for `setuptools-scm` or any package manager `*-scm` provider. """ - TAG_FORMAT_REGEXS = { - "$version": r"(?P<version>.+)", - "$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+)?", - } - - def _tag_format_matcher(self) -> Callable[[str], VersionProtocol | None]: - version_scheme = get_version_scheme(self.config) - pattern = self.config.settings["tag_format"] - if pattern == "$version": - pattern = version_scheme.parser.pattern - for var, tag_pattern in self.TAG_FORMAT_REGEXS.items(): - pattern = pattern.replace(var, tag_pattern) - - regex = re.compile(f"^{pattern}$", re.VERBOSE) - - def matcher(tag: str) -> Version | None: - match = regex.match(tag) - if not match: - return None - groups = match.groupdict() - if "version" in groups: - ver = groups["version"] - elif "major" in groups: - ver = "".join( - ( - groups["major"], - f".{groups['minor']}" if groups.get("minor") else "", - f".{groups['patch']}" if groups.get("patch") else "", - groups["prerelease"] if groups.get("prerelease") else "", - groups["devrelease"] if groups.get("devrelease") else "", - ) - ) - elif pattern == version_scheme.parser.pattern: - ver = tag - else: - return None - - try: - return version_scheme(ver) - except InvalidVersion: - return None - - return matcher - def get_version(self) -> str: - matcher = self._tag_format_matcher() - matches = sorted( - version - for t in get_tags(reachable_only=True) - if (version := matcher(t.name)) - ) - if not matches: - return "0.0.0" - return str(matches[-1]) + rules = TagRules.from_settings(self.config.settings) + tags = get_tags(reachable_only=True) + version_tags = rules.get_version_tags(tags) + version = max((rules.extract_version(t) for t in version_tags), default=None) + return str(version) if version is not None else "0.0.0" - def set_version(self, version: str): + def set_version(self, version: str) -> None: # Not necessary pass diff --git a/commitizen/providers/uv_provider.py b/commitizen/providers/uv_provider.py new file mode 100644 index 0000000000..0eb19e51a6 --- /dev/null +++ b/commitizen/providers/uv_provider.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from pathlib import Path + +import tomlkit +import tomlkit.items +from packaging.utils import canonicalize_name + +from commitizen.providers.base_provider import TomlProvider + + +class UvProvider(TomlProvider): + """ + uv.lock and pyproject.toml 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(encoding=self._get_encoding()) + ) + project_name = pyproject_toml_content["project"]["name"] # type: ignore[index] + normalized_project_name = canonicalize_name(str(project_name)) + + document = tomlkit.parse( + self.lock_file.read_text(encoding=self._get_encoding()) + ) + + packages: tomlkit.items.AoT = document["package"] # type: ignore[assignment] + for i, package in enumerate(packages): + if package["name"] == normalized_project_name: + document["package"][i]["version"] = version # type: ignore[index] + break + self.lock_file.write_text( + tomlkit.dumps(document), encoding=self._get_encoding() + ) diff --git a/commitizen/question.py b/commitizen/question.py new file mode 100644 index 0000000000..043b8f3ba3 --- /dev/null +++ b/commitizen/question.py @@ -0,0 +1,33 @@ +from collections.abc import Callable +from typing import Literal, TypedDict + + +class Choice(TypedDict, total=False): + value: str + name: str + key: str + + +class ListQuestion(TypedDict, total=False): + type: Literal["list"] + name: str + message: str + choices: list[Choice] + use_shortcuts: bool + + +class InputQuestion(TypedDict, total=False): + type: Literal["input"] + name: str + message: str + filter: Callable[[str], str] + + +class ConfirmQuestion(TypedDict): + type: Literal["confirm"] + name: str + message: str + default: bool + + +CzQuestion = ListQuestion | InputQuestion | ConfirmQuestion diff --git a/commitizen/tags.py b/commitizen/tags.py new file mode 100644 index 0000000000..11b9899bed --- /dev/null +++ b/commitizen/tags.py @@ -0,0 +1,283 @@ +from __future__ import annotations + +import re +import warnings +from dataclasses import dataclass, field +from functools import cached_property +from itertools import chain +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 collections.abc import Iterable, Sequence + + 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 + + @property + def tag_formats(self) -> Iterable[str]: + return chain([self.tag_format], self.legacy_tag_formats) + + @cached_property + def version_regexes(self) -> list[re.Pattern]: + """Regexes for all legit tag formats, current and legacy""" + return [re.compile(self._format_regex(f)) for f in self.tag_formats] + + @cached_property + def ignored_regexes(self) -> list[re.Pattern]: + """Regexes for known but ignored tag formats""" + return [ + re.compile(self._format_regex(f, star=True)) + for f in self.ignored_tag_formats + ] + + 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 _version_tag_error(self, tag: str) -> str: + """Format the error message for an invalid version tag""" + return f"Invalid version tag: '{tag}' does not match any configured tag format" + + 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.fullmatch(tag) for regex in self.version_regexes) + if warn and not is_legit and not self.is_ignored_tag(tag): + out.warn(self._version_tag_error(tag)) + 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: Iterable[GitTag], warn: bool = False + ) -> list[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 (match := next(candidates, None)): + raise InvalidVersion(self._version_tag_error(tag.name)) + + if version := match.groupdict().get("version"): + return self.scheme(version) + + return self.scheme(self._extract_version(match)) + + 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 + return not (self.merge_prereleases and version.is_prerelease) + + 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] + + groups = match.groupdict() + if version := groups.get("version"): + return VersionTag(version, match.group(0)) + + if not all(value in groups for value in ["major", "minor", "patch"]): + return None + + version = self._extract_version(match) + 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 = (list(version.release) + [0, 0, 0])[:3] + 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: Iterable[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 + release = version.release + + # If the requested version is incomplete (e.g., "1.2"), try to find the latest + # matching tag that shares the provided prefix. + if len(release) < 3: + matching_versions: list[tuple[Version, GitTag]] = [] + for tag in tags: + try: + tag_version = self.extract_version(tag) + except InvalidVersion: + continue + if tag_version.release[: len(release)] != release: + continue + matching_versions.append((tag_version, tag)) + + if matching_versions: + _, latest_tag = max(matching_versions, key=lambda vt: vt[0]) + return latest_tag + + possible_tags = set(self.normalize_tag(version, f) for f in self.tag_formats) + candidates = [t for t in tags if t.name 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"], + ) + + def _extract_version(self, match: re.Match[str]) -> str: + groups = match.groupdict() + parts: list[str] = [groups["major"]] + if minor := groups.get("minor"): + parts.append(f".{minor}") + if patch := groups.get("patch"): + parts.append(f".{patch}") + if prerelease := groups.get("prerelease"): + parts.append(f"-{prerelease}") + if devrelease := groups.get("devrelease"): + parts.append(devrelease) + return "".join(parts) diff --git a/commitizen/version_schemes.py b/commitizen/version_schemes.py index 346287a065..1f17e90871 100644 --- a/commitizen/version_schemes.py +++ b/commitizen/version_schemes.py @@ -1,8 +1,8 @@ from __future__ import annotations import re -import sys import warnings +from importlib import metadata from itertools import zip_longest from typing import ( TYPE_CHECKING, @@ -10,29 +10,19 @@ ClassVar, Literal, Protocol, - Type, 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 InvalidVersion # noqa: F401 (expose the common exception) from packaging.version import Version as _BaseVersion -from commitizen.config.base_config import BaseConfig -from commitizen.defaults import MAJOR, MINOR, PATCH +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 + import sys + from typing import TypeAlias # Self is Python 3.11+ but backported in typing-extensions if sys.version_info < (3, 11): @@ -43,7 +33,9 @@ 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+)?)" +_DEFAULT_VERSION_PARSER = re.compile( + 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 @@ -51,7 +43,7 @@ class VersionProtocol(Protocol): parser: ClassVar[re.Pattern] """Regex capturing this version scheme into a `version` group""" - def __init__(self, version: str): + def __init__(self, version: str) -> None: """ Initialize a version object from its string representation. @@ -80,7 +72,7 @@ def is_prerelease(self) -> bool: @property def prerelease(self) -> str | None: - """The prelease potion of the version is this is a prerelease.""" + """The prerelease potion of the version is this is a prerelease.""" raise NotImplementedError("must be implemented") @property @@ -144,13 +136,13 @@ def bump( 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. + that attempts to deduce the correct increment when a prerelease suffix is present. """ # With PEP 440 and SemVer semantic, Scheme is the type, Version is an instance Version: TypeAlias = VersionProtocol -VersionScheme: TypeAlias = Type[VersionProtocol] +VersionScheme: TypeAlias = type[VersionProtocol] class BaseVersion(_BaseVersion): @@ -158,7 +150,7 @@ class BaseVersion(_BaseVersion): A base class implementing the `VersionProtocol` for PEP440-like versions. """ - parser: ClassVar[re.Pattern] = re.compile(DEFAULT_VERSION_PARSER) + parser: ClassVar[re.Pattern] = _DEFAULT_VERSION_PARSER """Regex capturing this version scheme into a `version` group""" @property @@ -193,15 +185,10 @@ def generate_prerelease( # https://semver.org/#spec-item-11 if self.is_prerelease and self.pre: prerelease = max(prerelease, self.pre[0]) + if prerelease.startswith(self.pre[0]): + offset = self.pre[1] + 1 - # 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 + return f"{prerelease}{offset}" def generate_devrelease(self, devrelease: int | None) -> str: """Generate devrelease @@ -266,40 +253,36 @@ def bump( 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 + return self.scheme(f"{self.public}+{local_version}") # type: ignore[return-value] + + base = self._get_increment_base(increment, exact_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) + + pre_version = ( + self if base == current_base else cast("BaseVersion", self.scheme(base)) + ).generate_prerelease(prerelease, offset=prerelease_offset) + + # TODO: post version + return self.scheme( + f"{base}{pre_version}{dev_version}{self.generate_build_metadata(build_metadata)}" + ) # type: ignore[return-value] + + def _get_increment_base( + self, increment: Increment | None, exact_increment: bool + ) -> str: + if ( + not self.is_prerelease + or exact_increment + or (increment == MINOR and self.micro != 0) + or (increment == MAJOR and (self.minor != 0 or self.micro != 0)) + ): + return self.increment_base(increment) + return f"{self.major}.{self.minor}.{self.micro}" class Pep440(BaseVersion): @@ -318,7 +301,7 @@ class SemVer(BaseVersion): """ def __str__(self) -> str: - parts = [] + parts: list[str] = [] # Epoch if self.epoch != 0: @@ -353,7 +336,7 @@ class SemVer2(SemVer): See: https://semver.org/spec/v2.0.0.html """ - _STD_PRELEASES = { + _STD_PRERELEASES = { "a": "alpha", "b": "beta", } @@ -361,12 +344,12 @@ class SemVer2(SemVer): @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]) + prerelease_type = self._STD_PRERELEASES.get(self.pre[0], self.pre[0]) return f"{prerelease_type}.{self.pre[1]}" return None def __str__(self) -> str: - parts = [] + parts: list[str] = [] # Epoch if self.epoch != 0: @@ -375,9 +358,19 @@ def __str__(self) -> str: # Release segment parts.append(".".join(str(x) for x in self.release)) + if prerelease := self._get_prerelease(): + parts.append(f"-{prerelease}") + + # Local version segment + if self.local: + parts.append(f"+{self.local}") + + return "".join(parts) + + def _get_prerelease(self) -> str: # Pre-release identifiers # See: https://semver.org/spec/v2.0.0.html#spec-item-9 - prerelease_parts = [] + prerelease_parts: list[str] = [] if self.prerelease: prerelease_parts.append(f"{self.prerelease}") @@ -389,15 +382,7 @@ def __str__(self) -> str: 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) + return ".".join(prerelease_parts) DEFAULT_SCHEME: VersionScheme = Pep440 @@ -409,22 +394,25 @@ def __str__(self) -> str: """All known registered version schemes""" -def get_version_scheme(config: BaseConfig, name: str | None = None) -> VersionScheme: +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` + or from an overridden `name`. + + :raises VersionSchemeUnknown: if the version scheme is not found. """ - deprecated_setting: str | None = config.settings.get("version_type") + # 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. " + "`version_type` setting is deprecated and will be removed in v5. " "Please use `version_scheme` instead" ) ) - name = name or config.settings.get("version_scheme") or deprecated_setting + name = name or settings.get("version_scheme") or deprecated_setting if not name: return DEFAULT_SCHEME @@ -432,7 +420,7 @@ def get_version_scheme(config: BaseConfig, name: str | None = None) -> VersionSc (ep,) = metadata.entry_points(name=name, group=SCHEMES_ENTRYPOINT) except ValueError: raise VersionSchemeUnknown(f'Version scheme "{name}" unknown.') - scheme = cast(VersionScheme, ep.load()) + scheme = cast("VersionScheme", ep.load()) if not isinstance(scheme, VersionProtocol): warnings.warn(f"Version scheme {name} does not implement the VersionProtocol") diff --git a/docs/README.md b/docs/README.md index b4fa13eb2c..b4b97884ab 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,158 +1,312 @@ [![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) +[![Check Links](https://github.com/commitizen-tools/commitizen/actions/workflows/links.yml/badge.svg)](https://github.com/commitizen-tools/commitizen/actions/workflows/links.yml) [![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) +[![prek](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/j178/prek/master/docs/assets/badge-v0.json&style=flat-square&color=brightgreen)](https://github.com/j178/prek) -![Using commitizen cli](images/demo.gif) +![Using Commitizen cli](images/cli_interactive/commit.gif) --- -**Documentation:** [https://commitizen-tools.github.io/commitizen/](https://commitizen-tools.github.io/commitizen/) +[**Commitizen Documentation Site**](https://commitizen-tools.github.io/commitizen/) --- ## About -Commitizen is release management tool designed for teams. +Commitizen is a powerful release management tool that helps teams maintain consistent and meaningful commit messages while automating version management. -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. +### What Commitizen Does -By default, commitizen uses [conventional commits][conventional_commits], but you -can build your own set of rules, and publish them. +By enforcing standardized commit conventions (defaulting to [Conventional Commits][conventional_commits]), Commitizen helps teams: -Using a standardized set of rules to write commits, makes commits easier to read, and enforces writing -descriptive commits. +- Write clear, structured commit messages +- Automatically manage version numbers using semantic versioning +- Generate and maintain changelogs +- Streamline the release process + +### Key Benefits + +With just a simple `cz bump` command, Commitizen handles: + +1. **Version Management**: Automatically bumps version numbers and updates version files based on your commit history +2. **Changelog Generation**: Creates and updates changelogs following the [Keep a changelog][keepchangelog] format +3. **Commit Standardization**: Enforces consistent commit message formats across your team + +This standardization makes your commit history more readable and meaningful, while the automation reduces manual work and potential errors in the release process. ### 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) +- Interactive CLI for standardized commits with default [Conventional Commits][conventional_commits] support +- Intelligent [version bumping](https://commitizen-tools.github.io/commitizen/commands/bump/) using [Semantic Versioning][semver] +- Automatic [keep a changelog][keepchangelog] generation +- Built-in commit validation with pre-commit hooks +- [Customizable](https://commitizen-tools.github.io/commitizen/customization/config_file/) commit rules and templates +- Multi-format version file support +- Custom rules and plugins via pip + +## Getting Started + +### Requirements + +Before installing Commitizen, ensure you have: + +- [Python](https://www.python.org/downloads/) `3.10+` +- [Git][gitscm] `1.8.5.2+` + +### Installation -## Requirements +#### Global Installation (Recommended) -[Python](https://www.python.org/downloads/) `3.8+` +The recommended way to install Commitizen is using [`pipx`](https://pipx.pypa.io/) or [`uv`](https://docs.astral.sh/uv/), which ensures a clean, isolated installation: -[Git][gitscm] `1.8.5.2+` +**Using pipx:** +```bash +# Install Commitizen +pipx install commitizen -## Installation +# Keep it updated +pipx upgrade commitizen +``` -To make commitizen available in your system +**Using uv:** +```bash +# Install commitizen +uv tool install commitizen + +# Keep it updated +uv tool upgrade commitizen +``` +**(For macOS users) Using Homebrew:** ```bash -pip install --user -U Commitizen +brew install commitizen ``` -### Python project +#### Project-Specific Installation -You can add it to your local project using one of these: +You can add Commitizen to your Python project using any of these package managers: +**Using pip:** ```bash pip install -U commitizen ``` -for Poetry >= 1.2.0: +**Using conda:** +```bash +conda install -c conda-forge commitizen +``` +**Using Poetry:** ```bash +# For Poetry >= 1.2.0 poetry add commitizen --group dev + +# For Poetry < 1.2.0 +poetry add commitizen --dev ``` -for Poetry < 1.2.0: +**Using uv:** +```bash +uv add --dev commitizen +``` +**Using pdm:** ```bash -poetry add commitizen --dev +pdm add -d commitizen ``` -### macOS +### Basic Commands -via [homebrew](https://formulae.brew.sh/formula/commitizen): +#### Initialize Commitizen -```bash -brew install commitizen +To get started, run the `cz init` command. This will guide you through the process of creating a configuration file with your preferred settings. + +#### Create Commits + +Create standardized commits using: +```sh +cz commit +# or use the shortcut +cz c ``` -## Usage +To sign off your commits: +```sh +cz commit -- --signoff +# or use the shortcut +cz commit -- -s +``` -Most of the time this is the only command you'll run: +For more commit options, run `cz commit --help`. +#### Version Management + +The most common command you'll use is: ```sh cz bump ``` -On top of that, you can use commitizen to assist you with the creation of commits: +This command: + +- Bumps your project's version +- Creates a git tag +- Updates the changelog (if `update_changelog_on_bump` is enabled) +- Updates version files + +You can customize: + +- [Version files](https://commitizen-tools.github.io/commitizen/commands/bump/#version_files) +- [Version scheme](https://commitizen-tools.github.io/commitizen/commands/bump/#version_scheme) +- [Version provider](https://commitizen-tools.github.io/commitizen/config/version_provider/) + +For all available options, see the [bump command documentation](https://commitizen-tools.github.io/commitizen/commands/bump/). + +### Advanced Usage + +#### Get Project Version ```sh -cz commit +# Get your project's version (instead of Commitizen's version) +cz version -p +# Preview changelog changes +cz changelog --dry-run "$(cz version -p)" ``` -Read more in the section [Getting Started](./getting_started.md). +This command is particularly useful for automation scripts and CI/CD pipelines. + +For example, you can use the output of the command `cz changelog --dry-run "$(cz version -p)"` to notify your team about a new release in Slack. -### Help +#### Prek and Pre-commit Integration +Commitizen can automatically validate your commit messages using pre-commit hooks. + +1. Add to your `.pre-commit-config.yaml`: +```yaml +--- +repos: + - repo: https://github.com/commitizen-tools/commitizen + rev: master # Replace with latest tag + hooks: + - id: commitizen + - id: commitizen-branch + stages: [pre-push] +``` + +2. Install the hooks: ```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) +prek install --hook-type commit-msg --hook-type pre-push ``` +| Hook | Recommended Stage | +| ----------------- | ----------------- | +| commitizen | commit-msg | +| commitizen-branch | pre-push | + +> **Note**: Replace `master` with the [latest tag](https://github.com/commitizen-tools/commitizen/tags) to avoid warnings. You can automatically update this with: +> ```sh +> prek autoupdate +> ``` + +For more details about commit validation, see the [check command documentation](https://commitizen-tools.github.io/commitizen/commands/check/). + +## Help & Reference + +### Command Line Interface + +Commitizen provides a comprehensive CLI with various commands. Here's the complete reference: + +![cz --help](images/cli_help/cz___help.svg) + +### Quick Reference + +| Command | Description | Alias | +|---------|-------------|-------| +| `cz init` | Initialize Commitizen configuration | - | +| `cz commit` | Create a new commit | `cz c` | +| `cz bump` | Bump version and update changelog | - | +| `cz changelog` | Generate changelog | `cz ch` | +| `cz check` | Validate commit messages | - | +| `cz version` | Show version information | - | + +### Additional Resources + +- [Conventional Commits Specification][conventional_commits] +- [Exit Codes Reference](https://commitizen-tools.github.io/commitizen/exit_codes/) +- [Configuration Guide](https://commitizen-tools.github.io/commitizen/config/configuration_file/) +- [Command Documentation](https://commitizen-tools.github.io/commitizen/commands/init/) + +### Getting Help + +For each command, you can get detailed help by adding `--help`: + +```sh +cz commit --help +cz bump --help +cz changelog --help +``` + +For more details, visit our [documentation site](https://commitizen-tools.github.io/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. +Commitizen supports command-line completion through [argcomplete](https://kislyuk.github.io/argcomplete/), which is automatically installed as a dependency. This feature provides intelligent auto-completion for all Commitizen commands and options. + +### Supported Shells + +- **Bash**: Full support +- **Zsh**: Limited support +- **Fish**: Limited support +- **Tcsh**: Limited support + +### Installation Methods -argcomplete is installed when you install Commitizen since it's a dependency. +#### Global Installation (Recommended) -If Commitizen is installed globally, global activation can be executed: +If you installed Commitizen globally (e.g., using `pipx` or `brew`), you can enable global completion: ```bash +# Enable global completion for all Python applications sudo activate-global-python-argcomplete ``` -For permanent (but not global) Commitizen activation, use: +#### User-Specific Installation + +For a user-specific installation that persists across sessions: ```bash +# Add to your shell's startup file (e.g., ~/.bashrc, ~/.zshrc) register-python-argcomplete cz >> ~/.bashrc ``` -For one-time activation of argcomplete for Commitizen only, use: +#### Temporary Installation + +For one-time activation in your current shell session: ```bash +# Activate completion for current session only eval "$(register-python-argcomplete cz)" ``` -For further information on activation, please visit the [argcomplete website](https://kislyuk.github.io/argcomplete/). +### Verification + +After installation, you can verify the completion is working by: + +1. Opening a new terminal session +2. Typing `cz` followed by a space and pressing `TAB` twice +3. You should see a list of available commands + +For more detailed information about argcomplete configuration and troubleshooting, visit the [argcomplete documentation](https://kislyuk.github.io/argcomplete/). + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=commitizen-tools/commitizen)](https://star-history.com/#commitizen-tools/commitizen) + ## Sponsors diff --git a/docs/commands/bump.md b/docs/commands/bump.md index 4d370a6a1f..51293bca4b 100644 --- a/docs/commands/bump.md +++ b/docs/commands/bump.md @@ -1,69 +1,138 @@ -![Bump version](../images/bump.gif) +![Bump version](../images/cli_interactive/bump.gif) ## About -`cz bump` **automatically** increases the version, based on the commits. +`cz bump` is a powerful command that **automatically** determines and increases your project's version number based on your commit history. -The commits should follow the rules established by the committer in order to be parsed correctly. +It analyzes your commits to determine the appropriate version increment according to semantic versioning principles. -**prerelease** versions are supported (alpha, beta, release candidate). +!!! note + In the following documentation, the term "configuration file" refers to `pyproject.toml`, `.cz.toml` or other configuration files. + + We will use `pyproject.toml` as the configuration file throughout the documentation. + + See [Configuration file](../config/configuration_file.md) for more details. -The version can also be **manually** bumped. +## Key Features -The version format follows [PEP 0440][pep440] and [semantic versioning][semver]. +- **Automatic Version Detection**: Analyzes commit history to determine the appropriate version bump +- **Manual Version Control**: Supports manual version specification when needed +- **Pre-release Support**: Handles alpha, beta, and release candidate versions +- **Multiple Version Schemes**: Supports both [PEP 440][pep440] and [semantic versioning][semver] formats -This means `MAJOR.MINOR.PATCH` +### Version Increment Rules + +The version follows the `MAJOR.MINOR.PATCH` format, with increments determined by your commit types: | Increment | Description | Conventional commit map | | --------- | --------------------------- | ----------------------- | -| `MAJOR` | Breaking changes introduced | `BREAKING CHANGE` | +| `MAJOR` | Breaking changes introduced | `BREAKING CHANGE`, bang (e.g. `feat!`)| | `MINOR` | New features | `feat` | -| `PATCH` | Fixes | `fix` + everything else | +| `PATCH` | Fixes and improvements | `fix`, `perf`, `refactor`| + +### `--version-scheme` -[PEP 0440][pep440] is the default, you can switch by using the setting `version_scheme` or the cli: +By default, Commitizen uses [PEP 440][pep440] for version formatting. You can switch to semantic versioning using either: +1. Command line: ```sh cz bump --version-scheme semver ``` -Some examples of pep440: +2. Configuration file: +```toml title="pyproject.toml" +[tool.commitizen] +version_scheme = "semver" +``` -```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 +Available options are: + +- `pep440`: [PEP 440][pep440] (**default** and recommended for Python projects) +- `semver`: [Semantic Versioning][semver] (recommended for non-Python projects) + +You can also set this in the configuration file with `version_scheme = "semver"`. + +!!! note + [pep440][pep440] and [semver][semver] are quite similar, although their difference lies in + how the prereleases look. For example, `0.3.1a0` in pep440 is equivalent to `0.3.1-a0` in semver. + + The following table illustrates the difference between the two schemes: + + | Version Type | 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` | + + +!!! note "Incomplete Version Handling" + Commitizen treats a three-part version (major.minor.patch) as complete. + If your configured version is incomplete (for example, `1` or `1.2`), Commitizen pads missing parts with zeros when it needs `major/minor/patch` for tag formatting. + The tag output depends on your `tag_format`: formats using `${version}` keep `1`/`1.2`, while formats using `${major}.${minor}.${patch}` will render `1.0.0`/`1.2.0`. + + When bumping from an incomplete version, Commitizen looks for the latest existing tag that matches the provided release prefix. + For example, if the current version is `1.2` and the latest `1.2.x` tag is `1.2.3`, then a patch bump yields `1.2.4` and a minor bump yields `1.3.0`. + +!!! tip + To control the behaviour of bumping and version parsing, you may implement your own `version_scheme` by inheriting from `commitizen.version_schemes.BaseVersion` or use an existing plugin package. + + +### PEP440 Version Examples + +Commitizen supports the [PEP 440][pep440] version format, which includes several version types. Here are examples of each: + +#### Standard Releases +```text +0.9.0 # Initial development release +0.9.1 # Patch release +0.9.2 # Another patch release +0.9.10 # Tenth patch release +0.9.11 # Eleventh patch release +1.0.0 # First stable release +1.0.1 # Patch release after stable +1.1.0 # Minor feature release +2.0.0 # Major version release ``` -`post` releases are not supported yet. +#### Pre-releases +```text +1.0.0a0 # Alpha release 0 +1.0.0a1 # Alpha release 1 +1.0.0b0 # Beta release 0 +1.0.0rc0 # Release candidate 0 +1.0.0rc1 # Release candidate 1 +``` -## Usage +#### Development Releases +```text +1.0.0.dev0 # Development release 0 +1.0.0.dev1 # Development release 1 +``` -![cz bump --help](../images/cli_help/cz_bump___help.svg) +#### Combined Pre-release and Development +```text +1.0.0a1.dev0 # Development release 0 of alpha 1 +1.0.0b2.dev1 # Development release 1 of beta 2 +``` + +> **Note**: `post` releases (e.g., `1.0.0.post1`) are not currently supported. + +## Command line options +![cz bump --help](../images/cli_help/cz_bump___help.svg) -### `--files-only` +### `--version-files-only` -Bumps the version in the files defined in `version_files` without creating a commit and tag on the git repository, +Bumps the version in the files defined in [`version_files`][version_files] without creating a commit and tag on the git repository. ```bash -cz bump --files-only +cz bump --version-files-only ``` ### `--changelog` -Generate a **changelog** along with the new version and tag when bumping. +Generate a **changelog** along with the new version and tag when bumping. See [changelog](./changelog.md) for more details. ```bash cz bump --changelog @@ -72,8 +141,8 @@ 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 +pre-release segment compatible with the bump's version scheme, where the segment consists 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). @@ -86,7 +155,7 @@ Note that as per [semantic versioning spec](https://semver.org/#spec-item-9) 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.0` is the currently 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 @@ -95,16 +164,21 @@ by their precedence and showcase how a release might flow through a development ### `--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. +#### `--increment-mode=linear` (default) + +Ensures that bumping pre-releases **maintains linearity**. + +Bumping 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. +#### `--increment-mode=exact` -Below are some examples that illustrate the difference in behavior: +Applies 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. + +#### Examples + +The following table illustrates the difference in behavior between the two modes: | Increment | Pre-release | Start Version | `--increment-mode=linear` | `--increment-mode=exact` | |-----------|-------------|---------------|---------------------------|--------------------------| @@ -117,16 +191,15 @@ Below are some examples that illustrate the difference in behavior: ### `--check-consistency` -Check whether the versions defined in `version_files` and the version in commitizen -configuration are consistent before bumping version. +Check whether the versions defined in [version_files][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` +For example, if we have the following configuration file `pyproject.toml`: -```toml +```toml title="pyproject.toml" [tool.commitizen] version = "1.21.0" version_files = [ @@ -135,457 +208,323 @@ version_files = [ ] ``` -`src/__version__.py`, +and the following version files `src/__version__.py` and `setup.py`: -```python +```python title="src/__version__.py" __version__ = "1.21.0" ``` -and `setup.py`. - -```python +```python title="setup.py" 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`. +When you run `cz bump --check-consistency`, Commitizen will verify that the current version in `pyproject.toml` (`1.21.0`) exists in all files listed in [version_files][version_files]. +In this example, it will detect that `setup.py` contains `1.0.5` instead of `1.21.0`, causing the bump to fail. + +!!! warning "Partial updates on failure" + If the consistency check fails, Commitizen may have already updated some files (like `pyproject.toml` and `src/__version__.py`) before detecting the inconsistency. + In this case, you'll need to restore the files to their previous state. + + To resolve this issue: + + 1. Restore the modified files to their previous state: + ```bash + git checkout . + ``` + + 2. Manually update the version in `setup.py` to match the version in `pyproject.toml`: + ```diff title="setup.py" + from setuptools import setup + + - setup(..., version="1.0.5", ...) + + setup(..., version="1.21.0", ...) + ``` -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` + 3. Run the bump command again: + ```bash + cz bump --check-consistency + ``` ### `--local-version` Bump the local portion of the version. -```bash -cz bump --local-version -``` - -For example, if we have `pyproject.toml` +For example, if we have the following configuration file `pyproject.toml`: -```toml +```toml title="pyproject.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`. +When you run `cz bump --local-version`, 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` +Create annotated tags. -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. +It is also available via configuration files. -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`). +For example, in `pyproject.toml`: -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 +```toml title="pyproject.toml" +[tool.commitizen] +annotated_tag = true ``` -### `--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. +!!! note + By default, Commitizen uses lightweight tags. -### `--retry` +### `--annotated-tag-message` -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. +Create annotated tags with the given message. -Useful to combine with code formatters, like [Prettier](https://prettier.io/). +It is also available via configuration files. -### `--major-version-zero` +For example, in `pyproject.toml`: -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. +```toml title="pyproject.toml" +[tool.commitizen] +annotated_tag_message = "Annotated tag message" +``` -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. +### `--changelog-to-stdout` -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. +Send the incremental changelog generated by `cz bump` to `stdout`. +Any other messages generated by `cz bump` will be sent to `stderr`. -### `--version-scheme` +When this flag is used, `--changelog` is implied. +However, it is recommended to set `--changelog` (or the setting `update_changelog_on_bump`) explicitly when the option `--changelog-to-stdout` is used. -Choose the version format, options: `pep440`, `semver`. +!!! note "Useful scenarios" + Pipe the newly created changelog to another tool. -Default: `pep440` + The output can be redirected to an auditing system, or used to create a GitHub Release, etc. -Recommended for python: `pep440` + ```bash + cz bump --changelog --changelog-to-stdout > body.md + ``` -Recommended for other: `semver` +### `--git-output-to-stderr` -You can also set this in the [configuration](#version_scheme) with `version_scheme = "semver"`. +Redirects git commands output to `stderr`. -[pep440][pep440] and [semver][semver] are quite similar, their difference lies in -how the prereleases look. +Useful when used with `--changelog-to-stdout` and piping the output to a file. -| 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` | +For example, `git commit` output may pollute `stdout`, so it is recommended to use this flag when piping the output to a file. -Can I transition from one to the other? +### `--retry` -Yes, you shouldn't have any issues. +If you use tools like [pre-commit](https://pre-commit.com/), you can add this flag. +It will retry the commit if it fails the first time. -### `--template` +Useful to combine with code formatters, like [Prettier](https://prettier.io/). -Provides your own changelog jinja template. -See [the template customization section](../customization.md#customizing-the-changelog-template) +### `--major-version-zero` -### `--extra` +Breaking changes do not bump the major version number. -Provides your own changelog extra variables by using the `extras` settings or the `--extra/-e` parameter. +Say you have a project with the version `0.1.x` and you commit a breaking change like this: -```bash -cz bump --changelog --extra key=value -e short="quoted value" +```text +fix(magic)!: fully deprecate whatever ``` -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. +and you run ```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. - -## 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 +cz bump --major-version-zero ``` -### Easy way +Then the version of your project will be bumped to `0.2.0` instead of `1.0.0`. -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: +!!! note + 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. -```sh -cz --no-raise 3 -``` + 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. -You can tell commitizen to skip more than one if needed: +!!! warning + This option is only compatible with projects that have major version number zero, `0.x.x` for example. -```sh -cz --no-raise 3,4,5 -``` + It fails when used with projects that have a version number greater than zero like `1.x.x`. -### Longer way + If used together with a manual version, the command also fails. -Check the list of [exit_codes](../exit_codes.md) and understand which one you have -to skip and why. + ```bash + # This fails + cz bump 0.1.0 --major-version-zero + ``` -Remember to document somewhere this, because you'll forget. +### `--gpg-sign` -For example if the system raises a `NoneIncrementExit` error, you look it up -on the list and then you can use the exit code: +Creates gpg signed tags. -```sh -cz -nr 21 bump +```bash +cz bump --gpg-sign ``` -## Configuration - -### `tag_format` - -`tag_format` and `version_scheme` are combined to make Git tag names from versions. +!!! note + By default, Commitizen uses lightweight tags. -These are used in: +### `--template` -* `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) +Provides your own changelog jinja template. +See [the template customization section](../customization/changelog_template.md) -Commitizen supports 2 types of formats, a simple and a more complex. +### `--extra` -```bash -cz bump --tag-format="v$version" -``` +Provides your own changelog extra variables by using the `extras` settings or the `--extra/-e` parameter. ```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" +cz bump --changelog --extra key=value -e short="quoted value" ``` -The variables must be preceded by a `$` sign. Default is `$version`. - -Supported variables: +See [the template customization section](../customization/changelog_template.md). -| Variable | Description | -| ------------- | ------------------------------------------- | -| `$version` | full generated version | -| `$major` | MAJOR increment | -| `$minor` | MINOR increment | -| `$patch` | PATCH increment | -| `$prerelease` | Prerelease (alpha, beta, release candidate) | -| `$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 +### `--build-metadata` -`pyproject.toml`, `.cz.toml` or `cz.toml` +Specifies additional metadata in the version string. -```toml -[tool.commitizen] -version_files = [ - "src/__version__.py", - "setup.py:version" -] +```bash +# creates a version like `1.1.2+yourmetadata`. +cz bump --build-metadata yourmetadata ``` -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 "Example usage" + - Git hash in version + - Labeling the version with additional metadata. !!! note - Files can be specified using relative (to the execution) paths, absolute paths - or glob patterns. + Commitizen ignores everything after `+` when it bumps the version. ---- + It is therefore safe to write different build-metadata between versions. -### `bump_message` -Template used to specify the commit message generated when bumping. +!!! warning + Normally, you should not use this functionality, but if you decide to do so, keep in mind that: -defaults to: `bump: version $current_version → $new_version` + - 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. -| Variable | Description | -| ------------------ | ----------------------------------- | -| `$current_version` | the version existing before bumping | -| `$new_version` | version generated after bumping | +!!! warning + This parameter is not compatible with `--local-version` as it uses the same part of the version string. -Some examples +### `--get-next` -`pyproject.toml`, `.cz.toml` or `cz.toml` +Similar to `--dry-run` but only outputs the next version. -```toml -[tool.commitizen] -bump_message = "release $current_version → $new_version [skip-ci]" +```bash +# outputs 1.0.1 if the current version is 1.0.0 and the increment is PATCH +cz bump --get-next ``` ---- +Useful for determining the next version based on CI for non-production environments/builds. -### `update_changelog_on_bump` +!!! note "Compare with `--dry-run`" + `--dry-run` provides a more detailed output including the changes as they would appear in the changelog file, while `--get-next` only outputs the next version. -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. + The following is the output of `cz bump --dry-run`: -defaults to: `false` + ```text + bump: version 3.28.0 → 3.29.0 + tag to create: v3.29.0 + increment detected: MINOR + ``` -```toml -[tool.commitizen] -update_changelog_on_bump = true -``` + The following is the output of `cz bump --get-next`: ---- + ```text + 3.29.0 + ``` -### `annotated_tag` +!!! warning + The `--get-next` flag will raise a `NoneIncrementExit` if the found commits are not eligible for a version bump. -When set to `true` commitizen will create annotated tags. + For information on how to suppress this exit, see [Ignoring Exit Codes](../exit_codes.md#ignoring-exit-codes). -```toml -[tool.commitizen] -annotated_tag = true -``` +### `--allow-no-commit` ---- +Allow the project version to be bumped even when there's no eligible version. -### `gpg_sign` +Example usage: -When set to `true` commitizen will create gpg signed tags. +```sh +# Force to bump a minor version +cz bump --increment MINOR --allow-no-commit -```toml -[tool.commitizen] -gpg_sign = true +# bump version to 2.0.0 even when there's no breaking changes or even no commits +cz bump --allow-no-commit 2.0.0 ``` ---- +!!! note "Default increment" + The increment is overridden to `PATCH` if there is no increment detected or specified. -### `major_version_zero` + In other words, `cz bump --allow-no-commit` allows you to bump the version to the next patch version even when there is no eligible commit. -When set to `true` commitizen will keep the major version at zero. -Useful during the initial development stage of your project. + ```sh + # will bump to `1.0.1` if the current version is `1.0.0`. + cz bump --allow-no-commit -Defaults to: `false` + # bump version to 2.0.0 even when there's no breaking changes or even no commits + cz bump --allow-no-commit 2.0.0 + ``` -```toml -[tool.commitizen] -major_version_zero = true -``` - ---- +### `--tag-format` -### `pre_bump_hooks` +`tag_format` and [version_scheme][version_scheme] are combined to make Git tag names from versions. -A list of optional commands that will run right _after_ updating `version_files` -and _before_ actual committing and tagging the release. +These are used in: -Useful when you need to generate documentation based on the new version. During -execution of the script, some environment variables are available: +- `cz bump`: Find previous release tag (exact match) and generate new tag. +- Find previous release tags in `cz changelog`. + - If `--incremental`: Using the 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) -| 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 | +Commitizen supports two types of formats, a simple and a more complex. -```toml -[tool.commitizen] -pre_bump_hooks = [ - "scripts/generate_documentation.sh" -] +```bash +cz bump --tag-format="v$version" ``` ---- - -### `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" -] +```bash +cz bump --tag-format="v$minor.$major.$patch$prerelease.$devrelease" ``` -### `prerelease_offset` - -Offset with which to start counting prereleses. - -Defaults to: `0` +In your configuration file: ```toml [tool.commitizen] -prerelease_offset = 1 +tag_format = "v$major.$minor.$patch$prerelease" ``` -### `version_scheme` +The variables must be preceded by a `$` sign and optionally can be wrapped in `{}`. The default is `$version`. -Choose version scheme +Supported variables: -| 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` | +| Variable | Description | +|--------------------------------|---------------------------------------------| +| `$version`, `${version}` | fully 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 | -Options: `pep440`, `semver`, `semver2` +### `--yes` -Defaults to: `pep440` +Automatically answers “yes” to all interactive prompts during the bump process, allowing the command to run without manual confirmation. -```toml -[tool.commitizen] -version_scheme = "semver" +```bash +cz bump --yes ``` -## Custom bump - -Read the [customizing section](../customization.md). - [pep440]: https://www.python.org/dev/peps/pep-0440/ [semver]: https://semver.org/ +[version_files]: ../config/bump.md#version_files diff --git a/docs/commands/changelog.md b/docs/commands/changelog.md index cbf22b15a7..da40eb6fbe 100644 --- a/docs/commands/changelog.md +++ b/docs/commands/changelog.md @@ -1,45 +1,38 @@ ## About -This command will generate a changelog following the committing rules established. +Generates 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) +!!! tip + To create the changelog automatically on bump, add the setting [update_changelog_on_bump](../config/bump.md#update_changelog_on_bump) -```toml -[tool.commitizen] -update_changelog_on_bump = true -``` + ```toml + [tool.commitizen] + update_changelog_on_bump = true + ``` ## Usage ![cz changelog --help](../images/cli_help/cz_changelog___help.svg) -### Examples - -#### Generate full changelog +## Examples ```bash +# Generate full changelog cz changelog -``` -```bash +# Or use the alias cz ch -``` - -#### Get the changelog for the given version -```bash +# Get the changelog for the given version cz changelog 0.3.0 --dry-run -``` - -#### Get the changelog for the given version range -```bash +# Get the changelog for the given version range cz changelog 0.3.0..0.4.0 --dry-run ``` -## Constrains +## Constraints -changelog generation is constrained only to **markdown** files. +Changelog generation is constrained only to **markdown** files. ## Description @@ -53,90 +46,68 @@ These are the variables used by the changelog generator. - **<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. +Creates a full block like above per version found in the tags, and a list of the commits found. +The `change_type` and `scope` are optional and don't need to be provided, +but if your regex parses them, they will be rendered. -The format followed by the changelog is the one from [keep a changelog][keepachangelog] +The format followed by the changelog is 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` | +| `date` | Date when 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` | +| `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 +| `breaking` | Whether it is a breaking change or not | `commit regex` | -### `unreleased_version` +!!! note + `message` is the only variable required to be parsed by the regex. -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. +## Command line options -If you create the changelog before bumping the version, then you -usually don't have the latest tag, and the _Unreleased_ title appears. +### `--extras` -By introducing `unreleased_version` you can prevent this situation. - -Before bumping you can run: +Provide your own changelog extra variables by using the `extras` settings or the `--extra/-e` parameter. ```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 +cz changelog --extra key=value -e short="quoted value" ``` -### `file-name` +### `--file-name` -This value can be updated in the `toml` file with the key `changelog_file` under `tools.commitizen` +This value can be updated in the configuration file with the key `changelog_file` under `tool.commitizen`. -Specify the name of the output file, remember that changelog only works with markdown. +Specify the name of the output file. Note that changelog generation only works with Markdown files. ```bash cz changelog --file-name="CHANGES.md" ``` -### `incremental` +### `--incremental` -This flag can be set in the `toml` file with the key `changelog_incremental` under `tools.commitizen` +This flag can be set in the configuration file with the key `changelog_incremental` under `tool.commitizen` Benefits: -- Build from latest version found in changelog, this is useful if you have a different changelog and want to use commitizen +- Build from the latest version found in changelog. This is useful if you have an existing changelog and want to use commitizen to extend it. - Update unreleased area -- Allows users to manually touch the changelog without being rewritten. +- Allows users to manually edit the changelog without it being completely rewritten. ```bash cz changelog --incremental ``` ```toml -[tools.commitizen] +[tool.commitizen] # ... changelog_incremental = true ``` -### `start-rev` +### `--start-rev` -This value can be set in the `toml` file with the key `changelog_start_rev` under `tools.commitizen` +This value can be set in the configuration file with the key `changelog_start_rev` under `tool.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. @@ -145,51 +116,69 @@ cz changelog --start-rev="v0.2.0" ``` ```toml -[tools.commitizen] +[tool.commitizen] # ... changelog_start_rev = "v0.2.0" ``` -### merge-prerelease +### `--merge-prerelease` -This flag can be set in the `toml` file with the key `changelog_merge_prerelease` under `tools.commitizen` +This flag can be set in the configuration file with the key `changelog_merge_prerelease` under `tool.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. +Collects changes from prereleases into the next non-prerelease version. If you have a prerelease version followed by a normal release, the changelog will show the prerelease changes as part of the normal release. If not set, prereleases will be included as separate entries in the changelog. ```bash cz changelog --merge-prerelease ``` ```toml -[tools.commitizen] +[tool.commitizen] # ... changelog_merge_prerelease = true ``` -### `template` +### `--template` + +Provide your own changelog Jinja template by using the `template` settings or the `--template` parameter. + +```bash +cz changelog --template="path/to/template.j2" +``` + +### `--unreleased-version` -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) +There is usually a chicken-and-egg situation when automatically bumping the version and creating the changelog: -### `extras` +- If you bump the version first, you have no changelog yet, and it won't be included in the release of the created version. +- If you create the changelog before bumping the version, you usually don't have the latest tag, and the _Unreleased_ title appears. -Provides your own changelog extra variables by using the `extras` settings or the `--extra/-e` parameter. +By using `--unreleased-version`, you can prevent this situation. + +Before bumping you can run: ```bash -cz changelog --extra key=value -e short="quoted value" +cz changelog --unreleased-version="v1.0.0" ``` -See [the template customization section](../customization.md#customizing-the-changelog-template) +Remember to use the tag format instead of the raw version number. + +For example, if your tag format includes a `v` prefix (e.g., `v1.0.0`), use that format. If your tag is the same as the raw version (e.g., `1.0.0`), use the raw version. + +Alternatively, you can directly bump the version and create the changelog by running: + +```bash +cz bump --changelog +``` ## 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 +- Per parsed message: Useful to add links to commits or issues +- End of changelog generation: Useful to send Slack or chat messages, or notify another department Read more about hooks in the [customization page][customization] [keepachangelog]: https://keepachangelog.com/ [semver]: https://semver.org/ -[customization]: ../customization.md +[customization]: ../customization/config_file.md diff --git a/docs/commands/check.md b/docs/commands/check.md index c31fd085ee..ef7f8f5f90 100644 --- a/docs/commands/check.md +++ b/docs/commands/check.md @@ -1,77 +1,164 @@ -# Check +This feature checks whether a string or a range of git commits follows the given committing rules. Comments in git messages will be ignored. -## 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). +To set up 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`: +More specifically, 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 +- Validate a range of git commit messages with `--rev-range` +- Validate a given string with `--message` or by piping the message to it +- Validate a commit message from a file with `--commit-msg-file` -### Git Rev Range +### Use `cz check` to validate a commit message before committing -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`. +#### Option 1: use `--message` to check a given string: ```bash -$ cz check --rev-range REV_RANGE +cz check --message <message_to_be_checked> ``` -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`. +#### Option 2: pipe the message to `cz check`: +```bash +echo <message_to_be_checked> | cz check +``` -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). +#### Option 3: use `--commit-msg-file` to read the commit message from a file +```bash +cz check --commit-msg-file /path/to/file.txt +``` -### Commit Message +## Command Line Options -There are two ways you can provide your plain message and check it. +### `--rev-range` -#### Method 1: use -m or --message +Test if a given range of commits in the git log passes `cz check`. ```bash -$ cz check --message MESSAGE +cz check --rev-range REV_RANGE ``` -In this option, MESSAGE is the commit message to be checked. +For more information on `REV_RANGE`, check the [git documentation](https://git-scm.com/book/en/v2/Git-Tools-Revision-Selection#_commit_ranges). + +#### Use cases + +1. Validate the latest 3 commit messages: + ```bash + cz check --rev-range HEAD~3..HEAD + # or + cz check --rev-range HEAD~3.. + # or + cz check --rev-range HEAD~~~.. + ``` + +1. Validate all git commit messages on some branch up to HEAD: + ```bash + cz check --rev-range <branch_name>..HEAD + ``` + + For example, to check all git commit messages on `main` branch up to HEAD: + ```bash + cz check --rev-range main..HEAD + ``` -#### Method 2: use pipe to pipe it to `cz check` + or if your project still uses `master` branch: + ```bash + cz check --rev-range master..HEAD + ``` + + !!! note "Default branch" + Usually the default branch is `main` or `master`. + You can check the default branch by running `cz check --use-default-range`. + +1. Validate all git commit messages starting from when you first implemented commit message linting: + + **(Why this is useful?)** Let's say you decided to enforce commit message today. However, it is impractical to `git rebase` all your previous commits. `--rev-range` helps you skip commits before you first implemented commit message linting by using a specific commit hash. + + ```bash + cz check --rev-range <first_commit_sha>..HEAD + ``` + +### `--use-default-range` + +Equivalent to `--rev-range <default_branch>..HEAD`. ```bash -$ echo MESSAGE | cz check +cz check --use-default-range +# or +cz check -d ``` -In this option, MESSAGE is piped to cz check and would be checked. +### `--message` -### Commit Message File +Test if a given string passes `cz check`. ```bash -$ cz check --commit-msg-file COMMIT_MSG_FILE +cz check --message <message_to_be_checked> ``` -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. +### `--commit-msg-file` -### Allow Abort +Test if a given file contains a commit message that passes `cz check`. ```bash -cz check --message MESSAGE --allow-abort +cz check --commit-msg-file <path_to_file_containing_message_to_be_checked> +``` + +This can be useful when cooperating with git hooks. Please check [Automatically check message before commit](../tutorials/auto_check.md) for more detailed examples. + +### `--allow-abort` + +Example: + +```bash +cz check --message <message_to_be_checked> --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. +permit them. Since `git commit` accepts the `--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` + +Skip validation for commit messages that start with the specified prefixes. + +If not set, commit messages starting with the following prefixes are ignored by `cz check`: + +- `Merge` +- `Revert` +- `Pull request` +- `fixup!` +- `squash!` +- `amend!` + +```bash +cz check --message <message_to_be_checked> --allowed-prefixes 'Merge' 'Revert' 'Custom Prefix' +``` + +For example, -### Allowed Prefixes +```bash +# The following message passes the check because it starts with 'Merge' +cz check --message "Merge branch 'main' into feature/new-feature" --allowed-prefixes 'Merge' + +# The following fails +cz check --message "Merge branch 'main' into feature/new-feature" --allowed-prefixes 'aaa' +``` -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!`. +### `--message-length-limit` + +Restrict the length of **the first line** of the commit message. ```bash -cz check --message MESSAGE --allowed-prefixes 'Merge' 'Revert' 'Custom Prefix' +# The following passes +cz check --message "docs(check): fix grammar issues" -l 80 + +# The following fails +cz check --message "docs:very long long long long message with many words" -l 3 ``` + +By default, the limit is set to `0`, which means no limit on the length. + +!!! note + 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 index 7760a2b88e..5e93a2274f 100644 --- a/docs/commands/commit.md +++ b/docs/commands/commit.md @@ -1,52 +1,65 @@ -![Using commitizen cli](../images/demo.gif) +## Usage -## About +![cz commit --help](../images/cli_help/cz_commit___help.svg) -In your terminal run `cz commit` or the shortcut `cz c` to generate a guided git commit. +## Overview -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). +![Using Commitizen cli](../images/cli_interactive/commit.gif) +The `commit` command provides an interactive way to create structured commits. Use either: -!!! 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). +- `cz commit` +- `cz c` (shortcut) -## Usage +By default, Commitizen uses conventional commits, but you can customize the commit rules to match your project's needs. See the [customization guide](../customization/config_file.md) for details. -![cz commit --help](../images/cli_help/cz_commit___help.svg) +## Basic Usage -### git options +### Interactive Commit Creation -`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: +Simply run `cz commit` in your terminal to start the interactive commit creation process. The command will guide you through creating a properly formatted commit message according to your configured rules. + +### Writing Messages to File + +You can save the generated commit message to a file using: +```sh +cz commit --write-message-to-file COMMIT_MSG_FILE +``` + +This can be combined with `--dry-run` to only write the message without creating a commit. This is particularly useful for [automatically preparing commit messages](../tutorials/auto_prepare_commit_message.md). + +## Advanced Features + +### Git Command Options + +You can pass any git commit options using the `--` syntax: ```sh cz commit <commitizen-args> -- <git-cli-args> -# e.g., cz commit --dry-run -- -a -S +# Examples: +cz c --dry-run -- -a -S # Stage all changes and sign the commit +cz c -a -- -n # Stage all changes and skip the pre-commit and commit-msg hooks ``` -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. +!!! warning + The `--signoff` option (or `-s`) is now recommended being used with the new syntax: `cz commit -- -s`. The old syntax `cz commit --signoff` is deprecated and will be removed in v5. ### 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. +- Use `cz commit --retry` to reuse the last commit message after a failed commit attempt +- Set `retry_after_failure: true` in your configuration to automatically retry +- Use `cz commit --no-retry` to force a new commit message prompt + +### Message Length Control -### Commit message length limit +Control the length of your commit messages using the `-l` or `--message-length-limit` option: +```sh +cz commit -l 72 # Limits message length to 72 characters +``` + +!!! note + The length limit only applies to the first line of the commit message. For conventional commits, this means the limit applies from the type of change through the subject. The body and footer are not counted. -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. +## Technical Notes -**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. +For platform compatibility, the `commit` command disables ANSI escaping in its output. This means pre-commit hooks coloring will be deactivated as discussed in [commitizen-tools/commitizen#417](https://github.com/commitizen-tools/commitizen/issues/417). diff --git a/docs/commands/init.md b/docs/commands/init.md index 01e1db6be8..122e1bef5f 100644 --- a/docs/commands/init.md +++ b/docs/commands/init.md @@ -1,27 +1,68 @@ +The `cz init` command helps you set up Commitizen in your project by creating a configuration file with your preferred settings. + ## Usage ![cz init --help](../images/cli_help/cz_init___help.svg) -## Example +## Command + +```sh +cz init +``` + +## Interactive Configuration + +When you run `cz init`, Commitizen will guide you through an interactive setup process: + +![init](../images/cli_interactive/init.gif) -To start using commitizen, the recommended approach is to run +## Configuration File + +The initialization process will create a configuration file in your project root. + +See [Configuration File][configuration_file] for more details. + +## Configuration Options + +During the initialization process, you'll be prompted to configure the following settings: + +1. **Convention Rules**: Select the commit message convention to follow (e.g., conventional commits) +2. **Version Provider**: Choose how to manage versioning in your project. Commitizen supports multiple version management systems: + - `commitizen`: Uses Commitizen's built-in version management system + - `npm`: Manages version in `package.json` for Node.js projects + - `cargo`: Manages version in `Cargo.toml` for Rust projects + - `composer`: Manages version in `composer.json` for PHP projects + - `pep621`: Uses `pyproject.toml` with PEP 621 standard + - `poetry`: Uses `pyproject.toml` with Poetry configuration + - `uv`: Uses `pyproject.toml` and `uv.lock` for Python projects + - `scm`: Reads version directly from git tags without modifying files +3. **Project Version**: The current version of your project will be detected automatically +4. **Tag Format**: The format used for version tags in your repository +5. **Version Type**: Choose between: + - `semver` or `semver2`: Semantic Versioning (MAJOR.MINOR.PATCH) + - `pep440`: Python Package Versioning +6. **Changelog Generation**: Configure whether to automatically generate changelog during version bumps +7. **Alpha Versioning**: Option to keep major version at 0 for alpha/beta software +8. **Pre-commit Hooks**: Set up Git pre-commit hooks for automated commit message validation + +See [Configuration Options][configuration_options] for more details. + +## Example ```sh +# Start the initialization process cz init + +# Follow the interactive prompts to configure your project ``` -![init](../images/init.gif) +## Next Steps -This command will ask you for information about the project and will -configure the selected file type (`pyproject.toml`, `.cz.toml`, etc.). +After initialization, you can: -The `init` will help you with +1. Start using [`cz commit`](./commit.md) to create conventional commits +2. Use [`cz bump`](./bump.md) to manage versioning +3. Configure additional settings in your project's [configuration_file][configuration_file] -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. +[configuration_file]: ../config/configuration_file.md +[configuration_options]: ../config/option.md diff --git a/docs/commands/version.md b/docs/commands/version.md index 9a8176b45f..4d2e6a0323 100644 --- a/docs/commands/version.md +++ b/docs/commands/version.md @@ -1,4 +1,4 @@ -Get the version of the installed commitizen or the current project (default: installed commitizen) +Get the version of the installed Commitizen or the current project (default: installed commitizen) ## Usage diff --git a/docs/config.md b/docs/config.md deleted file mode 100644 index 210b5d7ff8..0000000000 --- a/docs/config.md +++ /dev/null @@ -1,403 +0,0 @@ -# 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] - -### `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 | -| `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/config/bump.md b/docs/config/bump.md new file mode 100644 index 0000000000..10ca5bcf8d --- /dev/null +++ b/docs/config/bump.md @@ -0,0 +1,217 @@ +# Bump Options + +<!-- When adding a new option, please keep the alphabetical order. --> + +## `annotated_tag` + +When set to `true`, `cz bump` is equivalent to `cz bump --annotated-tag`. + +```toml title="pyproject.toml" +[tool.commitizen] +annotated_tag = true +``` + +## `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 | + +```toml title="pyproject.toml" +[tool.commitizen] +bump_message = "release $current_version → $new_version [skip-ci]" +``` + +## `gpg_sign` + +When set to `true`, `cz bump` is equivalent to `cz bump --gpg-sign`. See [`--gpg-sign`](../commands/bump.md#-gpg-sign). + +```toml title="pyproject.toml" +[tool.commitizen] +gpg_sign = true +``` + +## `ignored_tag_formats` + +- Type: `list` +- Default: `[]` + +Tags matching those formats will be totally ignored and won't raise a warning. +Each entry uses the syntax as [`tag_format`](#tag_format) with the addition of `*` that will match everything (non-greedy). + +## `major_version_zero` + +When set to `true`, `cz bump` is equivalent to `cz bump --major-version-zero`. See [`--major-version-zero`](../commands/bump.md#-major-version-zero). + +```toml title="pyproject.toml" +[tool.commitizen] +major_version_zero = true +``` + +## `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 uses the syntax as `tag_format`. + +## `pre_bump_hooks` + +A list of optional commands that will run right *after* updating [`version_files`](#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 `PATCH` release | +| `CZ_PRE_CHANGELOG_FILE_NAME` | Path to the changelog file, if available | + +```toml title="pyproject.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 `PATCH` release | +| `CZ_POST_CHANGELOG_FILE_NAME` | Path to the changelog file, if available | + +```toml title="pyproject.toml" +[tool.commitizen] +post_bump_hooks = [ + "scripts/slack_notification.sh" +] +``` + +## `prerelease_offset` + +Offset with which to start counting prereleases. + +If not specified, defaults to `0`. + +```toml title="pyproject.toml" +[tool.commitizen] +prerelease_offset = 1 +``` + +!!! note + Under some circumstances, a prerelease cannot start with `0`-for example, in embedded projects where individual characters are encoded as bytes. You can specify an offset from which to start counting. + +## `tag_format` + +See [`--tag-format`](../commands/bump.md#-tag-format). + +## `update_changelog_on_bump` + +When set to `true`, `cz bump` is equivalent to `cz bump --changelog`. + +```toml title="pyproject.toml" +[tool.commitizen] +update_changelog_on_bump = true +``` + +## `version_files` + +Identify the files or glob patterns which should be updated with the new version. + +Commitizen will update its configuration file automatically when bumping, +regardless of whether the file is present or not in `version_files`. + +You may specify the `version_files` in your configuration file. + +```toml title="pyproject.toml" +[tool.commitizen] +version_files = [ + "src/__version__.py", +] +``` + +It is also possible to provide a pattern for each file, separated by a colon (e.g. `file:pattern`). See the below example for more details. + +```toml title="pyproject.toml" +[tool.commitizen] +version_files = [ + "packages/*/pyproject.toml:version", + "setup.json:version", +] +``` + +!!! note "Example scenario" + We have a project with the following configuration file `pyproject.toml`: + + ```toml title="pyproject.toml" + [tool.commitizen] + version_files = [ + "src/__version__.py", + "packages/*/pyproject.toml:version", + "setup.json:version", + ] + ``` + + For the reference `"setup.json:version"`, it means that it will look for a file `setup.json` and will only change the lines that contain the substring `"version"`. + + For example, if the content of `setup.json` is: + + <!-- DEPENDENCY: repeated_version_number.json --> + + ```json title="setup.json" + { + "name": "magictool", + "version": "1.2.3", + "dependencies": { + "lodash": "1.2.3" + } + } + ``` + + After running `cz bump 2.0.0`, its content will be updated to: + + ```diff title="setup.json" + { + "name": "magictool", + - "version": "1.2.3", + + "version": "2.0.0", + "dependencies": { + "lodash": "1.2.3" + } + } + ``` + +!!! note + Files can be specified using relative (to the execution) paths, absolute paths, or glob patterns. + +!!! note "Historical note" + This option was renamed from `files` to `version_files`. + +## `version_scheme` + +See [`--version-scheme`](../commands/bump.md#-version-scheme). diff --git a/docs/config/changelog.md b/docs/config/changelog.md new file mode 100644 index 0000000000..86d4363105 --- /dev/null +++ b/docs/config/changelog.md @@ -0,0 +1,8 @@ +# Changelog Options + +<!-- When adding a new option, please keep the alphabetical order. --> +<!-- If there is a new configuration option that doesn't have a corresponding command line option, please add it here. --> + +As for now, each of the options that is used by `cz changelog` command can correlate to a command line option. + +See [changelog command line options](../commands/changelog.md#command-line-options) for more details. diff --git a/docs/config/check.md b/docs/config/check.md new file mode 100644 index 0000000000..2c8dda27b4 --- /dev/null +++ b/docs/config/check.md @@ -0,0 +1,27 @@ +# Check Options + +<!-- When adding a new option, please keep the alphabetical order. --> + +## `allow_abort` + +- Type: `bool` +- Default: `False` + +Disallow empty commit messages. Useful in CI. + +## `allowed_prefixes` + +- Type: `list` +- Default: `["Merge", "Revert", "Pull request", "fixup!", "squash!"]` + +List of prefixes that commitizen ignores when verifying messages. + +## `message_length_limit` + +- Type: `int` +- Default: `0` (no limit) + +Maximum length of the commit message. Setting it to `0` disables the length limit. + +!!! note + This option can be overridden by the `-l/--message-length-limit` command line argument. diff --git a/docs/config/commit.md b/docs/config/commit.md new file mode 100644 index 0000000000..72fdff947c --- /dev/null +++ b/docs/config/commit.md @@ -0,0 +1,26 @@ +# Commit Options + +<!-- When adding a new option, please keep the alphabetical order. --> + +## `breaking_change_exclamation_in_title` + +- Type: `bool` +- Default: `False` + +When true, breaking changes will be also indicated by an exclamation mark in the commit title (e.g., `feat!: breaking change`). + +When false, breaking changes will be only indicated by `BREAKING CHANGE:` in the footer. See [writing commits](../tutorials/writing_commits.md) for more details. + +## `encoding` + +- Type: `str` +- Default: `"utf-8"` + +Sets the character encoding to be used when parsing commit messages. + +## `retry_after_failure` + +- Type: `bool` +- Default: `False` + +Retries failed commit when running `cz commit`. diff --git a/docs/config/configuration_file.md b/docs/config/configuration_file.md new file mode 100644 index 0000000000..172cbce1a3 --- /dev/null +++ b/docs/config/configuration_file.md @@ -0,0 +1,247 @@ +# Configuration File + +Commitizen uses configuration files to customize its behavior for your project. These files define settings such as which commit rules to use, version management preferences, changelog generation options, and more. + +## Creating a Configuration File + +It is recommended to create a configuration file via our [`cz init`](../commands/init.md) command. This command will guide you through setting up your configuration file with the appropriate settings for your project. + +## File Location and Search Order + +Configuration files are typically located in the root of your project directory. Commitizen searches for configuration files in the following order: + +<!-- DEPENDENCY: commitizen/defaults.py CONFIG_FILES --> + +1. `.cz.toml` +2. `cz.toml` +3. `.cz.json` +4. `cz.json` +5. `.cz.yaml` +6. `cz.yaml` +7. `pyproject.toml` (in the `[tool.commitizen]` section) + +The first valid configuration file found will be used. If no configuration file is found, Commitizen will use its default settings. + +!!! note + Commitizen supports explicitly specifying a configuration file using the `--config` option, which is useful when the configuration file is not located in the project root directory. + When `--config` is provided, Commitizen will only load configuration from the specified file and will not search for configuration files using the default search order described above. If the specified configuration file does not exist, Commitizen raises the `ConfigFileNotFound` error. If the specified configuration file exists but is empty, Commitizen raises the `ConfigFileIsEmpty` error. + + ```bash + cz --config <PATH> <command> + ``` + +!!! tip + For Python projects, you can add your Commitizen configuration to `pyproject.toml` to keep all project configuration in one place. + +!!! warning "Multiple Configuration Files" + If Commitizen detects more than one configuration file in your project directory (excluding `pyproject.toml`), it will display a warning message and identify which file is being used. To avoid confusion, ensure you have only one Commitizen configuration file in your project. + +## Supported Formats + +Commitizen supports three configuration file formats: + +- **TOML** (`.toml`) - Recommended for Python projects +- **JSON** (`.json`) +- **YAML** (`.yaml`) + +All formats support the same configuration options. Choose the format that best fits your project's ecosystem. + +## Configuration Structure + +=== "TOML Format" + + For TOML files, Commitizen settings are placed under the `[tool.commitizen]` section. If you're using a standalone `.cz.toml` or `cz.toml` file, you can use `[tool.commitizen]` or just `[commitizen]`. + + **Example: `pyproject.toml`, `.cz.toml` or `cz.toml`** + + ```toml title="pyproject.toml" + [tool.commitizen] + name = "cz_conventional_commits" + version = "0.1.0" + version_provider = "commitizen" + version_scheme = "pep440" + version_files = [ + "src/__version__.py", + "pyproject.toml:version" + ] + tag_format = "$version" + update_changelog_on_bump = true + changelog_file = "CHANGELOG.md" + changelog_incremental = false + bump_message = "bump: version $current_version → $new_version" + gpg_sign = false + annotated_tag = false + major_version_zero = false + prerelease_offset = 0 + retry_after_failure = false + allow_abort = false + message_length_limit = 0 + allowed_prefixes = [ + "Merge", + "Revert", + "Pull request", + "fixup!", + "squash!", + "amend!" + ] + breaking_change_exclamation_in_title = false + use_shortcuts = false + pre_bump_hooks = [] + post_bump_hooks = [] + encoding = "utf-8" + + # Optional: Custom styling for prompts + 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"] + ] + ``` + +=== "JSON Format" + + For JSON files, Commitizen settings are placed under the `commitizen` key. + + **Example: `.cz.json` or `cz.json`** + + ```json title=".cz.json" + { + "commitizen": { + "name": "cz_conventional_commits", + "version": "0.1.0", + "version_provider": "commitizen", + "version_scheme": "pep440", + "version_files": [ + "src/__version__.py", + "pyproject.toml:version" + ], + "tag_format": "$version", + "update_changelog_on_bump": true, + "changelog_file": "CHANGELOG.md", + "changelog_incremental": false, + "bump_message": "bump: version $current_version → $new_version", + "gpg_sign": false, + "annotated_tag": false, + "major_version_zero": false, + "prerelease_offset": 0, + "retry_after_failure": false, + "allow_abort": false, + "message_length_limit": 0, + "allowed_prefixes": [ + "Merge", + "Revert", + "Pull request", + "fixup!", + "squash!", + "amend!" + ], + "breaking_change_exclamation_in_title": false, + "use_shortcuts": false, + "pre_bump_hooks": [], + "post_bump_hooks": [], + "encoding": "utf-8", + "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"] + ] + } + } + ``` + +=== "YAML Format" + + For YAML files, Commitizen settings are placed under the `commitizen` key. + + **Example: `.cz.yaml` or `cz.yaml`** + + ```yaml title=".cz.yaml" + commitizen: + name: cz_conventional_commits + version: "0.1.0" + version_provider: commitizen + version_scheme: pep440 + version_files: + - src/__version__.py + - pyproject.toml:version + tag_format: "$version" + update_changelog_on_bump: true + changelog_file: CHANGELOG.md + changelog_incremental: false + bump_message: "bump: version $current_version → $new_version" + gpg_sign: false + annotated_tag: false + major_version_zero: false + prerelease_offset: 0 + retry_after_failure: false + allow_abort: false + message_length_limit: 0 + allowed_prefixes: + - Merge + - Revert + - Pull request + - fixup! + - squash! + - amend! + breaking_change_exclamation_in_title: false + use_shortcuts: false + pre_bump_hooks: [] + post_bump_hooks: [] + encoding: utf-8 + 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 + ``` + +## Configuration Options + +For a complete list of all available configuration options and their descriptions, see the [Configuration Settings](../config/option.md) documentation. + +Key configuration categories include: + +- **Commit Rules**: `name` - Select which commit convention to use +- **Version Management**: `version`, `version_provider`, `version_scheme`, `version_files` +- **Tagging**: `tag_format`, `legacy_tag_formats`, `ignored_tag_formats`, `gpg_sign`, `annotated_tag` +- **Changelog**: `changelog_file`, `changelog_format`, `changelog_incremental`, `update_changelog_on_bump` +- **Bumping**: `bump_message`, `major_version_zero`, `prerelease_offset`, `pre_bump_hooks`, `post_bump_hooks` +- **Commit Validation**: `allowed_prefixes`, `message_length_limit`, `allow_abort`, `retry_after_failure` +- **Customization**: `customize`, `style`, `use_shortcuts`, `template`, `extras` + +## Customization + +For advanced customization, including creating custom commit rules, see the [Customization](../customization/config_file.md) documentation. + +!!! note + The `customize` option is supported in TOML, JSON, and YAML configuration files. For Python projects, adding it to `pyproject.toml` keeps all project configuration in one place. diff --git a/docs/config/option.md b/docs/config/option.md new file mode 100644 index 0000000000..4109f3ff7b --- /dev/null +++ b/docs/config/option.md @@ -0,0 +1,52 @@ +# Misc Options + +## `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"`. + +## `style` + +- Type: `list` +- Default: `[]` + +Style for the prompts (It will merge this value with default style.) See [Styling your prompts with your favorite colors](https://github.com/tmbo/questionary#additional-features) for more details. + +## `customize` + +- Type: `dict` +- Default: `None` + +**Supported in TOML, JSON, and YAML configuration files.** + +Custom rules for committing and bumping. See [customization](../customization/config_file.md) for more details. + +## `use_shortcuts` + +- Type: `bool` +- Default: `False` + +Show keyboard shortcuts when selecting from a list. When enabled, each choice shows a shortcut key; press that key or use the arrow keys to select. + +Example: + +```toml title="pyproject.toml" +[tool.commitizen] +name = "cz_conventional_commits" +use_shortcuts = true +``` + +Run `cz commit` to see shortcut keys on each choice. + +![Menu with shortcut keys](../images/cli_interactive/shortcut_default.gif) + +To customize which key is used for each choice (via the `key` field when using `cz_customize`), see [shortcut keys customization](../customization/config_file.md#shortcut-keys). diff --git a/docs/config/version_provider.md b/docs/config/version_provider.md new file mode 100644 index 0000000000..ccbbe6cfd3 --- /dev/null +++ b/docs/config/version_provider.md @@ -0,0 +1,335 @@ +# Version Providers + +Version providers are the mechanism by which Commitizen reads and writes version information in your project. + +They abstract away the details of where and how version numbers are stored, allowing Commitizen to work seamlessly with different project types and package management systems. + +## Overview + +By default, Commitizen uses the `commitizen` provider, which stores the version in your Commitizen configuration file. +However, you can configure Commitizen to use any available provider that matches your project's setup. +This is particularly useful when you want Commitizen to manage versions in the same location as your package manager (e.g., `package.json` for Node.js projects, `pyproject.toml` for Python projects). + +## Built-in Providers + +Commitizen includes several built-in version providers for common package management formats: + +### `commitizen` (Default) + +The default version provider stores and retrieves the version from your Commitizen configuration file (e.g., `pyproject.toml`, `.cz.toml`, etc.). + +**Use when:** + +- You want to keep version management separate from your package manager +- Your project doesn't use a standard package manager +- You need maximum flexibility in version management + +**Configuration:** +```toml +[tool.commitizen] +version_provider = "commitizen" +version = "0.1.0" # Required when using this provider +``` + +### `scm` + +Fetches the version from Git tags using `git describe`. This provider **only reads** version information and never writes it back to files. It's designed to work with tools like `setuptools-scm` or other package manager `*-scm` plugins that derive version numbers from Git history. + +**Use when:** + +- You're using `setuptools-scm` or similar tools +- You want version numbers derived from Git tags +- You don't want Commitizen to modify any files for version management + +**Configuration:** +```toml +[tool.commitizen] +version_provider = "scm" +# No version field needed - it's read from Git tags +``` + +!!! note + The `scm` provider is read-only. When you run `cz bump`, it will create a Git tag but won't update any files. This is intentional and works well with tools that derive versions from Git tags. + +### `pep621` + +Manages version in `pyproject.toml` under the `project.version` field, following [PEP 621](https://peps.python.org/pep-0621/) standards. + +**Use when:** + +- You're using a modern Python project with PEP 621-compliant `pyproject.toml` +- You want version management integrated with your Python project metadata + +**Configuration:** +```toml +[tool.commitizen] +version_provider = "pep621" +``` + +**Example `pyproject.toml`:** +```toml +[project] +name = "my-package" +version = "0.1.0" # Managed by Commitizen +``` + +### `poetry` + +Manages version in `pyproject.toml` under the `tool.poetry.version` field, which is used by the [Poetry](https://python-poetry.org/) package manager. This approach is recommended only for users running Poetry versions earlier than 2.0 or relying on Poetry-specific features. For most users on Poetry 2.0 or later, it is recommended to use `pep621` instead. [Read More](https://python-poetry.org/docs/main/managing-dependencies/) + +**Use when:** + +- You're using Poetry < 2.0 as your Python package manager +- You're using Poetry >= 2.0 as your Python package manager, but don't need poetry-specific features +- You want Commitizen to manage the version that Poetry uses + +**Configuration:** +```toml +[tool.commitizen] +version_provider = "poetry" +``` + +**Example `pyproject.toml`:** +```toml +[tool.poetry] +name = "my-package" +version = "0.1.0" # Managed by Commitizen +``` + +### `uv` + +Manages version in both `pyproject.toml` (`project.version`) and `uv.lock` (`package.version` for the matching package name). This ensures consistency between your project metadata and lock file. + + +!!! note + Even though uv follows PEP 621 format, `pep621` does not manage the version in `uv.lock`. `uv` is still suggested for uv users. + +**Use when:** + +- You're using `uv` as your Python package manager +- You want version synchronization between `pyproject.toml` and `uv.lock` + +**Configuration:** +```toml +[tool.commitizen] +version_provider = "uv" +``` + +### `cargo` + +Manages version in both `Cargo.toml` (`package.version`) and `Cargo.lock` (`package.version` for the matching package name). This ensures consistency between your Rust project's manifest and lock file. + +**Use when:** + +- You're working with a Rust project using Cargo +- You want Commitizen to manage Rust package versions + +**Configuration:** +```toml +[tool.commitizen] +version_provider = "cargo" +``` + +**Example `Cargo.toml`:** +```toml +[package] +name = "my-crate" +version = "0.1.0" # Managed by Commitizen +``` + +### `npm` + +Manages version in `package.json` and optionally synchronizes with `package-lock.json` and `npm-shrinkwrap.json` if they exist. + +**Use when:** + +- You're working with a Node.js/JavaScript project +- You want Commitizen to manage npm package versions + +**Configuration:** +```toml +[tool.commitizen] +version_provider = "npm" +``` + +**Example `package.json`:** +```json +{ + "name": "my-package", + "version": "0.1.0" +} +``` + +### `composer` + +Manages version in `composer.json` under the `version` field, used by PHP's Composer package manager. + +**Use when:** + +- You're working with a PHP project using Composer +- You want Commitizen to manage Composer package versions + +**Configuration:** +```toml +[tool.commitizen] +version_provider = "composer" +``` + +**Example `composer.json`:** +```json +{ + "name": "vendor/package", + "version": "0.1.0" +} +``` + +## Provider Comparison Table + +| Provider | File(s) Modified | Read-Only | Best For | +| ------------ | ----------------------------------- | --------- | --------------------------------- | +| `commitizen` | Commitizen config file | No | General use, flexible projects | +| `scm` | None (reads from Git tags) | Yes | `setuptools-scm` users | +| `pep621` | `pyproject.toml` (`project.version`) | No | Modern Python (PEP 621) | +| `poetry` | `pyproject.toml` (`tool.poetry.version`) | No | Poetry projects | +| `uv` | `pyproject.toml` + `uv.lock` | No | uv package manager | +| `cargo` | `Cargo.toml` + `Cargo.lock` | No | Rust/Cargo projects | +| `npm` | `package.json` (+ lock files) | No | Node.js/npm projects | +| `composer` | `composer.json` | No | PHP/Composer projects | + +## Creating Custom Version Providers + +If none of the built-in providers meet your needs, you can create a custom version provider by extending the `VersionProvider` base class and registering it as a plugin. + +### Step 1: Create Your Provider Class + +Create a Python file (e.g., `my_provider.py`) that extends `VersionProvider`: + +```python title="my_provider.py" +from pathlib import Path +from commitizen.providers import VersionProvider + + +class MyProvider(VersionProvider): + """ + Custom version provider that reads/writes from a VERSION file. + """ + + def get_version(self) -> str: + """Read version from VERSION file.""" + version_file = Path("VERSION") + if not version_file.is_file(): + return "0.0.0" + return version_file.read_text().strip() + + def set_version(self, version: str) -> None: + """Write version to VERSION file.""" + version_file = Path("VERSION") + version_file.write_text(f"{version}\n") +``` + +### Step 2: Register as an Entry Point + +Register your provider using the `commitizen.provider` entry point. You can do this in your `setup.py`, `setup.cfg`, or `pyproject.toml`: + +**Using `pyproject.toml` (recommended):** + +```toml title="pyproject.toml" +[project] +name = "my-commitizen-provider" +version = "0.1.0" +dependencies = ["commitizen"] + +[project.entry-points."commitizen.provider"] +my-provider = "my_provider:MyProvider" +``` + +**Using `setup.py` (for legacy setup):** + +```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", + ] + }, +) +``` + +### Step 3: Install and Use + +1. Install your provider package: + + - Once your custom Commitizen provider is packaged and published (for example, to PyPI), install it like any standard Python package: + + ```bash + pip install my-commitizen-provider + ``` + + - If you want to use the provider directly from the current project source (during development), install it in editable mode ([See pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-e)): + + ```bash + pip install -e . + ``` + +2. Configure Commitizen to use your provider: + ```toml + [tool.commitizen] + version_provider = "my-provider" + ``` + +### Provider Implementation Guidelines + +When creating a custom provider, keep these guidelines in mind: + +- **`get_version()`** should return a string representing the current version. If no version is found, you can return `"0.0.0"` or raise an appropriate exception. +- **`set_version(version: str)`** should write the version to your chosen storage location. The version string will be properly formatted according to your `version_scheme` setting. +- The provider has access to `self.config`, which is a `BaseConfig` instance containing all Commitizen settings. +- For file-based providers, consider using the `FileProvider` or `JsonProvider`/`TomlProvider` base classes from `commitizen.providers.base_provider` to simplify implementation. + +### Example: JSON-based Provider + +Here's a more complete example using the `JsonProvider` base class: + +```python title="json_version_provider.py" +from commitizen.providers.base_provider import JsonProvider + + +class JsonVersionProvider(JsonProvider): + """ + Version provider that uses a custom version.json file. + """ + + filename = "version.json" + + def get(self, document): + """Extract version from JSON document.""" + return document["version"] + + def set(self, document, version): + """Set version in JSON document.""" + document["version"] = version +``` + +This example leverages the `JsonProvider` base class, which handles file reading, writing, and JSON parsing automatically. + +## Choosing the Right Provider + +Select a version provider based on your project's characteristics: + +- **Python projects** + - **with `uv`**: Use `uv` + - **with `pyproject.toml` that follows PEP 621**: Use `pep621` + - **with Poetry**: Use `poetry` + - **setuptools-scm**: Use `scm` +- **Rust projects**: Use `cargo` +- **Node.js projects**: Use `npm` +- **PHP projects**: Use `composer` +- **Other cases or custom needs**: Use `commitizen` (default) or create a custom provider + +Remember that you can always use `version_files` in combination with any provider to update additional files during version bumps, regardless of which provider you choose for reading/writing the primary version. diff --git a/docs/contributing.md b/docs/contributing.md deleted file mode 100644 index a41843d753..0000000000 --- a/docs/contributing.md +++ /dev/null @@ -1,108 +0,0 @@ -## 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/) `1.2.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 run pre-commit install`) -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 `./scripts/format` and `./scripts/test` to ensure you follow the coding style and the tests pass. -9. Optionally, update the `./docs/README.md`. -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 run mkdocs serve` 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/contributing/contributing.md b/docs/contributing/contributing.md new file mode 100644 index 0000000000..446410533c --- /dev/null +++ b/docs/contributing/contributing.md @@ -0,0 +1,157 @@ +# Contributing to Commitizen + +First, thank you for taking the time to contribute! 🎉 Your contributions help make Commitizen better for everyone. + +When contributing to Commitizen, we encourage you to: + +1. First, check out the [issues](https://github.com/commitizen-tools/commitizen/issues) and [Features we won't add](../features_wont_add.md) to see if there's already a discussion about the change you wish to make. +2. If there's no discussion, [create an issue](https://github.com/commitizen-tools/commitizen/issues/new) to discuss your proposed changes. +3. Follow our [development workflow](#development-workflow) and guidelines below. + +If you're a first-time contributor, please check out issues labeled [good first issue](https://github.com/commitizen-tools/commitizen/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - these are specifically chosen to be beginner-friendly. + +## Prerequisites & Setup + +### Required Tools + +1. **Python Environment** + - Python `>=3.10` + - [uv](https://docs.astral.sh/uv/getting-started/installation/) `>=0.9.0` +2. **Version Control & Security** + - Git + - Commitizen + - [GPG](https://gnupg.org) for commit signing + - [Installation page](https://gnupg.org/documentation/manuals/gnupg/Installation.html#Installation) + - For Mac users: `brew install gnupg` + - For Windows users: Download from [Gpg4win](https://www.gpg4win.org/) + - For Linux users: Use your distribution's package manager (e.g., `apt install gnupg` for Ubuntu) + +### Getting Started + +1. Fork [Commitizen](https://github.com/commitizen-tools/commitizen) +2. Clone your fork: + ```bash + git clone https://github.com/YOUR_USERNAME/commitizen.git + cd commitizen + ``` +3. Add the upstream repository: + ```bash + git remote add upstream https://github.com/commitizen-tools/commitizen.git + ``` +4. Set up the development environment: + ```bash + uv sync --dev --frozen + ``` +5. Set up pre-commit hooks: + ```bash + uv run poe setup-pre-commit + ``` + +## Development Workflow + +1. **Create a New Branch** + ```bash + git switch -c feature/your-feature-name + # or + git switch -c fix/your-bug-fix + ``` +2. **Make Your Changes** + - Write your code + - Add tests for new functionalities or fixes + - Update documentation if needed + - Follow the existing code style +3. **Testing** + - Run the full test suite: `uv run poe all` + - Ensure test coverage doesn't drop (we use [CodeCov](https://app.codecov.io/gh/commitizen-tools/commitizen)) + - For documentation changes, run `uv run poe doc` to check for warnings/errors + - If you need to change some file regression snapshots, run: `uv run poe test:regen` +4. **Committing Changes** + - Use Commitizen to make commits (we follow [conventional commits](https://www.conventionalcommits.org/)) + - Example: `cz commit` +5. **Documentation** + - Update `docs/README.md` if needed + - For CLI help screenshots: `uv run poe doc:screenshots` + - Prefer [Google style documentation](https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings), which works well with editors like VSCode and PyCharm + - **DO NOT** update `CHANGELOG.md` (automatically generated) + - **DO NOT** update version numbers (automatically handled) +6. **Pull Request** + - Push your changes: `git push origin your-branch-name` + - Create a pull request on GitHub + - Ensure CI checks pass + - Wait for review and address any feedback + +## 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 +``` diff --git a/docs/contributing/contributing_tldr.md b/docs/contributing/contributing_tldr.md new file mode 100644 index 0000000000..af350d2bb7 --- /dev/null +++ b/docs/contributing/contributing_tldr.md @@ -0,0 +1,39 @@ +# Contributing TL;DR + +Feel free to send a PR to update this file if you find anything useful. 🙇 + +## Environment + +- Python `>=3.10` +- [uv](https://docs.astral.sh/uv/getting-started/installation/) `>=0.9.0` + +## Useful commands + +Please check the [pyproject.toml](https://github.com/commitizen-tools/commitizen/blob/master/pyproject.toml) for a comprehensive list of commands. + +### Code Changes + +```bash +# Ensure you have the correct dependencies +uv sync --dev --frozen + +# Make ruff happy +uv run poe format + +# Check if ruff and mypy are happy +uv run poe lint + +# Check if mypy is happy in python 3.10 +mypy --python-version 3.10 + +# Run tests in parallel. +pytest -n auto # This may take a while. +pytest -n auto <test_suite> +``` + +### Documentation Changes + +```bash +# Build the documentation locally and check for broken links +uv run poe doc +``` diff --git a/docs/contributing/pull_request.md b/docs/contributing/pull_request.md new file mode 100644 index 0000000000..4682496b4c --- /dev/null +++ b/docs/contributing/pull_request.md @@ -0,0 +1,71 @@ +# Pull Request Guidelines + +This document outlines important guidelines to follow when creating a pull request. + +## Before Creating a Pull Request + +1. **Check Existing Issues**: Ensure there's a related issue or discussion for your changes. If not, create one first to discuss the proposed changes. +2. **Follow the Development Workflow**: Make sure you've followed all steps in the [contributing guidelines](contributing.md). + +## Making Changes + +When adding new code, match the existing coding style in the file you're modifying. Consistency within a file and throughout the project is crucial for maintainability. + +## Following PR Description Template Instructions + +The PR description template includes a checklist and specific requirements. Please: + +1. **Complete the Checklist**: Check off all applicable items before requesting review. +2. **Provide Clear Descriptions**: Clearly describe what the change does, expected behavior, and steps to test. +3. **Respond to Feedback**: Carefully read maintainer feedback and address all points raised. Ask for clarification if something is unclear. + +## AI-Assisted Contributions + +We welcome contributions that use AI tools for assistance, but we have strict quality standards to maintain code quality and avoid "AI spaghetti code." + +!!! note + Most of our new documentation changes are, of course, generated by AI, but we still need to review it and make sure it's correct. + +![when bro's code is filled with "🔥 🚀 💥 ❌ ✅"](https://images3.memedroid.com/images/UPLOADED78/69501f1c23cab.webp) + +### Guidelines for AI-Assisted PRs + +1. **Review and Refine**: Thoroughly review and understand all AI-generated code. Refactor to match our project's style and remove unnecessary complexity. +2. **Test Thoroughly**: Don't assume AI-generated code works—test it extensively with comprehensive test cases and manual testing when possible. +3. **Understand the Code**: You should be able to explain every line. If you don't understand something, learn about it or rewrite it. Maintainers may ask you to explain your code. +4. **Avoid Common Pitfalls**: + - Over-engineering (simplify when possible) + - Inconsistent style (ensure consistency with our standards) + - Unnecessary dependencies + - Copy-paste without adaptation + - Verbose or obvious comments +5. **Code Quality Still Matters**: AI assistance doesn't excuse poor code quality. All code must pass linting, type checking, and follow best practices. +6. **Be Transparent**: **Mention AI assistance in the PR description**. Code quality standards remain the same regardless of how the code was written. + +!!! warning "Consequences of Poor AI-Assisted Contributions" + Maintainers who identify low-quality AI-generated code or copy-pasted responses will have no choice but to close the related PRs. This adds unnecessary burden on maintainers and doesn't help the project. Additionally, the contributor's reputation is impacted as maintainers may lose confidence and might restrict the user from making further contributions. + +### What We Consider "AI Spaghetti" + +Red flags that may result in PR rejection or requests for significant refactoring: + +- Overly complex solutions when simpler ones exist +- Unnecessary abstractions or design patterns +- Code that's difficult to understand or maintain +- Missing or inadequate tests +- Copy-pasted code blocks that don't fit the project structure +- Excessive comments explaining obvious things + +**Remember**: If you use AI tools, you're still responsible for the quality of the final code. Use AI as a starting point, not a final solution. + +## Commenting on the PR + +When commenting on the PR: + +- Address the feedback points raised by maintainers +- Ask questions if something is unclear +- **Do not** just copy-paste AI-generated responses + - Maintainers want to see your thought process and understanding of the codebase + - Maintainers and other contributors can tell if you're just copying and pasting AI-generated responses + +Thank you for helping make Commitizen better! 🎉 diff --git a/docs/customization.md b/docs/customization.md deleted file mode 100644 index e8f233fce1..0000000000 --- a/docs/customization.md +++ /dev/null @@ -1,522 +0,0 @@ -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 json 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` | `None` | (OPTIONAL) Provide an example to help understand the style. Used by `cz example`. | -| `schema` | `str` | `None` | (OPTIONAL) Show the schema used. Used by `cz schema`. | -| `schema_pattern` | `str` | `None` | (OPTIONAL) The regular expression used to do commit message validation. Used by `cz check`. | -| `info_path` | `str` | `None` | (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` | `None` | (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)** | -[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` | -| 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 - -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/customization/changelog_template.md b/docs/customization/changelog_template.md new file mode 100644 index 0000000000..60f40ebea7 --- /dev/null +++ b/docs/customization/changelog_template.md @@ -0,0 +1,85 @@ + +# 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: + - Through the configuration file + - 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 are 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 your 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 provide 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 +- providing them on the command line with the `--extra/-e` parameter to `bump` and `changelog` commands diff --git a/docs/customization/config_file.md b/docs/customization/config_file.md new file mode 100644 index 0000000000..50185a7583 --- /dev/null +++ b/docs/customization/config_file.md @@ -0,0 +1,234 @@ +# 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. +!!! warning + `cz_customize` is likely to be removed or renamed in the next major release. + This change is still under discussion; you can continue using `cz_customize` for now and follow [#1385](https://github.com/commitizen-tools/commitizen/issues/1385) for the rationale, options, and current status. + +The following shows the **same configuration** in TOML, JSON, and YAML; use the format your project uses. + +Example: + +=== "TOML" + + ```toml title="pyproject.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?" + ``` + +=== "JSON" + + ```json title=".cz.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?" + } + ] + } + } + } + ``` + +=== "YAML" + + ```yaml title=".cz.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?' + ``` + +## Configuration File Options + +| 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/commands/changelog/ + +### Detailed `questions` content + +| Parameter | Type | Default | Description | +| ----------- | ------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `type` | `str` | `None` | The type of questions. Valid types: `list`, `select`, `input`, etc. The `select` type provides an interactive searchable list interface. [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` or `type = select`. 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`. | +| `use_search_filter` | `bool` | `False` | (OPTIONAL) Enable search/filter functionality for list/select type questions. This allows users to type and filter through the choices. | +| `use_jk_keys` | `bool` | `True` | (OPTIONAL) Enable/disable j/k keys for navigation in list/select type questions. Set to false if you prefer arrow keys only. | + +[different-question-types]: https://github.com/tmbo/questionary#different-question-types + +### Shortcut keys + +For a basic overview of `use_shortcuts` and how the default menu looks, see the [`use_shortcuts` option](../config/option.md#use_shortcuts). + +#### `use_shortcuts` with `cz_customize` + +When using `cz_customize`, enabling `use_shortcuts` lets you set an optional `key` for each list/select choice so that choice shows your chosen shortcut. Rules below. + +Example: + +```toml title="pyproject.toml" +[tool.commitizen] +name = "cz_customize" +use_shortcuts = true + +[tool.commitizen.customize] +message_template = "{{prefix}}: {{message}}" +schema = "<type>: <body>" +schema_pattern = "(feat|fix|docs|test):(\\s.*)" + +[[tool.commitizen.customize.questions]] +type = "list" +name = "prefix" +message = "Select the type of change you are committing" +choices = [ + { value = "feat", name = "feat: A new feature.", key = "f" }, + { value = "fix", name = "fix: A bug fix.", key = "x" }, + { value = "docs", name = "docs: Documentation only changes", key = "d" }, + { value = "test", name = "test: Adding or correcting tests", key = "t" } +] + +[[tool.commitizen.customize.questions]] +type = "input" +name = "message" +message = "Commit body: " +``` + +![Menu with custom shortcut keys settings](../images/cli_interactive/shortcut_custom.gif) + +**Rules for `key`** + +| Rule | Description | +|------|-------------| +| Allowed | Lowercase `a`–`z` or digits `0`–`9` only | +| Uniqueness | Each `key` must be unique among all choices | +| Optional | Omit `key` to use default numeric order (1, 2, 3, …) | diff --git a/docs/customization/python_class.md b/docs/customization/python_class.md new file mode 100644 index 0000000000..c698b03dee --- /dev/null +++ b/docs/customization/python_class.md @@ -0,0 +1,310 @@ +# Customizing through a python class + +The basic steps are: + +1. Inheriting from `BaseCommitizen`. +2. Give a name to your rules. +3. Create a Python package using proper [build backend](https://packaging.python.org/en/latest/glossary/#term-Build-Backend) +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. + +See [Third-party plugins](../third-party-plugins/about.md) for more details on how to create a third-party Commitizen plugin. + +## 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 title="cz_jira.py" +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 f"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 title="setup.py" +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 .` + +## 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 title="cz_strange.py" +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 commit validation and error message + +The commit message validation can be customized by overriding the `validate_commit_message` and `format_error_message` +methods from `BaseCommitizen`. This allows for a more detailed feedback to the user where the error originates from. + +```python +import re +from commitizen.cz.base import BaseCommitizen, ValidationResult +from commitizen import git + + +class CustomValidationCz(BaseCommitizen): + def validate_commit_message( + self, + *, + commit_msg: str, + pattern: str | None, + allow_abort: bool, + allowed_prefixes: list[str], + max_msg_length: int, + ) -> ValidationResult: + """Validate commit message against the pattern.""" + if not commit_msg: + return allow_abort, [] if allow_abort else [f"commit message is empty"] + if pattern is None: + return True, [] + if any(map(commit_msg.startswith, allowed_prefixes)): + return True, [] + if max_msg_length: + msg_len = len(commit_msg.partition("\n")[0].strip()) + if msg_len > max_msg_length: + return False, [ + f"commit message is too long. Max length is {max_msg_length}" + ] + pattern_match = re.match(pattern, commit_msg) + if pattern_match: + return True, [] + else: + # Perform additional validation of the commit message format + # and add custom error messages as needed + return False, ["commit message does not match the pattern"] + + def format_exception_message( + self, ill_formatted_commits: list[tuple[git.GitCommit, list]] + ) -> str: + """Format commit errors.""" + displayed_msgs_content = "\n".join( + ( + f'commit "{commit.rev}": "{commit.message}"' + f"errors:\n" + "\n".join((f"- {error}" for error in errors)) + ) + for commit, errors in ill_formatted_commits + ) + return ( + "commit validation: failed!\n" + "please enter a commit message in the commitizen format.\n" + f"{displayed_msgs_content}\n" + f"pattern: {self.schema_pattern()}" + ) +``` + +## Custom changelog generator + +The changelog generator should just work in a very basic manner without touching anything. +You can customize it of course, and the following variables are the ones 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 releases before they are rendered. Must return the update release + +```python title="cz_strange.py" +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 +``` + +## 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 your 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 your `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`. diff --git a/docs/exit_codes.md b/docs/exit_codes.md index af9cb83627..1a214e2832 100644 --- a/docs/exit_codes.md +++ b/docs/exit_codes.md @@ -1,39 +1,142 @@ # 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. +Commitizen handles expected exceptions through `CommitizenException` and returns different exit codes for different situations. This reference is useful when you need to ignore specific errors in your CI/CD pipeline or automation scripts. -These exit codes can be found in `commitizen/exceptions.py::ExitCode`. +All exit codes are defined in [commitizen/exceptions.py](https://github.com/commitizen-tools/commitizen/blob/master/commitizen/exceptions.py). + +## Exit Code Reference | 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 | +| `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 commits found | +| `NoVersionSpecifiedError` | 4 | Version is not specified in configuration file | +| `NoPatternMapError` | 5 | bump / changelog pattern or map can not be found in configuration file | +| `BumpCommitFailedError` | 6 | Commit failed when bumping version | +| `BumpTagFailedError` | 7 | Tag failed when bumping version | +| `NoAnswersError` | 8 | No user response given | +| `CommitError` | 9 | git commit error | +| `NoCommitBackupError` | 10 | Commit backup file is not 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 is missing for `cz_customize` | +| `NoRevisionError` | 16 | No revision found | +| `CurrentVersionNotFoundError`| 17 | Current version cannot be found in `version_files` | +| `InvalidCommandArgumentError`| 18 | The argument provided to the 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 | Invalid combination of command line / configuration file options | +| `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 | Unknown `version_provider` | +| `VersionSchemeUnknown` | 28 | Unknown `version_scheme` | +| `ChangelogFormatUnknown` | 29 | Unknown `changelog_format` or cannot be determined by the file extension | +| `ConfigFileNotFound` | 30 | The configuration file is not found | +| `ConfigFileIsEmpty` | 31 | The configuration file is empty | +| `CommitMessageLengthLimitExceededError`| 32 | The commit message length exceeds the given limit. | + +## Ignoring Exit Codes + +In some scenarios, you may want Commitizen to continue execution even when certain errors occur. This is particularly useful in CI/CD pipelines where you want to handle specific errors gracefully. + +### Using `--no-raise` Flag + +The `--no-raise` (or `-nr`) flag allows you to specify exit codes that should not cause Commitizen to exit with an error. You can use either: + +- **Exit code numbers**: `21`, `3`, `4` +- **Exit code names**: `NO_INCREMENT`, `NO_COMMITS_FOUND`, `NO_VERSION_SPECIFIED` +- **Mixed format**: `21,NO_COMMITS_FOUND,4` + +Multiple exit codes can be specified as a comma-separated list. + +### Common Use Cases + +#### Ignoring No Increment Errors + +The most common use case is to ignore `NoneIncrementExit` (exit code 21) when running `cz bump`. This allows the command to succeed even when no commits are eligible for a version bump: + +```sh +cz -nr 21 bump +``` + +Or using the exit code name: + +```sh +cz -nr NO_INCREMENT bump +``` + +This is useful in CI pipelines where you want to run `cz bump` regularly, but don't want the pipeline to fail when there are no version-worthy commits. + +#### Ignoring Multiple Exit Codes + +You can ignore multiple exit codes at once: + +```sh +cz --no-raise 21,3,4 bump +``` + +This example ignores: + +- `21` (`NoneIncrementExit`) - No eligible commits for bump +- `3` (`NoCommitsFoundError`) - No commits found +- `4` (`NoVersionSpecifiedError`) - Version not specified + +### Finding the Exit Code + +If you encounter an error and want to ignore it, you can find the exit code in two ways: + +#### Method 1: Check the Exit Code After Running + +After running a Commitizen command that fails, check the exit code: + +```sh +cz bump +echo $? # Prints the exit code (e.g., 21) +``` + +Then use that exit code with `--no-raise`: + +```sh +cz -nr 21 bump +``` + +#### Method 2: Look Up the Exception + +1. Check the error message to identify the exception type +2. Find the corresponding exit code in the table above +3. Use that exit code with `--no-raise` + +For example, if you see `NoneIncrementExit` in the error, look it up in the table to find it's exit code 21, then use: + +```sh +cz -nr 21 bump +``` + +### Best Practices + +- **Document your usage**: If you use `--no-raise` in scripts or CI/CD, document why specific exit codes are ignored +- **Be specific**: Only ignore exit codes you understand and have a reason to ignore +- **Test thoroughly**: Ensure that ignoring certain exit codes doesn't mask real problems in your workflow +- **Use exit code names**: When possible, use exit code names (e.g., `NO_INCREMENT`) instead of numbers for better readability + +### Example: CI/CD Pipeline + +Here's an example of using `--no-raise` in a CI/CD pipeline: + +```yaml +# .github/workflows/release.yml +- name: Bump version + run: | + cz -nr NO_INCREMENT bump || true + # Continue even if no version bump is needed +``` + +This ensures the pipeline continues even when there are no commits eligible for a version bump. diff --git a/docs/external_links.md b/docs/external_links.md index 388bcc8dea..dc9bc1b0a2 100644 --- a/docs/external_links.md +++ b/docs/external_links.md @@ -1,18 +1,19 @@ -> If you have written over commitizen, make a PR and add the link here 💪 +> 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/#/) | +| ------------------------------------------------------------------------- | --------------- | ---------------------------------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| [Atomic Commits: An Easy & Proven Way to Manage & Automate Release Process](https://www.youtube.com/watch?v=IxzN9ClXhs8) | Wei Lee | COSCUP 2023 | Taiwanese Mandarin | [slides](https://speakerdeck.com/leew/atomic-commits-an-easy-and-proven-way-to-manage-and-automate-release-process) | +| commitizen-tools: What can we gain from crafting a git message convention | Wei Lee | Taipei.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) +- [Python Table Manners - Commitizen: 規格化 commit message](https://blog.wei-lee.me/posts/tech/2020/03/python-table-manners-commitizen/) (Written in Traditional Mandarin) +- [Automating semantic release with commitizen](https://woile.dev/blog/automating-deployment-with-commitizen.html) (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) +- [Continuous delivery made easy (in Python)](https://blog.devgenius.io/continuous-delivery-made-easy-in-python-c085e9c82e69) [automatizando]: https://youtu.be/t3aE2M8UPBo diff --git a/docs/faq.md b/docs/faq.md index 4bcb2bc7cf..568ec2fbe8 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,6 +1,10 @@ -## Support for PEP621 +# FAQ -PEP621 establishes a `[project]` definition inside `pyproject.toml` +This page contains frequently asked how to questions. + +## Support for [`PEP621`](https://peps.python.org/pep-0621/) + +`PEP621` establishes a `[project]` definition inside `pyproject.toml` ```toml [project] @@ -8,7 +12,8 @@ name = "spam" version = "2.5.1" ``` -Commitizen provides a [`pep621` version provider](config.md#version-providers) to get and set version from this field. +Commitizen provides a [`PEP621` version provider](config/version_provider.md) to get and set version from this field. + You just need to set the proper `version_provider` setting: ```toml @@ -20,13 +25,6 @@ version = "2.5.1" 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: @@ -39,34 +37,12 @@ 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 +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 @@ -76,34 +52,40 @@ 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. +This error was caused by a Python bug on Windows. It's been fixed by [cpython #22017](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? +## How to change the tag format ? + +You can use the [`legacy_tag_formats`](config/bump.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: -`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. +- changelog generation (full, incremental and version range) +- bump new version computation (automatically guessed or increment given) -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]). +So given if you change from `myproject-$version` to `${version}` and then `v${version}`, +your Commitizen configuration will look like this: -If you would like to learn more about both schemes, there are plenty of good resources: +```toml +tag_format = "v${version}" +legacy_tag_formats = [ + "${version}", + "myproject-$version", +] +``` + +## How to avoid warnings for expected non-version tags? -- [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/) +You can explicitly ignore them with [`ignored_tag_formats`](config/bump.md#ignored_tag_formats). -[#173]: https://github.com/commitizen-tools/commitizen/issues/173 -[#385]: https://github.com/commitizen-tools/commitizen/pull/385 +```toml +tag_format = "v${version}" +ignored_tag_formats = [ + "stable", + "component-*", + "env/*", + "v${major}.${minor}", +] +``` diff --git a/docs/features_wont_add.md b/docs/features_wont_add.md new file mode 100644 index 0000000000..01b5da1ff7 --- /dev/null +++ b/docs/features_wont_add.md @@ -0,0 +1,71 @@ +# Feature request graveyard + +This page contains features and designs that have been proposed or considered but won't be implemented in Commitizen. + +For a comprehensive list, please refer to our [issue tracker](https://github.com/commitizen-tools/commitizen/issues?q=is:issue%20state:closed%20label:%22issue-status:%20wont-fix%22%20OR%20label:%22issue-status:%20wont-implement%22). + +## Enable multiple locations of config file `.cz.*` [#955](https://github.com/commitizen-tools/commitizen/issues/955) + +<!-- TODO: Add more details about why we won't add this feature --> + +## Create a flag to build the changelog from commits in multiple git repositories [#790](https://github.com/commitizen-tools/commitizen/issues/790) + +<!-- TODO: Add more details about why we won't add this feature --> + +## Global Configuration [#597](https://github.com/commitizen-tools/commitizen/issues/597) + +<!-- TODO: Add more details about why we won't add this feature --> + +## 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/config_file.md)). + +See more discussion in + +- [issue #142](https://github.com/commitizen-tools/commitizen/issues/142) +- [issue #36](https://github.com/commitizen-tools/commitizen/issues/36) + + +## 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 purpose. 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/post/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 + + +## Why don't we use [Pydantic](https://docs.pydantic.dev/)? + +While Pydantic is a powerful and popular library for data validation, we intentionally avoid using it in this project to keep our dependency tree minimal and maintainable. + +Including Pydantic would increase the chances of version conflicts for users - especially with major changes introduced in Pydantic v3. Because we pin dependencies tightly, adding Pydantic could unintentionally restrict what other tools or libraries users can install alongside `commitizen`. + +Moreover we don't rely on the full feature set of Pydantic. Simpler alternatives like Python's built-in `TypedDict` offer sufficient type safety for our use cases, without the runtime overhead or dependency burden. + +In short, avoiding Pydantic helps us: + +- Keep dependencies lightweight +- Reduce compatibility issues for users +- Maintain clarity about what contributors should and shouldn't use diff --git a/docs/getting_started.md b/docs/getting_started.md deleted file mode 100644 index 81da513e0b..0000000000 --- a/docs/getting_started.md +++ /dev/null @@ -1,119 +0,0 @@ -## 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: [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/history.md b/docs/history.md new file mode 100644 index 0000000000..47a726e100 --- /dev/null +++ b/docs/history.md @@ -0,0 +1,23 @@ +## Is this project affiliated with the [cz-cli][cz-cli] project? + +**It is not affiliated.** + +Both are used for similar purposes, parsing commits, generating changelog and version we presume. +Our Commitizen project is written in python to make integration easier for python projects, whereas [cz-cli][cz-cli] is written in JavaScript and serves the JS packages. + +<!-- TODO: Add more details about the differences between Commitizen and cz-cli --> + +They differ a bit in design, not sure if cz-cli does any of this, but these are some things you can do with our Commitizen: + +- create custom rules, version bumps and changelog generation. By default, we use the popular conventional commits (I think cz-cli allows this). +- single package, install one thing and it will work. cz-cli is a monorepo, but you have to install different dependencies as far as I know. +- 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-cli][cz-cli] 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-cli]: https://github.com/commitizen/cz-cli diff --git a/docs/images/bump.gif b/docs/images/bump.gif deleted file mode 100644 index 0e97550ade..0000000000 Binary files a/docs/images/bump.gif and /dev/null differ diff --git a/docs/images/bump.tape b/docs/images/bump.tape new file mode 100644 index 0000000000..08003602e2 --- /dev/null +++ b/docs/images/bump.tape @@ -0,0 +1,134 @@ +Output cli_interactive/bump.gif + +Require cz + +# Use bash for cross-platform compatibility (macOS, Linux, Windows) +Set Shell bash + +Set FontSize 16 +Set Width 878 +Set Height 568 +Set Padding 20 +Set TypingSpeed 50ms + +Set Theme { + "name": "Commitizen", + "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", + "background": "#1e1e2e", + "foreground": "#afafaf", + "cursor": "#c7c7c7" +} + +# Hide initial shell prompt +Hide + +# Wait for terminal to be ready +Sleep 1s + +# Set a clean, simple prompt (while hidden) +Type "PS1='$ '" +Enter +Sleep 300ms + +# Create a clean temporary directory for recording +Type "rm -rf /tmp/commitizen-example && mkdir -p /tmp/commitizen-example && cd /tmp/commitizen-example" +Enter +Sleep 500ms + +# Initialize git repository +Type "git init" +Enter +Type "git config user.email 'you@example.com'" +Enter +Type "git config user.name 'Your Name'" +Enter +Sleep 500ms + +# Initialize commitizen config with version 0.0.1 and changelog enabled +Type `cat > pyproject.toml << 'EOF'` +Enter +Sleep 100ms +Type `[tool.commitizen]` +Enter +Sleep 100ms +Type `version = "0.0.1"` +Enter +Sleep 100ms +Type `update_changelog_on_bump = true` +Enter +Sleep 100ms +Type "EOF" +Enter +Sleep 300ms + +# Create initial commit (no tag, so cz bump will ask "Is this the first tag created?") +Type "git add pyproject.toml" +Enter +Sleep 300ms + +Type "git commit -m 'chore: initial commit'" +Enter +Sleep 500ms + +# Create a feat commit that will trigger a MINOR bump (0.0.1 -> 0.1.0) +Type "echo 'new feature' > feature.py" +Enter +Sleep 300ms + +Type "git add feature.py" +Enter +Sleep 300ms + +Type "git commit -m 'feat: add awesome new feature'" +Enter +Sleep 500ms + +# Clear the screen to start fresh +Type "clear" +Enter +Sleep 500ms + +# Show commands from here +Show + +# Step 1: Show current version +Type "cz version --project" +Sleep 500ms +Enter +Sleep 1s + +# Step 2: Run cz bump (no existing tag, will prompt for first tag) +Type "cz bump" +Sleep 500ms +Enter + +# Wait for the "Is this the first tag created?" prompt +Sleep 2s + +# Answer Yes to "Is this the first tag created?" (default is Yes, just press Enter) +Enter +Sleep 3s + +# Step 3: Show new version after bump +Type "cz version --project" +Sleep 500ms +Enter +Sleep 1s + +# Wait for final output +Sleep 3s diff --git a/docs/images/cli_help/cz___help.svg b/docs/images/cli_help/cz___help.svg index 22a9e4d0e7..5c8f8fef39 100644 --- a/docs/images/cli_help/cz___help.svg +++ b/docs/images/cli_help/cz___help.svg @@ -1,4 +1,4 @@ -<svg class="rich-terminal" viewBox="0 0 994 928.4" xmlns="http://www.w3.org/2000/svg"> +<svg class="rich-terminal" viewBox="0 0 994 952.8" xmlns="http://www.w3.org/2000/svg"> <!-- Generated with Rich https://www.textualize.io --> <style> @@ -19,183 +19,187 @@ font-weight: 700; } - .terminal-4198725382-matrix { + .terminal-1104329960-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4198725382-title { + .terminal-1104329960-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4198725382-r1 { fill: #c5c8c6 } -.terminal-4198725382-r2 { fill: #c5c8c6;font-weight: bold } -.terminal-4198725382-r3 { fill: #d0b344 } -.terminal-4198725382-r4 { fill: #1984e9;text-decoration: underline; } -.terminal-4198725382-r5 { fill: #68a0b3;font-weight: bold } + .terminal-1104329960-r1 { fill: #c5c8c6 } +.terminal-1104329960-r2 { fill: #c5c8c6;font-weight: bold } +.terminal-1104329960-r3 { fill: #d0b344 } +.terminal-1104329960-r4 { fill: #1984e9;text-decoration: underline; } +.terminal-1104329960-r5 { fill: #68a0b3;font-weight: bold } </style> <defs> - <clipPath id="terminal-4198725382-clip-terminal"> - <rect x="0" y="0" width="975.0" height="877.4" /> + <clipPath id="terminal-1104329960-clip-terminal"> + <rect x="0" y="0" width="975.0" height="901.8" /> </clipPath> - <clipPath id="terminal-4198725382-line-0"> + <clipPath id="terminal-1104329960-line-0"> <rect x="0" y="1.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-1"> +<clipPath id="terminal-1104329960-line-1"> <rect x="0" y="25.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-2"> +<clipPath id="terminal-1104329960-line-2"> <rect x="0" y="50.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-3"> +<clipPath id="terminal-1104329960-line-3"> <rect x="0" y="74.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-4"> +<clipPath id="terminal-1104329960-line-4"> <rect x="0" y="99.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-5"> +<clipPath id="terminal-1104329960-line-5"> <rect x="0" y="123.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-6"> +<clipPath id="terminal-1104329960-line-6"> <rect x="0" y="147.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-7"> +<clipPath id="terminal-1104329960-line-7"> <rect x="0" y="172.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-8"> +<clipPath id="terminal-1104329960-line-8"> <rect x="0" y="196.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-9"> +<clipPath id="terminal-1104329960-line-9"> <rect x="0" y="221.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-10"> +<clipPath id="terminal-1104329960-line-10"> <rect x="0" y="245.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-11"> +<clipPath id="terminal-1104329960-line-11"> <rect x="0" y="269.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-12"> +<clipPath id="terminal-1104329960-line-12"> <rect x="0" y="294.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-13"> +<clipPath id="terminal-1104329960-line-13"> <rect x="0" y="318.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-14"> +<clipPath id="terminal-1104329960-line-14"> <rect x="0" y="343.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-15"> +<clipPath id="terminal-1104329960-line-15"> <rect x="0" y="367.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-16"> +<clipPath id="terminal-1104329960-line-16"> <rect x="0" y="391.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-17"> +<clipPath id="terminal-1104329960-line-17"> <rect x="0" y="416.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-18"> +<clipPath id="terminal-1104329960-line-18"> <rect x="0" y="440.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-19"> +<clipPath id="terminal-1104329960-line-19"> <rect x="0" y="465.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-20"> +<clipPath id="terminal-1104329960-line-20"> <rect x="0" y="489.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-21"> +<clipPath id="terminal-1104329960-line-21"> <rect x="0" y="513.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-22"> +<clipPath id="terminal-1104329960-line-22"> <rect x="0" y="538.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-23"> +<clipPath id="terminal-1104329960-line-23"> <rect x="0" y="562.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-24"> +<clipPath id="terminal-1104329960-line-24"> <rect x="0" y="587.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-25"> +<clipPath id="terminal-1104329960-line-25"> <rect x="0" y="611.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-26"> +<clipPath id="terminal-1104329960-line-26"> <rect x="0" y="635.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-27"> +<clipPath id="terminal-1104329960-line-27"> <rect x="0" y="660.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-28"> +<clipPath id="terminal-1104329960-line-28"> <rect x="0" y="684.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-29"> +<clipPath id="terminal-1104329960-line-29"> <rect x="0" y="709.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-30"> +<clipPath id="terminal-1104329960-line-30"> <rect x="0" y="733.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-31"> +<clipPath id="terminal-1104329960-line-31"> <rect x="0" y="757.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-32"> +<clipPath id="terminal-1104329960-line-32"> <rect x="0" y="782.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-33"> +<clipPath id="terminal-1104329960-line-33"> <rect x="0" y="806.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4198725382-line-34"> +<clipPath id="terminal-1104329960-line-34"> <rect x="0" y="831.1" width="976" height="24.65"/> </clipPath> +<clipPath id="terminal-1104329960-line-35"> + <rect x="0" y="855.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="926.4" rx="8"/> + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="950.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-4198725382-clip-terminal)"> + <g transform="translate(9, 41)" clip-path="url(#terminal-1104329960-clip-terminal)"> - <g class="terminal-4198725382-matrix"> - <text class="terminal-4198725382-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-4198725382-line-0)">$ cz --help</text><text class="terminal-4198725382-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-4198725382-line-0)"> -</text><text class="terminal-4198725382-r1" x="0" y="44.4" textLength="122" clip-path="url(#terminal-4198725382-line-1)">usage: cz </text><text class="terminal-4198725382-r2" x="122" y="44.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-1)">[</text><text class="terminal-4198725382-r1" x="134.2" y="44.4" textLength="24.4" clip-path="url(#terminal-4198725382-line-1)">-h</text><text class="terminal-4198725382-r2" x="158.6" y="44.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-1)">]</text><text class="terminal-4198725382-r2" x="183" y="44.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-1)">[</text><text class="terminal-4198725382-r1" x="195.2" y="44.4" textLength="183" clip-path="url(#terminal-4198725382-line-1)">--config CONFIG</text><text class="terminal-4198725382-r2" x="378.2" y="44.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-1)">]</text><text class="terminal-4198725382-r2" x="402.6" y="44.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-1)">[</text><text class="terminal-4198725382-r1" x="414.8" y="44.4" textLength="85.4" clip-path="url(#terminal-4198725382-line-1)">--debug</text><text class="terminal-4198725382-r2" x="500.2" y="44.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-1)">]</text><text class="terminal-4198725382-r2" x="524.6" y="44.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-1)">[</text><text class="terminal-4198725382-r1" x="536.8" y="44.4" textLength="85.4" clip-path="url(#terminal-4198725382-line-1)">-n NAME</text><text class="terminal-4198725382-r2" x="622.2" y="44.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-1)">]</text><text class="terminal-4198725382-r2" x="646.6" y="44.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-1)">[</text><text class="terminal-4198725382-r1" x="658.8" y="44.4" textLength="146.4" clip-path="url(#terminal-4198725382-line-1)">-nr NO_RAISE</text><text class="terminal-4198725382-r2" x="805.2" y="44.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-1)">]</text><text class="terminal-4198725382-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-1)"> -</text><text class="terminal-4198725382-r2" x="122" y="68.8" textLength="12.2" clip-path="url(#terminal-4198725382-line-2)">{</text><text class="terminal-4198725382-r1" x="134.2" y="68.8" textLength="829.6" clip-path="url(#terminal-4198725382-line-2)">init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version</text><text class="terminal-4198725382-r2" x="963.8" y="68.8" textLength="12.2" clip-path="url(#terminal-4198725382-line-2)">}</text><text class="terminal-4198725382-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-4198725382-line-2)"> -</text><text class="terminal-4198725382-r3" x="122" y="93.2" textLength="36.6" clip-path="url(#terminal-4198725382-line-3)">...</text><text class="terminal-4198725382-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-4198725382-line-3)"> -</text><text class="terminal-4198725382-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-4198725382-line-4)"> -</text><text class="terminal-4198725382-r1" x="0" y="142" textLength="707.6" clip-path="url(#terminal-4198725382-line-5)">Commitizen is a cli tool to generate conventional commits.</text><text class="terminal-4198725382-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-4198725382-line-5)"> -</text><text class="terminal-4198725382-r1" x="0" y="166.4" textLength="524.6" clip-path="url(#terminal-4198725382-line-6)">For more information about the topic go to </text><text class="terminal-4198725382-r4" x="524.6" y="166.4" textLength="390.4" clip-path="url(#terminal-4198725382-line-6)">https://conventionalcommits.org/</text><text class="terminal-4198725382-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-6)"> -</text><text class="terminal-4198725382-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-4198725382-line-7)"> -</text><text class="terminal-4198725382-r1" x="0" y="215.2" textLength="97.6" clip-path="url(#terminal-4198725382-line-8)">options:</text><text class="terminal-4198725382-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-4198725382-line-8)"> -</text><text class="terminal-4198725382-r1" x="0" y="239.6" textLength="671" clip-path="url(#terminal-4198725382-line-9)">  -h, --help            show this help message and exit</text><text class="terminal-4198725382-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-4198725382-line-9)"> -</text><text class="terminal-4198725382-r1" x="0" y="264" textLength="658.8" clip-path="url(#terminal-4198725382-line-10)">  --config CONFIG       the path of configuration file</text><text class="terminal-4198725382-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-4198725382-line-10)"> -</text><text class="terminal-4198725382-r1" x="0" y="288.4" textLength="463.6" clip-path="url(#terminal-4198725382-line-11)">  --debug               use debug mode</text><text class="terminal-4198725382-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-11)"> -</text><text class="terminal-4198725382-r1" x="0" y="312.8" textLength="597.8" clip-path="url(#terminal-4198725382-line-12)">  -n NAME, --name NAME  use the given commitizen </text><text class="terminal-4198725382-r2" x="597.8" y="312.8" textLength="12.2" clip-path="url(#terminal-4198725382-line-12)">(</text><text class="terminal-4198725382-r1" x="610" y="312.8" textLength="97.6" clip-path="url(#terminal-4198725382-line-12)">default:</text><text class="terminal-4198725382-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-4198725382-line-12)"> -</text><text class="terminal-4198725382-r1" x="0" y="337.2" textLength="573.4" clip-path="url(#terminal-4198725382-line-13)">                        cz_conventional_commits</text><text class="terminal-4198725382-r2" x="573.4" y="337.2" textLength="12.2" clip-path="url(#terminal-4198725382-line-13)">)</text><text class="terminal-4198725382-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-4198725382-line-13)"> -</text><text class="terminal-4198725382-r1" x="0" y="361.6" textLength="427" clip-path="url(#terminal-4198725382-line-14)">  -nr NO_RAISE, --no-raise NO_RAISE</text><text class="terminal-4198725382-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-4198725382-line-14)"> -</text><text class="terminal-4198725382-r1" x="0" y="386" textLength="902.8" clip-path="url(#terminal-4198725382-line-15)">                        comma separated error codes that won't rise error,</text><text class="terminal-4198725382-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-4198725382-line-15)"> -</text><text class="terminal-4198725382-r1" x="0" y="410.4" textLength="439.2" clip-path="url(#terminal-4198725382-line-16)">                        e.g: cz -nr </text><text class="terminal-4198725382-r5" x="439.2" y="410.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-16)">1</text><text class="terminal-4198725382-r1" x="451.4" y="410.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-16)">,</text><text class="terminal-4198725382-r5" x="463.6" y="410.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-16)">2</text><text class="terminal-4198725382-r1" x="475.8" y="410.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-16)">,</text><text class="terminal-4198725382-r5" x="488" y="410.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-16)">3</text><text class="terminal-4198725382-r1" x="500.2" y="410.4" textLength="231.8" clip-path="url(#terminal-4198725382-line-16)"> bump. See codes at</text><text class="terminal-4198725382-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-16)"> -</text><text class="terminal-4198725382-r4" x="292.8" y="434.8" textLength="231.8" clip-path="url(#terminal-4198725382-line-17)">https://commitizen-</text><text class="terminal-4198725382-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-4198725382-line-17)"> -</text><text class="terminal-4198725382-r1" x="0" y="459.2" textLength="756.4" clip-path="url(#terminal-4198725382-line-18)">                        tools.github.io/commitizen/exit_codes/</text><text class="terminal-4198725382-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-4198725382-line-18)"> -</text><text class="terminal-4198725382-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-4198725382-line-19)"> -</text><text class="terminal-4198725382-r1" x="0" y="508" textLength="109.8" clip-path="url(#terminal-4198725382-line-20)">commands:</text><text class="terminal-4198725382-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-4198725382-line-20)"> -</text><text class="terminal-4198725382-r2" x="24.4" y="532.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-21)">{</text><text class="terminal-4198725382-r1" x="36.6" y="532.4" textLength="829.6" clip-path="url(#terminal-4198725382-line-21)">init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version</text><text class="terminal-4198725382-r2" x="866.2" y="532.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-21)">}</text><text class="terminal-4198725382-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-21)"> -</text><text class="terminal-4198725382-r1" x="0" y="556.8" textLength="646.6" clip-path="url(#terminal-4198725382-line-22)">    init                init commitizen configuration</text><text class="terminal-4198725382-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-4198725382-line-22)"> -</text><text class="terminal-4198725382-r1" x="0" y="581.2" textLength="134.2" clip-path="url(#terminal-4198725382-line-23)">    commit </text><text class="terminal-4198725382-r2" x="134.2" y="581.2" textLength="12.2" clip-path="url(#terminal-4198725382-line-23)">(</text><text class="terminal-4198725382-r1" x="146.4" y="581.2" textLength="12.2" clip-path="url(#terminal-4198725382-line-23)">c</text><text class="terminal-4198725382-r2" x="158.6" y="581.2" textLength="12.2" clip-path="url(#terminal-4198725382-line-23)">)</text><text class="terminal-4198725382-r1" x="170.8" y="581.2" textLength="329.4" clip-path="url(#terminal-4198725382-line-23)">          create new commit</text><text class="terminal-4198725382-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-4198725382-line-23)"> -</text><text class="terminal-4198725382-r1" x="0" y="605.6" textLength="610" clip-path="url(#terminal-4198725382-line-24)">    ls                  show available commitizens</text><text class="terminal-4198725382-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-4198725382-line-24)"> -</text><text class="terminal-4198725382-r1" x="0" y="630" textLength="524.6" clip-path="url(#terminal-4198725382-line-25)">    example             show commit example</text><text class="terminal-4198725382-r1" x="976" y="630" textLength="12.2" clip-path="url(#terminal-4198725382-line-25)"> -</text><text class="terminal-4198725382-r1" x="0" y="654.4" textLength="646.6" clip-path="url(#terminal-4198725382-line-26)">    info                show information about the cz</text><text class="terminal-4198725382-r1" x="976" y="654.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-26)"> -</text><text class="terminal-4198725382-r1" x="0" y="678.8" textLength="512.4" clip-path="url(#terminal-4198725382-line-27)">    schema              show commit schema</text><text class="terminal-4198725382-r1" x="976" y="678.8" textLength="12.2" clip-path="url(#terminal-4198725382-line-27)"> -</text><text class="terminal-4198725382-r1" x="0" y="703.2" textLength="805.2" clip-path="url(#terminal-4198725382-line-28)">    bump                bump semantic version based on the git log</text><text class="terminal-4198725382-r1" x="976" y="703.2" textLength="12.2" clip-path="url(#terminal-4198725382-line-28)"> -</text><text class="terminal-4198725382-r1" x="0" y="727.6" textLength="170.8" clip-path="url(#terminal-4198725382-line-29)">    changelog </text><text class="terminal-4198725382-r2" x="170.8" y="727.6" textLength="12.2" clip-path="url(#terminal-4198725382-line-29)">(</text><text class="terminal-4198725382-r1" x="183" y="727.6" textLength="24.4" clip-path="url(#terminal-4198725382-line-29)">ch</text><text class="terminal-4198725382-r2" x="207.4" y="727.6" textLength="12.2" clip-path="url(#terminal-4198725382-line-29)">)</text><text class="terminal-4198725382-r1" x="219.6" y="727.6" textLength="305" clip-path="url(#terminal-4198725382-line-29)">      generate changelog </text><text class="terminal-4198725382-r2" x="524.6" y="727.6" textLength="12.2" clip-path="url(#terminal-4198725382-line-29)">(</text><text class="terminal-4198725382-r1" x="536.8" y="727.6" textLength="329.4" clip-path="url(#terminal-4198725382-line-29)">note that it will overwrite</text><text class="terminal-4198725382-r1" x="976" y="727.6" textLength="12.2" clip-path="url(#terminal-4198725382-line-29)"> -</text><text class="terminal-4198725382-r1" x="0" y="752" textLength="451.4" clip-path="url(#terminal-4198725382-line-30)">                        existing file</text><text class="terminal-4198725382-r2" x="451.4" y="752" textLength="12.2" clip-path="url(#terminal-4198725382-line-30)">)</text><text class="terminal-4198725382-r1" x="976" y="752" textLength="12.2" clip-path="url(#terminal-4198725382-line-30)"> -</text><text class="terminal-4198725382-r1" x="0" y="776.4" textLength="951.6" clip-path="url(#terminal-4198725382-line-31)">    check               validates that a commit message matches the commitizen</text><text class="terminal-4198725382-r1" x="976" y="776.4" textLength="12.2" clip-path="url(#terminal-4198725382-line-31)"> -</text><text class="terminal-4198725382-r1" x="0" y="800.8" textLength="366" clip-path="url(#terminal-4198725382-line-32)">                        schema</text><text class="terminal-4198725382-r1" x="976" y="800.8" textLength="12.2" clip-path="url(#terminal-4198725382-line-32)"> -</text><text class="terminal-4198725382-r1" x="0" y="825.2" textLength="902.8" clip-path="url(#terminal-4198725382-line-33)">    version             get the version of the installed commitizen or the</text><text class="terminal-4198725382-r1" x="976" y="825.2" textLength="12.2" clip-path="url(#terminal-4198725382-line-33)"> -</text><text class="terminal-4198725382-r1" x="0" y="849.6" textLength="488" clip-path="url(#terminal-4198725382-line-34)">                        current project </text><text class="terminal-4198725382-r2" x="488" y="849.6" textLength="12.2" clip-path="url(#terminal-4198725382-line-34)">(</text><text class="terminal-4198725382-r1" x="500.2" y="849.6" textLength="353.8" clip-path="url(#terminal-4198725382-line-34)">default: installed commitizen</text><text class="terminal-4198725382-r2" x="854" y="849.6" textLength="12.2" clip-path="url(#terminal-4198725382-line-34)">)</text><text class="terminal-4198725382-r1" x="976" y="849.6" textLength="12.2" clip-path="url(#terminal-4198725382-line-34)"> -</text><text class="terminal-4198725382-r1" x="976" y="874" textLength="12.2" clip-path="url(#terminal-4198725382-line-35)"> + <g class="terminal-1104329960-matrix"> + <text class="terminal-1104329960-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-1104329960-line-0)">$ cz --help</text><text class="terminal-1104329960-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-1104329960-line-0)"> +</text><text class="terminal-1104329960-r1" x="0" y="44.4" textLength="122" clip-path="url(#terminal-1104329960-line-1)">usage: cz </text><text class="terminal-1104329960-r2" x="122" y="44.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-1)">[</text><text class="terminal-1104329960-r1" x="134.2" y="44.4" textLength="24.4" clip-path="url(#terminal-1104329960-line-1)">-h</text><text class="terminal-1104329960-r2" x="158.6" y="44.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-1)">]</text><text class="terminal-1104329960-r2" x="183" y="44.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-1)">[</text><text class="terminal-1104329960-r1" x="195.2" y="44.4" textLength="183" clip-path="url(#terminal-1104329960-line-1)">--config CONFIG</text><text class="terminal-1104329960-r2" x="378.2" y="44.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-1)">]</text><text class="terminal-1104329960-r2" x="402.6" y="44.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-1)">[</text><text class="terminal-1104329960-r1" x="414.8" y="44.4" textLength="85.4" clip-path="url(#terminal-1104329960-line-1)">--debug</text><text class="terminal-1104329960-r2" x="500.2" y="44.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-1)">]</text><text class="terminal-1104329960-r2" x="524.6" y="44.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-1)">[</text><text class="terminal-1104329960-r1" x="536.8" y="44.4" textLength="85.4" clip-path="url(#terminal-1104329960-line-1)">-n NAME</text><text class="terminal-1104329960-r2" x="622.2" y="44.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-1)">]</text><text class="terminal-1104329960-r2" x="646.6" y="44.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-1)">[</text><text class="terminal-1104329960-r1" x="658.8" y="44.4" textLength="146.4" clip-path="url(#terminal-1104329960-line-1)">-nr NO_RAISE</text><text class="terminal-1104329960-r2" x="805.2" y="44.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-1)">]</text><text class="terminal-1104329960-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-1)"> +</text><text class="terminal-1104329960-r2" x="122" y="68.8" textLength="12.2" clip-path="url(#terminal-1104329960-line-2)">{</text><text class="terminal-1104329960-r1" x="134.2" y="68.8" textLength="829.6" clip-path="url(#terminal-1104329960-line-2)">init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version</text><text class="terminal-1104329960-r2" x="963.8" y="68.8" textLength="12.2" clip-path="url(#terminal-1104329960-line-2)">}</text><text class="terminal-1104329960-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-1104329960-line-2)"> +</text><text class="terminal-1104329960-r3" x="122" y="93.2" textLength="36.6" clip-path="url(#terminal-1104329960-line-3)">...</text><text class="terminal-1104329960-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-1104329960-line-3)"> +</text><text class="terminal-1104329960-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-1104329960-line-4)"> +</text><text class="terminal-1104329960-r1" x="0" y="142" textLength="915" clip-path="url(#terminal-1104329960-line-5)">Commitizen is a powerful release management tool that helps teams maintain </text><text class="terminal-1104329960-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-1104329960-line-5)"> +</text><text class="terminal-1104329960-r1" x="0" y="166.4" textLength="951.6" clip-path="url(#terminal-1104329960-line-6)">consistent and meaningful commit messages while automating version management.</text><text class="terminal-1104329960-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-6)"> +</text><text class="terminal-1104329960-r1" x="0" y="190.8" textLength="427" clip-path="url(#terminal-1104329960-line-7)">For more information, please visit </text><text class="terminal-1104329960-r4" x="427" y="190.8" textLength="549" clip-path="url(#terminal-1104329960-line-7)">https://commitizen-tools.github.io/commitizen</text><text class="terminal-1104329960-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-1104329960-line-7)"> +</text><text class="terminal-1104329960-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-1104329960-line-8)"> +</text><text class="terminal-1104329960-r1" x="0" y="239.6" textLength="97.6" clip-path="url(#terminal-1104329960-line-9)">options:</text><text class="terminal-1104329960-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-1104329960-line-9)"> +</text><text class="terminal-1104329960-r1" x="0" y="264" textLength="671" clip-path="url(#terminal-1104329960-line-10)">  -h, --help            show this help message and exit</text><text class="terminal-1104329960-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-1104329960-line-10)"> +</text><text class="terminal-1104329960-r1" x="0" y="288.4" textLength="719.8" clip-path="url(#terminal-1104329960-line-11)">  --config CONFIG       The path to the configuration file.</text><text class="terminal-1104329960-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-11)"> +</text><text class="terminal-1104329960-r1" x="0" y="312.8" textLength="475.8" clip-path="url(#terminal-1104329960-line-12)">  --debug               Use debug mode.</text><text class="terminal-1104329960-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-1104329960-line-12)"> +</text><text class="terminal-1104329960-r1" x="0" y="337.2" textLength="597.8" clip-path="url(#terminal-1104329960-line-13)">  -n NAME, --name NAME  Use the given commitizen </text><text class="terminal-1104329960-r2" x="597.8" y="337.2" textLength="12.2" clip-path="url(#terminal-1104329960-line-13)">(</text><text class="terminal-1104329960-r1" x="610" y="337.2" textLength="97.6" clip-path="url(#terminal-1104329960-line-13)">default:</text><text class="terminal-1104329960-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-1104329960-line-13)"> +</text><text class="terminal-1104329960-r1" x="0" y="361.6" textLength="573.4" clip-path="url(#terminal-1104329960-line-14)">                        cz_conventional_commits</text><text class="terminal-1104329960-r2" x="573.4" y="361.6" textLength="12.2" clip-path="url(#terminal-1104329960-line-14)">)</text><text class="terminal-1104329960-r1" x="585.6" y="361.6" textLength="12.2" clip-path="url(#terminal-1104329960-line-14)">.</text><text class="terminal-1104329960-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-1104329960-line-14)"> +</text><text class="terminal-1104329960-r1" x="0" y="386" textLength="427" clip-path="url(#terminal-1104329960-line-15)">  -nr NO_RAISE, --no-raise NO_RAISE</text><text class="terminal-1104329960-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-1104329960-line-15)"> +</text><text class="terminal-1104329960-r1" x="0" y="410.4" textLength="915" clip-path="url(#terminal-1104329960-line-16)">                        Comma-separated error codes that won't raise error,</text><text class="terminal-1104329960-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-16)"> +</text><text class="terminal-1104329960-r1" x="0" y="434.8" textLength="451.4" clip-path="url(#terminal-1104329960-line-17)">                        e.g., cz -nr </text><text class="terminal-1104329960-r5" x="451.4" y="434.8" textLength="12.2" clip-path="url(#terminal-1104329960-line-17)">1</text><text class="terminal-1104329960-r1" x="463.6" y="434.8" textLength="12.2" clip-path="url(#terminal-1104329960-line-17)">,</text><text class="terminal-1104329960-r5" x="475.8" y="434.8" textLength="12.2" clip-path="url(#terminal-1104329960-line-17)">2</text><text class="terminal-1104329960-r1" x="488" y="434.8" textLength="12.2" clip-path="url(#terminal-1104329960-line-17)">,</text><text class="terminal-1104329960-r5" x="500.2" y="434.8" textLength="12.2" clip-path="url(#terminal-1104329960-line-17)">3</text><text class="terminal-1104329960-r1" x="512.4" y="434.8" textLength="231.8" clip-path="url(#terminal-1104329960-line-17)"> bump. See codes at</text><text class="terminal-1104329960-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-1104329960-line-17)"> +</text><text class="terminal-1104329960-r4" x="292.8" y="459.2" textLength="231.8" clip-path="url(#terminal-1104329960-line-18)">https://commitizen-</text><text class="terminal-1104329960-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-1104329960-line-18)"> +</text><text class="terminal-1104329960-r1" x="0" y="483.6" textLength="756.4" clip-path="url(#terminal-1104329960-line-19)">                        tools.github.io/commitizen/exit_codes/</text><text class="terminal-1104329960-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-1104329960-line-19)"> +</text><text class="terminal-1104329960-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-1104329960-line-20)"> +</text><text class="terminal-1104329960-r1" x="0" y="532.4" textLength="109.8" clip-path="url(#terminal-1104329960-line-21)">commands:</text><text class="terminal-1104329960-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-21)"> +</text><text class="terminal-1104329960-r2" x="24.4" y="556.8" textLength="12.2" clip-path="url(#terminal-1104329960-line-22)">{</text><text class="terminal-1104329960-r1" x="36.6" y="556.8" textLength="829.6" clip-path="url(#terminal-1104329960-line-22)">init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version</text><text class="terminal-1104329960-r2" x="866.2" y="556.8" textLength="12.2" clip-path="url(#terminal-1104329960-line-22)">}</text><text class="terminal-1104329960-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-1104329960-line-22)"> +</text><text class="terminal-1104329960-r1" x="0" y="581.2" textLength="732" clip-path="url(#terminal-1104329960-line-23)">    init                Initialize commitizen configuration.</text><text class="terminal-1104329960-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-1104329960-line-23)"> +</text><text class="terminal-1104329960-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-1104329960-line-24)">    commit </text><text class="terminal-1104329960-r2" x="134.2" y="605.6" textLength="12.2" clip-path="url(#terminal-1104329960-line-24)">(</text><text class="terminal-1104329960-r1" x="146.4" y="605.6" textLength="12.2" clip-path="url(#terminal-1104329960-line-24)">c</text><text class="terminal-1104329960-r2" x="158.6" y="605.6" textLength="12.2" clip-path="url(#terminal-1104329960-line-24)">)</text><text class="terminal-1104329960-r1" x="170.8" y="605.6" textLength="341.6" clip-path="url(#terminal-1104329960-line-24)">          Create new commit.</text><text class="terminal-1104329960-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-1104329960-line-24)"> +</text><text class="terminal-1104329960-r1" x="0" y="630" textLength="622.2" clip-path="url(#terminal-1104329960-line-25)">    ls                  Show available Commitizens.</text><text class="terminal-1104329960-r1" x="976" y="630" textLength="12.2" clip-path="url(#terminal-1104329960-line-25)"> +</text><text class="terminal-1104329960-r1" x="0" y="654.4" textLength="536.8" clip-path="url(#terminal-1104329960-line-26)">    example             Show commit example.</text><text class="terminal-1104329960-r1" x="976" y="654.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-26)"> +</text><text class="terminal-1104329960-r1" x="0" y="678.8" textLength="658.8" clip-path="url(#terminal-1104329960-line-27)">    info                Show information about the cz.</text><text class="terminal-1104329960-r1" x="976" y="678.8" textLength="12.2" clip-path="url(#terminal-1104329960-line-27)"> +</text><text class="terminal-1104329960-r1" x="0" y="703.2" textLength="524.6" clip-path="url(#terminal-1104329960-line-28)">    schema              Show commit schema.</text><text class="terminal-1104329960-r1" x="976" y="703.2" textLength="12.2" clip-path="url(#terminal-1104329960-line-28)"> +</text><text class="terminal-1104329960-r1" x="0" y="727.6" textLength="817.4" clip-path="url(#terminal-1104329960-line-29)">    bump                Bump semantic version based on the git log.</text><text class="terminal-1104329960-r1" x="976" y="727.6" textLength="12.2" clip-path="url(#terminal-1104329960-line-29)"> +</text><text class="terminal-1104329960-r1" x="0" y="752" textLength="170.8" clip-path="url(#terminal-1104329960-line-30)">    changelog </text><text class="terminal-1104329960-r2" x="170.8" y="752" textLength="12.2" clip-path="url(#terminal-1104329960-line-30)">(</text><text class="terminal-1104329960-r1" x="183" y="752" textLength="24.4" clip-path="url(#terminal-1104329960-line-30)">ch</text><text class="terminal-1104329960-r2" x="207.4" y="752" textLength="12.2" clip-path="url(#terminal-1104329960-line-30)">)</text><text class="terminal-1104329960-r1" x="219.6" y="752" textLength="305" clip-path="url(#terminal-1104329960-line-30)">      Generate changelog </text><text class="terminal-1104329960-r2" x="524.6" y="752" textLength="12.2" clip-path="url(#terminal-1104329960-line-30)">(</text><text class="terminal-1104329960-r1" x="536.8" y="752" textLength="329.4" clip-path="url(#terminal-1104329960-line-30)">note that it will overwrite</text><text class="terminal-1104329960-r1" x="976" y="752" textLength="12.2" clip-path="url(#terminal-1104329960-line-30)"> +</text><text class="terminal-1104329960-r1" x="0" y="776.4" textLength="463.6" clip-path="url(#terminal-1104329960-line-31)">                        existing files</text><text class="terminal-1104329960-r2" x="463.6" y="776.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-31)">)</text><text class="terminal-1104329960-r1" x="475.8" y="776.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-31)">.</text><text class="terminal-1104329960-r1" x="976" y="776.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-31)"> +</text><text class="terminal-1104329960-r1" x="0" y="800.8" textLength="939.4" clip-path="url(#terminal-1104329960-line-32)">    check               Validate that a commit message matches the commitizen</text><text class="terminal-1104329960-r1" x="976" y="800.8" textLength="12.2" clip-path="url(#terminal-1104329960-line-32)"> +</text><text class="terminal-1104329960-r1" x="0" y="825.2" textLength="378.2" clip-path="url(#terminal-1104329960-line-33)">                        schema.</text><text class="terminal-1104329960-r1" x="976" y="825.2" textLength="12.2" clip-path="url(#terminal-1104329960-line-33)"> +</text><text class="terminal-1104329960-r1" x="0" y="849.6" textLength="902.8" clip-path="url(#terminal-1104329960-line-34)">    version             Get the version of the installed commitizen or the</text><text class="terminal-1104329960-r1" x="976" y="849.6" textLength="12.2" clip-path="url(#terminal-1104329960-line-34)"> +</text><text class="terminal-1104329960-r1" x="0" y="874" textLength="488" clip-path="url(#terminal-1104329960-line-35)">                        current project </text><text class="terminal-1104329960-r2" x="488" y="874" textLength="12.2" clip-path="url(#terminal-1104329960-line-35)">(</text><text class="terminal-1104329960-r1" x="500.2" y="874" textLength="353.8" clip-path="url(#terminal-1104329960-line-35)">default: installed commitizen</text><text class="terminal-1104329960-r2" x="854" y="874" textLength="12.2" clip-path="url(#terminal-1104329960-line-35)">)</text><text class="terminal-1104329960-r1" x="866.2" y="874" textLength="12.2" clip-path="url(#terminal-1104329960-line-35)">.</text><text class="terminal-1104329960-r1" x="976" y="874" textLength="12.2" clip-path="url(#terminal-1104329960-line-35)"> +</text><text class="terminal-1104329960-r1" x="976" y="898.4" textLength="12.2" clip-path="url(#terminal-1104329960-line-36)"> </text> </g> </g> diff --git a/docs/images/cli_help/cz_bump___help.svg b/docs/images/cli_help/cz_bump___help.svg index 96db25a964..42f618ea9c 100644 --- a/docs/images/cli_help/cz_bump___help.svg +++ b/docs/images/cli_help/cz_bump___help.svg @@ -1,4 +1,4 @@ -<svg class="rich-terminal" viewBox="0 0 994 2026.3999999999999" xmlns="http://www.w3.org/2000/svg"> +<svg class="rich-terminal" viewBox="0 0 994 2221.6" xmlns="http://www.w3.org/2000/svg"> <!-- Generated with Rich https://www.textualize.io --> <style> @@ -19,362 +19,395 @@ font-weight: 700; } - .terminal-509574676-matrix { + .terminal-4007273841-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-509574676-title { + .terminal-4007273841-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-509574676-r1 { fill: #c5c8c6 } -.terminal-509574676-r2 { fill: #c5c8c6;font-weight: bold } -.terminal-509574676-r3 { fill: #68a0b3;font-weight: bold } -.terminal-509574676-r4 { fill: #98a84b } + .terminal-4007273841-r1 { fill: #c5c8c6 } +.terminal-4007273841-r2 { fill: #c5c8c6;font-weight: bold } +.terminal-4007273841-r3 { fill: #68a0b3;font-weight: bold } +.terminal-4007273841-r4 { fill: #98729f;font-weight: bold } +.terminal-4007273841-r5 { fill: #98a84b } </style> <defs> - <clipPath id="terminal-509574676-clip-terminal"> - <rect x="0" y="0" width="975.0" height="1975.3999999999999" /> + <clipPath id="terminal-4007273841-clip-terminal"> + <rect x="0" y="0" width="975.0" height="2170.6" /> </clipPath> - <clipPath id="terminal-509574676-line-0"> + <clipPath id="terminal-4007273841-line-0"> <rect x="0" y="1.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-1"> +<clipPath id="terminal-4007273841-line-1"> <rect x="0" y="25.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-2"> +<clipPath id="terminal-4007273841-line-2"> <rect x="0" y="50.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-3"> +<clipPath id="terminal-4007273841-line-3"> <rect x="0" y="74.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-4"> +<clipPath id="terminal-4007273841-line-4"> <rect x="0" y="99.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-5"> +<clipPath id="terminal-4007273841-line-5"> <rect x="0" y="123.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-6"> +<clipPath id="terminal-4007273841-line-6"> <rect x="0" y="147.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-7"> +<clipPath id="terminal-4007273841-line-7"> <rect x="0" y="172.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-8"> +<clipPath id="terminal-4007273841-line-8"> <rect x="0" y="196.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-9"> +<clipPath id="terminal-4007273841-line-9"> <rect x="0" y="221.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-10"> +<clipPath id="terminal-4007273841-line-10"> <rect x="0" y="245.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-11"> +<clipPath id="terminal-4007273841-line-11"> <rect x="0" y="269.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-12"> +<clipPath id="terminal-4007273841-line-12"> <rect x="0" y="294.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-13"> +<clipPath id="terminal-4007273841-line-13"> <rect x="0" y="318.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-14"> +<clipPath id="terminal-4007273841-line-14"> <rect x="0" y="343.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-15"> +<clipPath id="terminal-4007273841-line-15"> <rect x="0" y="367.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-16"> +<clipPath id="terminal-4007273841-line-16"> <rect x="0" y="391.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-17"> +<clipPath id="terminal-4007273841-line-17"> <rect x="0" y="416.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-18"> +<clipPath id="terminal-4007273841-line-18"> <rect x="0" y="440.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-19"> +<clipPath id="terminal-4007273841-line-19"> <rect x="0" y="465.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-20"> +<clipPath id="terminal-4007273841-line-20"> <rect x="0" y="489.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-21"> +<clipPath id="terminal-4007273841-line-21"> <rect x="0" y="513.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-22"> +<clipPath id="terminal-4007273841-line-22"> <rect x="0" y="538.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-23"> +<clipPath id="terminal-4007273841-line-23"> <rect x="0" y="562.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-24"> +<clipPath id="terminal-4007273841-line-24"> <rect x="0" y="587.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-25"> +<clipPath id="terminal-4007273841-line-25"> <rect x="0" y="611.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-26"> +<clipPath id="terminal-4007273841-line-26"> <rect x="0" y="635.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-27"> +<clipPath id="terminal-4007273841-line-27"> <rect x="0" y="660.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-28"> +<clipPath id="terminal-4007273841-line-28"> <rect x="0" y="684.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-29"> +<clipPath id="terminal-4007273841-line-29"> <rect x="0" y="709.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-30"> +<clipPath id="terminal-4007273841-line-30"> <rect x="0" y="733.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-31"> +<clipPath id="terminal-4007273841-line-31"> <rect x="0" y="757.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-32"> +<clipPath id="terminal-4007273841-line-32"> <rect x="0" y="782.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-33"> +<clipPath id="terminal-4007273841-line-33"> <rect x="0" y="806.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-34"> +<clipPath id="terminal-4007273841-line-34"> <rect x="0" y="831.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-35"> +<clipPath id="terminal-4007273841-line-35"> <rect x="0" y="855.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-36"> +<clipPath id="terminal-4007273841-line-36"> <rect x="0" y="879.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-37"> +<clipPath id="terminal-4007273841-line-37"> <rect x="0" y="904.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-38"> +<clipPath id="terminal-4007273841-line-38"> <rect x="0" y="928.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-39"> +<clipPath id="terminal-4007273841-line-39"> <rect x="0" y="953.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-40"> +<clipPath id="terminal-4007273841-line-40"> <rect x="0" y="977.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-41"> +<clipPath id="terminal-4007273841-line-41"> <rect x="0" y="1001.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-42"> +<clipPath id="terminal-4007273841-line-42"> <rect x="0" y="1026.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-43"> +<clipPath id="terminal-4007273841-line-43"> <rect x="0" y="1050.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-44"> +<clipPath id="terminal-4007273841-line-44"> <rect x="0" y="1075.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-45"> +<clipPath id="terminal-4007273841-line-45"> <rect x="0" y="1099.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-46"> +<clipPath id="terminal-4007273841-line-46"> <rect x="0" y="1123.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-47"> +<clipPath id="terminal-4007273841-line-47"> <rect x="0" y="1148.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-48"> +<clipPath id="terminal-4007273841-line-48"> <rect x="0" y="1172.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-49"> +<clipPath id="terminal-4007273841-line-49"> <rect x="0" y="1197.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-50"> +<clipPath id="terminal-4007273841-line-50"> <rect x="0" y="1221.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-51"> +<clipPath id="terminal-4007273841-line-51"> <rect x="0" y="1245.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-52"> +<clipPath id="terminal-4007273841-line-52"> <rect x="0" y="1270.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-53"> +<clipPath id="terminal-4007273841-line-53"> <rect x="0" y="1294.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-54"> +<clipPath id="terminal-4007273841-line-54"> <rect x="0" y="1319.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-55"> +<clipPath id="terminal-4007273841-line-55"> <rect x="0" y="1343.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-56"> +<clipPath id="terminal-4007273841-line-56"> <rect x="0" y="1367.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-57"> +<clipPath id="terminal-4007273841-line-57"> <rect x="0" y="1392.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-58"> +<clipPath id="terminal-4007273841-line-58"> <rect x="0" y="1416.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-59"> +<clipPath id="terminal-4007273841-line-59"> <rect x="0" y="1441.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-60"> +<clipPath id="terminal-4007273841-line-60"> <rect x="0" y="1465.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-61"> +<clipPath id="terminal-4007273841-line-61"> <rect x="0" y="1489.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-62"> +<clipPath id="terminal-4007273841-line-62"> <rect x="0" y="1514.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-63"> +<clipPath id="terminal-4007273841-line-63"> <rect x="0" y="1538.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-64"> +<clipPath id="terminal-4007273841-line-64"> <rect x="0" y="1563.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-65"> +<clipPath id="terminal-4007273841-line-65"> <rect x="0" y="1587.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-66"> +<clipPath id="terminal-4007273841-line-66"> <rect x="0" y="1611.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-67"> +<clipPath id="terminal-4007273841-line-67"> <rect x="0" y="1636.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-68"> +<clipPath id="terminal-4007273841-line-68"> <rect x="0" y="1660.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-69"> +<clipPath id="terminal-4007273841-line-69"> <rect x="0" y="1685.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-70"> +<clipPath id="terminal-4007273841-line-70"> <rect x="0" y="1709.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-71"> +<clipPath id="terminal-4007273841-line-71"> <rect x="0" y="1733.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-72"> +<clipPath id="terminal-4007273841-line-72"> <rect x="0" y="1758.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-73"> +<clipPath id="terminal-4007273841-line-73"> <rect x="0" y="1782.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-74"> +<clipPath id="terminal-4007273841-line-74"> <rect x="0" y="1807.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-75"> +<clipPath id="terminal-4007273841-line-75"> <rect x="0" y="1831.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-76"> +<clipPath id="terminal-4007273841-line-76"> <rect x="0" y="1855.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-77"> +<clipPath id="terminal-4007273841-line-77"> <rect x="0" y="1880.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-78"> +<clipPath id="terminal-4007273841-line-78"> <rect x="0" y="1904.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-509574676-line-79"> +<clipPath id="terminal-4007273841-line-79"> <rect x="0" y="1929.1" width="976" height="24.65"/> </clipPath> +<clipPath id="terminal-4007273841-line-80"> + <rect x="0" y="1953.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4007273841-line-81"> + <rect x="0" y="1977.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4007273841-line-82"> + <rect x="0" y="2002.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4007273841-line-83"> + <rect x="0" y="2026.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4007273841-line-84"> + <rect x="0" y="2051.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4007273841-line-85"> + <rect x="0" y="2075.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4007273841-line-86"> + <rect x="0" y="2099.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-4007273841-line-87"> + <rect x="0" y="2124.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="2024.4" rx="8"/> + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="2219.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-509574676-clip-terminal)"> + <g transform="translate(9, 41)" clip-path="url(#terminal-4007273841-clip-terminal)"> - <g class="terminal-509574676-matrix"> - <text class="terminal-509574676-r1" x="0" y="20" textLength="195.2" clip-path="url(#terminal-509574676-line-0)">$ cz bump --help</text><text class="terminal-509574676-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-509574676-line-0)"> -</text><text class="terminal-509574676-r1" x="0" y="44.4" textLength="183" clip-path="url(#terminal-509574676-line-1)">usage: cz bump </text><text class="terminal-509574676-r2" x="183" y="44.4" textLength="12.2" clip-path="url(#terminal-509574676-line-1)">[</text><text class="terminal-509574676-r1" x="195.2" y="44.4" textLength="24.4" clip-path="url(#terminal-509574676-line-1)">-h</text><text class="terminal-509574676-r2" x="219.6" y="44.4" textLength="12.2" clip-path="url(#terminal-509574676-line-1)">]</text><text class="terminal-509574676-r2" x="244" y="44.4" textLength="12.2" clip-path="url(#terminal-509574676-line-1)">[</text><text class="terminal-509574676-r1" x="256.2" y="44.4" textLength="109.8" clip-path="url(#terminal-509574676-line-1)">--dry-run</text><text class="terminal-509574676-r2" x="366" y="44.4" textLength="12.2" clip-path="url(#terminal-509574676-line-1)">]</text><text class="terminal-509574676-r2" x="390.4" y="44.4" textLength="12.2" clip-path="url(#terminal-509574676-line-1)">[</text><text class="terminal-509574676-r1" x="402.6" y="44.4" textLength="146.4" clip-path="url(#terminal-509574676-line-1)">--files-only</text><text class="terminal-509574676-r2" x="549" y="44.4" textLength="12.2" clip-path="url(#terminal-509574676-line-1)">]</text><text class="terminal-509574676-r2" x="573.4" y="44.4" textLength="12.2" clip-path="url(#terminal-509574676-line-1)">[</text><text class="terminal-509574676-r1" x="585.6" y="44.4" textLength="183" clip-path="url(#terminal-509574676-line-1)">--local-version</text><text class="terminal-509574676-r2" x="768.6" y="44.4" textLength="12.2" clip-path="url(#terminal-509574676-line-1)">]</text><text class="terminal-509574676-r2" x="793" y="44.4" textLength="12.2" clip-path="url(#terminal-509574676-line-1)">[</text><text class="terminal-509574676-r1" x="805.2" y="44.4" textLength="134.2" clip-path="url(#terminal-509574676-line-1)">--changelog</text><text class="terminal-509574676-r2" x="939.4" y="44.4" textLength="12.2" clip-path="url(#terminal-509574676-line-1)">]</text><text class="terminal-509574676-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-509574676-line-1)"> -</text><text class="terminal-509574676-r2" x="183" y="68.8" textLength="12.2" clip-path="url(#terminal-509574676-line-2)">[</text><text class="terminal-509574676-r1" x="195.2" y="68.8" textLength="134.2" clip-path="url(#terminal-509574676-line-2)">--no-verify</text><text class="terminal-509574676-r2" x="329.4" y="68.8" textLength="12.2" clip-path="url(#terminal-509574676-line-2)">]</text><text class="terminal-509574676-r2" x="353.8" y="68.8" textLength="12.2" clip-path="url(#terminal-509574676-line-2)">[</text><text class="terminal-509574676-r1" x="366" y="68.8" textLength="61" clip-path="url(#terminal-509574676-line-2)">--yes</text><text class="terminal-509574676-r2" x="427" y="68.8" textLength="12.2" clip-path="url(#terminal-509574676-line-2)">]</text><text class="terminal-509574676-r2" x="451.4" y="68.8" textLength="12.2" clip-path="url(#terminal-509574676-line-2)">[</text><text class="terminal-509574676-r1" x="463.6" y="68.8" textLength="280.6" clip-path="url(#terminal-509574676-line-2)">--tag-format TAG_FORMAT</text><text class="terminal-509574676-r2" x="744.2" y="68.8" textLength="12.2" clip-path="url(#terminal-509574676-line-2)">]</text><text class="terminal-509574676-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-509574676-line-2)"> -</text><text class="terminal-509574676-r2" x="183" y="93.2" textLength="12.2" clip-path="url(#terminal-509574676-line-3)">[</text><text class="terminal-509574676-r1" x="195.2" y="93.2" textLength="329.4" clip-path="url(#terminal-509574676-line-3)">--bump-message BUMP_MESSAGE</text><text class="terminal-509574676-r2" x="524.6" y="93.2" textLength="12.2" clip-path="url(#terminal-509574676-line-3)">]</text><text class="terminal-509574676-r2" x="549" y="93.2" textLength="12.2" clip-path="url(#terminal-509574676-line-3)">[</text><text class="terminal-509574676-r1" x="561.2" y="93.2" textLength="158.6" clip-path="url(#terminal-509574676-line-3)">--prerelease </text><text class="terminal-509574676-r2" x="719.8" y="93.2" textLength="12.2" clip-path="url(#terminal-509574676-line-3)">{</text><text class="terminal-509574676-r1" x="732" y="93.2" textLength="158.6" clip-path="url(#terminal-509574676-line-3)">alpha,beta,rc</text><text class="terminal-509574676-r2" x="890.6" y="93.2" textLength="12.2" clip-path="url(#terminal-509574676-line-3)">}</text><text class="terminal-509574676-r2" x="902.8" y="93.2" textLength="12.2" clip-path="url(#terminal-509574676-line-3)">]</text><text class="terminal-509574676-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-509574676-line-3)"> -</text><text class="terminal-509574676-r2" x="183" y="117.6" textLength="12.2" clip-path="url(#terminal-509574676-line-4)">[</text><text class="terminal-509574676-r1" x="195.2" y="117.6" textLength="280.6" clip-path="url(#terminal-509574676-line-4)">--devrelease DEVRELEASE</text><text class="terminal-509574676-r2" x="475.8" y="117.6" textLength="12.2" clip-path="url(#terminal-509574676-line-4)">]</text><text class="terminal-509574676-r2" x="500.2" y="117.6" textLength="12.2" clip-path="url(#terminal-509574676-line-4)">[</text><text class="terminal-509574676-r1" x="512.4" y="117.6" textLength="146.4" clip-path="url(#terminal-509574676-line-4)">--increment </text><text class="terminal-509574676-r2" x="658.8" y="117.6" textLength="12.2" clip-path="url(#terminal-509574676-line-4)">{</text><text class="terminal-509574676-r1" x="671" y="117.6" textLength="207.4" clip-path="url(#terminal-509574676-line-4)">MAJOR,MINOR,PATCH</text><text class="terminal-509574676-r2" x="878.4" y="117.6" textLength="12.2" clip-path="url(#terminal-509574676-line-4)">}</text><text class="terminal-509574676-r2" x="890.6" y="117.6" textLength="12.2" clip-path="url(#terminal-509574676-line-4)">]</text><text class="terminal-509574676-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-509574676-line-4)"> -</text><text class="terminal-509574676-r2" x="183" y="142" textLength="12.2" clip-path="url(#terminal-509574676-line-5)">[</text><text class="terminal-509574676-r1" x="195.2" y="142" textLength="207.4" clip-path="url(#terminal-509574676-line-5)">--increment-mode </text><text class="terminal-509574676-r2" x="402.6" y="142" textLength="12.2" clip-path="url(#terminal-509574676-line-5)">{</text><text class="terminal-509574676-r1" x="414.8" y="142" textLength="146.4" clip-path="url(#terminal-509574676-line-5)">linear,exact</text><text class="terminal-509574676-r2" x="561.2" y="142" textLength="12.2" clip-path="url(#terminal-509574676-line-5)">}</text><text class="terminal-509574676-r2" x="573.4" y="142" textLength="12.2" clip-path="url(#terminal-509574676-line-5)">]</text><text class="terminal-509574676-r2" x="597.8" y="142" textLength="12.2" clip-path="url(#terminal-509574676-line-5)">[</text><text class="terminal-509574676-r1" x="610" y="142" textLength="231.8" clip-path="url(#terminal-509574676-line-5)">--check-consistency</text><text class="terminal-509574676-r2" x="841.8" y="142" textLength="12.2" clip-path="url(#terminal-509574676-line-5)">]</text><text class="terminal-509574676-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-509574676-line-5)"> -</text><text class="terminal-509574676-r2" x="183" y="166.4" textLength="12.2" clip-path="url(#terminal-509574676-line-6)">[</text><text class="terminal-509574676-r1" x="195.2" y="166.4" textLength="183" clip-path="url(#terminal-509574676-line-6)">--annotated-tag</text><text class="terminal-509574676-r2" x="378.2" y="166.4" textLength="12.2" clip-path="url(#terminal-509574676-line-6)">]</text><text class="terminal-509574676-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-509574676-line-6)"> -</text><text class="terminal-509574676-r2" x="183" y="190.8" textLength="12.2" clip-path="url(#terminal-509574676-line-7)">[</text><text class="terminal-509574676-r1" x="195.2" y="190.8" textLength="549" clip-path="url(#terminal-509574676-line-7)">--annotated-tag-message ANNOTATED_TAG_MESSAGE</text><text class="terminal-509574676-r2" x="744.2" y="190.8" textLength="12.2" clip-path="url(#terminal-509574676-line-7)">]</text><text class="terminal-509574676-r2" x="768.6" y="190.8" textLength="12.2" clip-path="url(#terminal-509574676-line-7)">[</text><text class="terminal-509574676-r1" x="780.8" y="190.8" textLength="122" clip-path="url(#terminal-509574676-line-7)">--gpg-sign</text><text class="terminal-509574676-r2" x="902.8" y="190.8" textLength="12.2" clip-path="url(#terminal-509574676-line-7)">]</text><text class="terminal-509574676-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-509574676-line-7)"> -</text><text class="terminal-509574676-r2" x="183" y="215.2" textLength="12.2" clip-path="url(#terminal-509574676-line-8)">[</text><text class="terminal-509574676-r1" x="195.2" y="215.2" textLength="256.2" clip-path="url(#terminal-509574676-line-8)">--changelog-to-stdout</text><text class="terminal-509574676-r2" x="451.4" y="215.2" textLength="12.2" clip-path="url(#terminal-509574676-line-8)">]</text><text class="terminal-509574676-r2" x="475.8" y="215.2" textLength="12.2" clip-path="url(#terminal-509574676-line-8)">[</text><text class="terminal-509574676-r1" x="488" y="215.2" textLength="268.4" clip-path="url(#terminal-509574676-line-8)">--git-output-to-stderr</text><text class="terminal-509574676-r2" x="756.4" y="215.2" textLength="12.2" clip-path="url(#terminal-509574676-line-8)">]</text><text class="terminal-509574676-r2" x="780.8" y="215.2" textLength="12.2" clip-path="url(#terminal-509574676-line-8)">[</text><text class="terminal-509574676-r1" x="793" y="215.2" textLength="85.4" clip-path="url(#terminal-509574676-line-8)">--retry</text><text class="terminal-509574676-r2" x="878.4" y="215.2" textLength="12.2" clip-path="url(#terminal-509574676-line-8)">]</text><text class="terminal-509574676-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-509574676-line-8)"> -</text><text class="terminal-509574676-r2" x="183" y="239.6" textLength="12.2" clip-path="url(#terminal-509574676-line-9)">[</text><text class="terminal-509574676-r1" x="195.2" y="239.6" textLength="244" clip-path="url(#terminal-509574676-line-9)">--major-version-zero</text><text class="terminal-509574676-r2" x="439.2" y="239.6" textLength="12.2" clip-path="url(#terminal-509574676-line-9)">]</text><text class="terminal-509574676-r2" x="463.6" y="239.6" textLength="12.2" clip-path="url(#terminal-509574676-line-9)">[</text><text class="terminal-509574676-r1" x="475.8" y="239.6" textLength="231.8" clip-path="url(#terminal-509574676-line-9)">--template TEMPLATE</text><text class="terminal-509574676-r2" x="707.6" y="239.6" textLength="12.2" clip-path="url(#terminal-509574676-line-9)">]</text><text class="terminal-509574676-r2" x="732" y="239.6" textLength="12.2" clip-path="url(#terminal-509574676-line-9)">[</text><text class="terminal-509574676-r1" x="744.2" y="239.6" textLength="158.6" clip-path="url(#terminal-509574676-line-9)">--extra EXTRA</text><text class="terminal-509574676-r2" x="902.8" y="239.6" textLength="12.2" clip-path="url(#terminal-509574676-line-9)">]</text><text class="terminal-509574676-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-509574676-line-9)"> -</text><text class="terminal-509574676-r2" x="183" y="264" textLength="12.2" clip-path="url(#terminal-509574676-line-10)">[</text><text class="terminal-509574676-r1" x="195.2" y="264" textLength="256.2" clip-path="url(#terminal-509574676-line-10)">--file-name FILE_NAME</text><text class="terminal-509574676-r2" x="451.4" y="264" textLength="12.2" clip-path="url(#terminal-509574676-line-10)">]</text><text class="terminal-509574676-r2" x="475.8" y="264" textLength="12.2" clip-path="url(#terminal-509574676-line-10)">[</text><text class="terminal-509574676-r1" x="488" y="264" textLength="451.4" clip-path="url(#terminal-509574676-line-10)">--prerelease-offset PRERELEASE_OFFSET</text><text class="terminal-509574676-r2" x="939.4" y="264" textLength="12.2" clip-path="url(#terminal-509574676-line-10)">]</text><text class="terminal-509574676-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-509574676-line-10)"> -</text><text class="terminal-509574676-r2" x="183" y="288.4" textLength="12.2" clip-path="url(#terminal-509574676-line-11)">[</text><text class="terminal-509574676-r1" x="195.2" y="288.4" textLength="207.4" clip-path="url(#terminal-509574676-line-11)">--version-scheme </text><text class="terminal-509574676-r2" x="402.6" y="288.4" textLength="12.2" clip-path="url(#terminal-509574676-line-11)">{</text><text class="terminal-509574676-r1" x="414.8" y="288.4" textLength="256.2" clip-path="url(#terminal-509574676-line-11)">pep440,semver,semver2</text><text class="terminal-509574676-r2" x="671" y="288.4" textLength="12.2" clip-path="url(#terminal-509574676-line-11)">}</text><text class="terminal-509574676-r2" x="683.2" y="288.4" textLength="12.2" clip-path="url(#terminal-509574676-line-11)">]</text><text class="terminal-509574676-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-509574676-line-11)"> -</text><text class="terminal-509574676-r2" x="183" y="312.8" textLength="12.2" clip-path="url(#terminal-509574676-line-12)">[</text><text class="terminal-509574676-r1" x="195.2" y="312.8" textLength="183" clip-path="url(#terminal-509574676-line-12)">--version-type </text><text class="terminal-509574676-r2" x="378.2" y="312.8" textLength="12.2" clip-path="url(#terminal-509574676-line-12)">{</text><text class="terminal-509574676-r1" x="390.4" y="312.8" textLength="256.2" clip-path="url(#terminal-509574676-line-12)">pep440,semver,semver2</text><text class="terminal-509574676-r2" x="646.6" y="312.8" textLength="12.2" clip-path="url(#terminal-509574676-line-12)">}</text><text class="terminal-509574676-r2" x="658.8" y="312.8" textLength="12.2" clip-path="url(#terminal-509574676-line-12)">]</text><text class="terminal-509574676-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-509574676-line-12)"> -</text><text class="terminal-509574676-r2" x="183" y="337.2" textLength="12.2" clip-path="url(#terminal-509574676-line-13)">[</text><text class="terminal-509574676-r1" x="195.2" y="337.2" textLength="378.2" clip-path="url(#terminal-509574676-line-13)">--build-metadata BUILD_METADATA</text><text class="terminal-509574676-r2" x="573.4" y="337.2" textLength="12.2" clip-path="url(#terminal-509574676-line-13)">]</text><text class="terminal-509574676-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-509574676-line-13)"> -</text><text class="terminal-509574676-r2" x="183" y="361.6" textLength="12.2" clip-path="url(#terminal-509574676-line-14)">[</text><text class="terminal-509574676-r1" x="195.2" y="361.6" textLength="170.8" clip-path="url(#terminal-509574676-line-14)">MANUAL_VERSION</text><text class="terminal-509574676-r2" x="366" y="361.6" textLength="12.2" clip-path="url(#terminal-509574676-line-14)">]</text><text class="terminal-509574676-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-509574676-line-14)"> -</text><text class="terminal-509574676-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-509574676-line-15)"> -</text><text class="terminal-509574676-r1" x="0" y="410.4" textLength="512.4" clip-path="url(#terminal-509574676-line-16)">bump semantic version based on the git log</text><text class="terminal-509574676-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-509574676-line-16)"> -</text><text class="terminal-509574676-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-509574676-line-17)"> -</text><text class="terminal-509574676-r1" x="0" y="459.2" textLength="256.2" clip-path="url(#terminal-509574676-line-18)">positional arguments:</text><text class="terminal-509574676-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-509574676-line-18)"> -</text><text class="terminal-509574676-r1" x="0" y="483.6" textLength="610" clip-path="url(#terminal-509574676-line-19)">  MANUAL_VERSION        bump to the given version </text><text class="terminal-509574676-r2" x="610" y="483.6" textLength="12.2" clip-path="url(#terminal-509574676-line-19)">(</text><text class="terminal-509574676-r1" x="622.2" y="483.6" textLength="61" clip-path="url(#terminal-509574676-line-19)">e.g: </text><text class="terminal-509574676-r3" x="683.2" y="483.6" textLength="36.6" clip-path="url(#terminal-509574676-line-19)">1.5</text><text class="terminal-509574676-r1" x="719.8" y="483.6" textLength="12.2" clip-path="url(#terminal-509574676-line-19)">.</text><text class="terminal-509574676-r3" x="732" y="483.6" textLength="12.2" clip-path="url(#terminal-509574676-line-19)">3</text><text class="terminal-509574676-r2" x="744.2" y="483.6" textLength="12.2" clip-path="url(#terminal-509574676-line-19)">)</text><text class="terminal-509574676-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-509574676-line-19)"> -</text><text class="terminal-509574676-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-509574676-line-20)"> -</text><text class="terminal-509574676-r1" x="0" y="532.4" textLength="97.6" clip-path="url(#terminal-509574676-line-21)">options:</text><text class="terminal-509574676-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-509574676-line-21)"> -</text><text class="terminal-509574676-r1" x="0" y="556.8" textLength="671" clip-path="url(#terminal-509574676-line-22)">  -h, --help            show this help message and exit</text><text class="terminal-509574676-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-509574676-line-22)"> -</text><text class="terminal-509574676-r1" x="0" y="581.2" textLength="915" clip-path="url(#terminal-509574676-line-23)">  --dry-run             show output to stdout, no commit, no modified files</text><text class="terminal-509574676-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-509574676-line-23)"> -</text><text class="terminal-509574676-r1" x="0" y="605.6" textLength="793" clip-path="url(#terminal-509574676-line-24)">  --files-only          bump version in the files from the config</text><text class="terminal-509574676-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-509574676-line-24)"> -</text><text class="terminal-509574676-r1" x="0" y="630" textLength="719.8" clip-path="url(#terminal-509574676-line-25)">  --local-version       bump only the local version portion</text><text class="terminal-509574676-r1" x="976" y="630" textLength="12.2" clip-path="url(#terminal-509574676-line-25)"> -</text><text class="terminal-509574676-r1" x="0" y="654.4" textLength="841.8" clip-path="url(#terminal-509574676-line-26)">  --changelog, -ch      generate the changelog for the newest version</text><text class="terminal-509574676-r1" x="976" y="654.4" textLength="12.2" clip-path="url(#terminal-509574676-line-26)"> -</text><text class="terminal-509574676-r1" x="0" y="678.8" textLength="902.8" clip-path="url(#terminal-509574676-line-27)">  --no-verify           this option bypasses the pre-commit and commit-msg</text><text class="terminal-509574676-r1" x="976" y="678.8" textLength="12.2" clip-path="url(#terminal-509574676-line-27)"> -</text><text class="terminal-509574676-r1" x="0" y="703.2" textLength="353.8" clip-path="url(#terminal-509574676-line-28)">                        hooks</text><text class="terminal-509574676-r1" x="976" y="703.2" textLength="12.2" clip-path="url(#terminal-509574676-line-28)"> -</text><text class="terminal-509574676-r1" x="0" y="727.6" textLength="719.8" clip-path="url(#terminal-509574676-line-29)">  --yes                 accept automatically questions done</text><text class="terminal-509574676-r1" x="976" y="727.6" textLength="12.2" clip-path="url(#terminal-509574676-line-29)"> -</text><text class="terminal-509574676-r1" x="0" y="752" textLength="305" clip-path="url(#terminal-509574676-line-30)">  --tag-format TAG_FORMAT</text><text class="terminal-509574676-r1" x="976" y="752" textLength="12.2" clip-path="url(#terminal-509574676-line-30)"> -</text><text class="terminal-509574676-r1" x="0" y="776.4" textLength="939.4" clip-path="url(#terminal-509574676-line-31)">                        the format used to tag the commit and read it, use it</text><text class="terminal-509574676-r1" x="976" y="776.4" textLength="12.2" clip-path="url(#terminal-509574676-line-31)"> -</text><text class="terminal-509574676-r1" x="0" y="800.8" textLength="866.2" clip-path="url(#terminal-509574676-line-32)">                        in existing projects, wrap around simple quotes</text><text class="terminal-509574676-r1" x="976" y="800.8" textLength="12.2" clip-path="url(#terminal-509574676-line-32)"> -</text><text class="terminal-509574676-r1" x="0" y="825.2" textLength="353.8" clip-path="url(#terminal-509574676-line-33)">  --bump-message BUMP_MESSAGE</text><text class="terminal-509574676-r1" x="976" y="825.2" textLength="12.2" clip-path="url(#terminal-509574676-line-33)"> -</text><text class="terminal-509574676-r1" x="0" y="849.6" textLength="902.8" clip-path="url(#terminal-509574676-line-34)">                        template used to create the release commit, useful</text><text class="terminal-509574676-r1" x="976" y="849.6" textLength="12.2" clip-path="url(#terminal-509574676-line-34)"> -</text><text class="terminal-509574676-r1" x="0" y="874" textLength="536.8" clip-path="url(#terminal-509574676-line-35)">                        when working with CI</text><text class="terminal-509574676-r1" x="976" y="874" textLength="12.2" clip-path="url(#terminal-509574676-line-35)"> -</text><text class="terminal-509574676-r1" x="0" y="898.4" textLength="183" clip-path="url(#terminal-509574676-line-36)">  --prerelease </text><text class="terminal-509574676-r2" x="183" y="898.4" textLength="12.2" clip-path="url(#terminal-509574676-line-36)">{</text><text class="terminal-509574676-r1" x="195.2" y="898.4" textLength="158.6" clip-path="url(#terminal-509574676-line-36)">alpha,beta,rc</text><text class="terminal-509574676-r2" x="353.8" y="898.4" textLength="12.2" clip-path="url(#terminal-509574676-line-36)">}</text><text class="terminal-509574676-r1" x="366" y="898.4" textLength="73.2" clip-path="url(#terminal-509574676-line-36)">, -pr </text><text class="terminal-509574676-r2" x="439.2" y="898.4" textLength="12.2" clip-path="url(#terminal-509574676-line-36)">{</text><text class="terminal-509574676-r1" x="451.4" y="898.4" textLength="158.6" clip-path="url(#terminal-509574676-line-36)">alpha,beta,rc</text><text class="terminal-509574676-r2" x="610" y="898.4" textLength="12.2" clip-path="url(#terminal-509574676-line-36)">}</text><text class="terminal-509574676-r1" x="976" y="898.4" textLength="12.2" clip-path="url(#terminal-509574676-line-36)"> -</text><text class="terminal-509574676-r1" x="0" y="922.8" textLength="597.8" clip-path="url(#terminal-509574676-line-37)">                        choose type of prerelease</text><text class="terminal-509574676-r1" x="976" y="922.8" textLength="12.2" clip-path="url(#terminal-509574676-line-37)"> -</text><text class="terminal-509574676-r1" x="0" y="947.2" textLength="488" clip-path="url(#terminal-509574676-line-38)">  --devrelease DEVRELEASE, -d DEVRELEASE</text><text class="terminal-509574676-r1" x="976" y="947.2" textLength="12.2" clip-path="url(#terminal-509574676-line-38)"> -</text><text class="terminal-509574676-r1" x="0" y="971.6" textLength="841.8" clip-path="url(#terminal-509574676-line-39)">                        specify non-negative integer for dev. release</text><text class="terminal-509574676-r1" x="976" y="971.6" textLength="12.2" clip-path="url(#terminal-509574676-line-39)"> -</text><text class="terminal-509574676-r1" x="0" y="996" textLength="170.8" clip-path="url(#terminal-509574676-line-40)">  --increment </text><text class="terminal-509574676-r2" x="170.8" y="996" textLength="12.2" clip-path="url(#terminal-509574676-line-40)">{</text><text class="terminal-509574676-r1" x="183" y="996" textLength="207.4" clip-path="url(#terminal-509574676-line-40)">MAJOR,MINOR,PATCH</text><text class="terminal-509574676-r2" x="390.4" y="996" textLength="12.2" clip-path="url(#terminal-509574676-line-40)">}</text><text class="terminal-509574676-r1" x="976" y="996" textLength="12.2" clip-path="url(#terminal-509574676-line-40)"> -</text><text class="terminal-509574676-r1" x="0" y="1020.4" textLength="756.4" clip-path="url(#terminal-509574676-line-41)">                        manually specify the desired increment</text><text class="terminal-509574676-r1" x="976" y="1020.4" textLength="12.2" clip-path="url(#terminal-509574676-line-41)"> -</text><text class="terminal-509574676-r1" x="0" y="1044.8" textLength="231.8" clip-path="url(#terminal-509574676-line-42)">  --increment-mode </text><text class="terminal-509574676-r2" x="231.8" y="1044.8" textLength="12.2" clip-path="url(#terminal-509574676-line-42)">{</text><text class="terminal-509574676-r1" x="244" y="1044.8" textLength="146.4" clip-path="url(#terminal-509574676-line-42)">linear,exact</text><text class="terminal-509574676-r2" x="390.4" y="1044.8" textLength="12.2" clip-path="url(#terminal-509574676-line-42)">}</text><text class="terminal-509574676-r1" x="976" y="1044.8" textLength="12.2" clip-path="url(#terminal-509574676-line-42)"> -</text><text class="terminal-509574676-r1" x="0" y="1069.2" textLength="902.8" clip-path="url(#terminal-509574676-line-43)">                        set the method by which the new version is chosen.</text><text class="terminal-509574676-r1" x="976" y="1069.2" textLength="12.2" clip-path="url(#terminal-509574676-line-43)"> -</text><text class="terminal-509574676-r4" x="292.8" y="1093.6" textLength="97.6" clip-path="url(#terminal-509574676-line-44)">'linear'</text><text class="terminal-509574676-r2" x="402.6" y="1093.6" textLength="12.2" clip-path="url(#terminal-509574676-line-44)">(</text><text class="terminal-509574676-r1" x="414.8" y="1093.6" textLength="85.4" clip-path="url(#terminal-509574676-line-44)">default</text><text class="terminal-509574676-r2" x="500.2" y="1093.6" textLength="12.2" clip-path="url(#terminal-509574676-line-44)">)</text><text class="terminal-509574676-r1" x="512.4" y="1093.6" textLength="414.8" clip-path="url(#terminal-509574676-line-44)"> guesses the next version based on</text><text class="terminal-509574676-r1" x="976" y="1093.6" textLength="12.2" clip-path="url(#terminal-509574676-line-44)"> -</text><text class="terminal-509574676-r1" x="0" y="1118" textLength="939.4" clip-path="url(#terminal-509574676-line-45)">                        typical linear version progression, such that bumping</text><text class="terminal-509574676-r1" x="976" y="1118" textLength="12.2" clip-path="url(#terminal-509574676-line-45)"> -</text><text class="terminal-509574676-r1" x="0" y="1142.4" textLength="866.2" clip-path="url(#terminal-509574676-line-46)">                        of a pre-release with lower precedence than the</text><text class="terminal-509574676-r1" x="976" y="1142.4" textLength="12.2" clip-path="url(#terminal-509574676-line-46)"> -</text><text class="terminal-509574676-r1" x="0" y="1166.8" textLength="939.4" clip-path="url(#terminal-509574676-line-47)">                        current pre-release phase maintains the current phase</text><text class="terminal-509574676-r1" x="976" y="1166.8" textLength="12.2" clip-path="url(#terminal-509574676-line-47)"> -</text><text class="terminal-509574676-r1" x="0" y="1191.2" textLength="561.2" clip-path="url(#terminal-509574676-line-48)">                        of higher precedence. </text><text class="terminal-509574676-r4" x="561.2" y="1191.2" textLength="85.4" clip-path="url(#terminal-509574676-line-48)">'exact'</text><text class="terminal-509574676-r1" x="646.6" y="1191.2" textLength="305" clip-path="url(#terminal-509574676-line-48)"> applies the changes that</text><text class="terminal-509574676-r1" x="976" y="1191.2" textLength="12.2" clip-path="url(#terminal-509574676-line-48)"> -</text><text class="terminal-509574676-r1" x="0" y="1215.6" textLength="536.8" clip-path="url(#terminal-509574676-line-49)">                        have been specified </text><text class="terminal-509574676-r2" x="536.8" y="1215.6" textLength="12.2" clip-path="url(#terminal-509574676-line-49)">(</text><text class="terminal-509574676-r1" x="549" y="1215.6" textLength="353.8" clip-path="url(#terminal-509574676-line-49)">or determined from the commit</text><text class="terminal-509574676-r1" x="976" y="1215.6" textLength="12.2" clip-path="url(#terminal-509574676-line-49)"> -</text><text class="terminal-509574676-r1" x="0" y="1240" textLength="329.4" clip-path="url(#terminal-509574676-line-50)">                        log</text><text class="terminal-509574676-r2" x="329.4" y="1240" textLength="12.2" clip-path="url(#terminal-509574676-line-50)">)</text><text class="terminal-509574676-r1" x="341.6" y="1240" textLength="585.6" clip-path="url(#terminal-509574676-line-50)"> without interpretation, such that the increment</text><text class="terminal-509574676-r1" x="976" y="1240" textLength="12.2" clip-path="url(#terminal-509574676-line-50)"> -</text><text class="terminal-509574676-r1" x="0" y="1264.4" textLength="707.6" clip-path="url(#terminal-509574676-line-51)">                        and pre-release are always honored</text><text class="terminal-509574676-r1" x="976" y="1264.4" textLength="12.2" clip-path="url(#terminal-509574676-line-51)"> -</text><text class="terminal-509574676-r1" x="0" y="1288.8" textLength="317.2" clip-path="url(#terminal-509574676-line-52)">  --check-consistency, -cc</text><text class="terminal-509574676-r1" x="976" y="1288.8" textLength="12.2" clip-path="url(#terminal-509574676-line-52)"> -</text><text class="terminal-509574676-r1" x="0" y="1313.2" textLength="951.6" clip-path="url(#terminal-509574676-line-53)">                        check consistency among versions defined in commitizen</text><text class="terminal-509574676-r1" x="976" y="1313.2" textLength="12.2" clip-path="url(#terminal-509574676-line-53)"> -</text><text class="terminal-509574676-r1" x="0" y="1337.6" textLength="671" clip-path="url(#terminal-509574676-line-54)">                        configuration and version_files</text><text class="terminal-509574676-r1" x="976" y="1337.6" textLength="12.2" clip-path="url(#terminal-509574676-line-54)"> -</text><text class="terminal-509574676-r1" x="0" y="1362" textLength="866.2" clip-path="url(#terminal-509574676-line-55)">  --annotated-tag, -at  create annotated tag instead of lightweight one</text><text class="terminal-509574676-r1" x="976" y="1362" textLength="12.2" clip-path="url(#terminal-509574676-line-55)"> -</text><text class="terminal-509574676-r1" x="0" y="1386.4" textLength="915" clip-path="url(#terminal-509574676-line-56)">  --annotated-tag-message ANNOTATED_TAG_MESSAGE, -atm ANNOTATED_TAG_MESSAGE</text><text class="terminal-509574676-r1" x="976" y="1386.4" textLength="12.2" clip-path="url(#terminal-509574676-line-56)"> -</text><text class="terminal-509574676-r1" x="0" y="1410.8" textLength="634.4" clip-path="url(#terminal-509574676-line-57)">                        create annotated tag message</text><text class="terminal-509574676-r1" x="976" y="1410.8" textLength="12.2" clip-path="url(#terminal-509574676-line-57)"> -</text><text class="terminal-509574676-r1" x="0" y="1435.2" textLength="719.8" clip-path="url(#terminal-509574676-line-58)">  --gpg-sign, -s        sign tag instead of lightweight one</text><text class="terminal-509574676-r1" x="976" y="1435.2" textLength="12.2" clip-path="url(#terminal-509574676-line-58)"> -</text><text class="terminal-509574676-r1" x="0" y="1459.6" textLength="280.6" clip-path="url(#terminal-509574676-line-59)">  --changelog-to-stdout</text><text class="terminal-509574676-r1" x="976" y="1459.6" textLength="12.2" clip-path="url(#terminal-509574676-line-59)"> -</text><text class="terminal-509574676-r1" x="0" y="1484" textLength="658.8" clip-path="url(#terminal-509574676-line-60)">                        Output changelog to the stdout</text><text class="terminal-509574676-r1" x="976" y="1484" textLength="12.2" clip-path="url(#terminal-509574676-line-60)"> -</text><text class="terminal-509574676-r1" x="0" y="1508.4" textLength="292.8" clip-path="url(#terminal-509574676-line-61)">  --git-output-to-stderr</text><text class="terminal-509574676-r1" x="976" y="1508.4" textLength="12.2" clip-path="url(#terminal-509574676-line-61)"> -</text><text class="terminal-509574676-r1" x="0" y="1532.8" textLength="646.6" clip-path="url(#terminal-509574676-line-62)">                        Redirect git output to stderr</text><text class="terminal-509574676-r1" x="976" y="1532.8" textLength="12.2" clip-path="url(#terminal-509574676-line-62)"> -</text><text class="terminal-509574676-r1" x="0" y="1557.2" textLength="744.2" clip-path="url(#terminal-509574676-line-63)">  --retry               retry commit if it fails the 1st time</text><text class="terminal-509574676-r1" x="976" y="1557.2" textLength="12.2" clip-path="url(#terminal-509574676-line-63)"> -</text><text class="terminal-509574676-r1" x="0" y="1581.6" textLength="939.4" clip-path="url(#terminal-509574676-line-64)">  --major-version-zero  keep major version at zero, even for breaking changes</text><text class="terminal-509574676-r1" x="976" y="1581.6" textLength="12.2" clip-path="url(#terminal-509574676-line-64)"> -</text><text class="terminal-509574676-r1" x="0" y="1606" textLength="414.8" clip-path="url(#terminal-509574676-line-65)">  --template TEMPLATE, -t TEMPLATE</text><text class="terminal-509574676-r1" x="976" y="1606" textLength="12.2" clip-path="url(#terminal-509574676-line-65)"> -</text><text class="terminal-509574676-r1" x="0" y="1630.4" textLength="646.6" clip-path="url(#terminal-509574676-line-66)">                        changelog template file name </text><text class="terminal-509574676-r2" x="646.6" y="1630.4" textLength="12.2" clip-path="url(#terminal-509574676-line-66)">(</text><text class="terminal-509574676-r1" x="658.8" y="1630.4" textLength="280.6" clip-path="url(#terminal-509574676-line-66)">relative to the current</text><text class="terminal-509574676-r1" x="976" y="1630.4" textLength="12.2" clip-path="url(#terminal-509574676-line-66)"> -</text><text class="terminal-509574676-r1" x="0" y="1654.8" textLength="500.2" clip-path="url(#terminal-509574676-line-67)">                        working directory</text><text class="terminal-509574676-r2" x="500.2" y="1654.8" textLength="12.2" clip-path="url(#terminal-509574676-line-67)">)</text><text class="terminal-509574676-r1" x="976" y="1654.8" textLength="12.2" clip-path="url(#terminal-509574676-line-67)"> -</text><text class="terminal-509574676-r1" x="0" y="1679.2" textLength="305" clip-path="url(#terminal-509574676-line-68)">  --extra EXTRA, -e EXTRA</text><text class="terminal-509574676-r1" x="976" y="1679.2" textLength="12.2" clip-path="url(#terminal-509574676-line-68)"> -</text><text class="terminal-509574676-r1" x="0" y="1703.6" textLength="622.2" clip-path="url(#terminal-509574676-line-69)">                        a changelog extra variable </text><text class="terminal-509574676-r2" x="622.2" y="1703.6" textLength="12.2" clip-path="url(#terminal-509574676-line-69)">(</text><text class="terminal-509574676-r1" x="634.4" y="1703.6" textLength="146.4" clip-path="url(#terminal-509574676-line-69)">in the form </text><text class="terminal-509574676-r4" x="780.8" y="1703.6" textLength="12.2" clip-path="url(#terminal-509574676-line-69)">'</text><text class="terminal-509574676-r4" x="793" y="1703.6" textLength="36.6" clip-path="url(#terminal-509574676-line-69)">key</text><text class="terminal-509574676-r4" x="829.6" y="1703.6" textLength="12.2" clip-path="url(#terminal-509574676-line-69)">=</text><text class="terminal-509574676-r4" x="841.8" y="1703.6" textLength="61" clip-path="url(#terminal-509574676-line-69)">value</text><text class="terminal-509574676-r4" x="902.8" y="1703.6" textLength="12.2" clip-path="url(#terminal-509574676-line-69)">'</text><text class="terminal-509574676-r2" x="915" y="1703.6" textLength="12.2" clip-path="url(#terminal-509574676-line-69)">)</text><text class="terminal-509574676-r1" x="976" y="1703.6" textLength="12.2" clip-path="url(#terminal-509574676-line-69)"> -</text><text class="terminal-509574676-r1" x="0" y="1728" textLength="280.6" clip-path="url(#terminal-509574676-line-70)">  --file-name FILE_NAME</text><text class="terminal-509574676-r1" x="976" y="1728" textLength="12.2" clip-path="url(#terminal-509574676-line-70)"> -</text><text class="terminal-509574676-r1" x="0" y="1752.4" textLength="573.4" clip-path="url(#terminal-509574676-line-71)">                        file name of changelog </text><text class="terminal-509574676-r2" x="573.4" y="1752.4" textLength="12.2" clip-path="url(#terminal-509574676-line-71)">(</text><text class="terminal-509574676-r1" x="585.6" y="1752.4" textLength="109.8" clip-path="url(#terminal-509574676-line-71)">default: </text><text class="terminal-509574676-r4" x="695.4" y="1752.4" textLength="170.8" clip-path="url(#terminal-509574676-line-71)">'CHANGELOG.md'</text><text class="terminal-509574676-r2" x="866.2" y="1752.4" textLength="12.2" clip-path="url(#terminal-509574676-line-71)">)</text><text class="terminal-509574676-r1" x="976" y="1752.4" textLength="12.2" clip-path="url(#terminal-509574676-line-71)"> -</text><text class="terminal-509574676-r1" x="0" y="1776.8" textLength="475.8" clip-path="url(#terminal-509574676-line-72)">  --prerelease-offset PRERELEASE_OFFSET</text><text class="terminal-509574676-r1" x="976" y="1776.8" textLength="12.2" clip-path="url(#terminal-509574676-line-72)"> -</text><text class="terminal-509574676-r1" x="0" y="1801.2" textLength="719.8" clip-path="url(#terminal-509574676-line-73)">                        start pre-releases with this offset</text><text class="terminal-509574676-r1" x="976" y="1801.2" textLength="12.2" clip-path="url(#terminal-509574676-line-73)"> -</text><text class="terminal-509574676-r1" x="0" y="1825.6" textLength="231.8" clip-path="url(#terminal-509574676-line-74)">  --version-scheme </text><text class="terminal-509574676-r2" x="231.8" y="1825.6" textLength="12.2" clip-path="url(#terminal-509574676-line-74)">{</text><text class="terminal-509574676-r1" x="244" y="1825.6" textLength="256.2" clip-path="url(#terminal-509574676-line-74)">pep440,semver,semver2</text><text class="terminal-509574676-r2" x="500.2" y="1825.6" textLength="12.2" clip-path="url(#terminal-509574676-line-74)">}</text><text class="terminal-509574676-r1" x="976" y="1825.6" textLength="12.2" clip-path="url(#terminal-509574676-line-74)"> -</text><text class="terminal-509574676-r1" x="0" y="1850" textLength="549" clip-path="url(#terminal-509574676-line-75)">                        choose version scheme</text><text class="terminal-509574676-r1" x="976" y="1850" textLength="12.2" clip-path="url(#terminal-509574676-line-75)"> -</text><text class="terminal-509574676-r1" x="0" y="1874.4" textLength="207.4" clip-path="url(#terminal-509574676-line-76)">  --version-type </text><text class="terminal-509574676-r2" x="207.4" y="1874.4" textLength="12.2" clip-path="url(#terminal-509574676-line-76)">{</text><text class="terminal-509574676-r1" x="219.6" y="1874.4" textLength="256.2" clip-path="url(#terminal-509574676-line-76)">pep440,semver,semver2</text><text class="terminal-509574676-r2" x="475.8" y="1874.4" textLength="12.2" clip-path="url(#terminal-509574676-line-76)">}</text><text class="terminal-509574676-r1" x="976" y="1874.4" textLength="12.2" clip-path="url(#terminal-509574676-line-76)"> -</text><text class="terminal-509574676-r1" x="0" y="1898.8" textLength="683.2" clip-path="url(#terminal-509574676-line-77)">                        Deprecated, use --version-scheme</text><text class="terminal-509574676-r1" x="976" y="1898.8" textLength="12.2" clip-path="url(#terminal-509574676-line-77)"> -</text><text class="terminal-509574676-r1" x="0" y="1923.2" textLength="402.6" clip-path="url(#terminal-509574676-line-78)">  --build-metadata BUILD_METADATA</text><text class="terminal-509574676-r1" x="976" y="1923.2" textLength="12.2" clip-path="url(#terminal-509574676-line-78)"> -</text><text class="terminal-509574676-r1" x="0" y="1947.6" textLength="915" clip-path="url(#terminal-509574676-line-79)">                        Add additional build-metadata to the version-number</text><text class="terminal-509574676-r1" x="976" y="1947.6" textLength="12.2" clip-path="url(#terminal-509574676-line-79)"> -</text><text class="terminal-509574676-r1" x="976" y="1972" textLength="12.2" clip-path="url(#terminal-509574676-line-80)"> + <g class="terminal-4007273841-matrix"> + <text class="terminal-4007273841-r1" x="0" y="20" textLength="195.2" clip-path="url(#terminal-4007273841-line-0)">$ cz bump --help</text><text class="terminal-4007273841-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-4007273841-line-0)"> +</text><text class="terminal-4007273841-r1" x="0" y="44.4" textLength="183" clip-path="url(#terminal-4007273841-line-1)">usage: cz bump </text><text class="terminal-4007273841-r2" x="183" y="44.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-1)">[</text><text class="terminal-4007273841-r1" x="195.2" y="44.4" textLength="24.4" clip-path="url(#terminal-4007273841-line-1)">-h</text><text class="terminal-4007273841-r2" x="219.6" y="44.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-1)">]</text><text class="terminal-4007273841-r2" x="244" y="44.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-1)">[</text><text class="terminal-4007273841-r1" x="256.2" y="44.4" textLength="109.8" clip-path="url(#terminal-4007273841-line-1)">--dry-run</text><text class="terminal-4007273841-r2" x="366" y="44.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-1)">]</text><text class="terminal-4007273841-r2" x="390.4" y="44.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-1)">[</text><text class="terminal-4007273841-r1" x="402.6" y="44.4" textLength="146.4" clip-path="url(#terminal-4007273841-line-1)">--files-only</text><text class="terminal-4007273841-r2" x="549" y="44.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-1)">]</text><text class="terminal-4007273841-r2" x="573.4" y="44.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-1)">[</text><text class="terminal-4007273841-r1" x="585.6" y="44.4" textLength="244" clip-path="url(#terminal-4007273841-line-1)">--version-files-only</text><text class="terminal-4007273841-r2" x="829.6" y="44.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-1)">]</text><text class="terminal-4007273841-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-1)"> +</text><text class="terminal-4007273841-r2" x="183" y="68.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-2)">[</text><text class="terminal-4007273841-r1" x="195.2" y="68.8" textLength="183" clip-path="url(#terminal-4007273841-line-2)">--local-version</text><text class="terminal-4007273841-r2" x="378.2" y="68.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-2)">]</text><text class="terminal-4007273841-r2" x="402.6" y="68.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-2)">[</text><text class="terminal-4007273841-r1" x="414.8" y="68.8" textLength="134.2" clip-path="url(#terminal-4007273841-line-2)">--changelog</text><text class="terminal-4007273841-r2" x="549" y="68.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-2)">]</text><text class="terminal-4007273841-r2" x="573.4" y="68.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-2)">[</text><text class="terminal-4007273841-r1" x="585.6" y="68.8" textLength="134.2" clip-path="url(#terminal-4007273841-line-2)">--no-verify</text><text class="terminal-4007273841-r2" x="719.8" y="68.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-2)">]</text><text class="terminal-4007273841-r2" x="744.2" y="68.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-2)">[</text><text class="terminal-4007273841-r1" x="756.4" y="68.8" textLength="61" clip-path="url(#terminal-4007273841-line-2)">--yes</text><text class="terminal-4007273841-r2" x="817.4" y="68.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-2)">]</text><text class="terminal-4007273841-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-2)"> +</text><text class="terminal-4007273841-r2" x="183" y="93.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-3)">[</text><text class="terminal-4007273841-r1" x="195.2" y="93.2" textLength="280.6" clip-path="url(#terminal-4007273841-line-3)">--tag-format TAG_FORMAT</text><text class="terminal-4007273841-r2" x="475.8" y="93.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-3)">]</text><text class="terminal-4007273841-r2" x="500.2" y="93.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-3)">[</text><text class="terminal-4007273841-r1" x="512.4" y="93.2" textLength="329.4" clip-path="url(#terminal-4007273841-line-3)">--bump-message BUMP_MESSAGE</text><text class="terminal-4007273841-r2" x="841.8" y="93.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-3)">]</text><text class="terminal-4007273841-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-3)"> +</text><text class="terminal-4007273841-r2" x="183" y="117.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-4)">[</text><text class="terminal-4007273841-r1" x="195.2" y="117.6" textLength="158.6" clip-path="url(#terminal-4007273841-line-4)">--prerelease </text><text class="terminal-4007273841-r2" x="353.8" y="117.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-4)">{</text><text class="terminal-4007273841-r1" x="366" y="117.6" textLength="158.6" clip-path="url(#terminal-4007273841-line-4)">alpha,beta,rc</text><text class="terminal-4007273841-r2" x="524.6" y="117.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-4)">}</text><text class="terminal-4007273841-r2" x="536.8" y="117.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-4)">]</text><text class="terminal-4007273841-r2" x="561.2" y="117.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-4)">[</text><text class="terminal-4007273841-r1" x="573.4" y="117.6" textLength="280.6" clip-path="url(#terminal-4007273841-line-4)">--devrelease DEVRELEASE</text><text class="terminal-4007273841-r2" x="854" y="117.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-4)">]</text><text class="terminal-4007273841-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-4)"> +</text><text class="terminal-4007273841-r2" x="183" y="142" textLength="12.2" clip-path="url(#terminal-4007273841-line-5)">[</text><text class="terminal-4007273841-r1" x="195.2" y="142" textLength="146.4" clip-path="url(#terminal-4007273841-line-5)">--increment </text><text class="terminal-4007273841-r2" x="341.6" y="142" textLength="12.2" clip-path="url(#terminal-4007273841-line-5)">{</text><text class="terminal-4007273841-r1" x="353.8" y="142" textLength="207.4" clip-path="url(#terminal-4007273841-line-5)">MAJOR,MINOR,PATCH</text><text class="terminal-4007273841-r2" x="561.2" y="142" textLength="12.2" clip-path="url(#terminal-4007273841-line-5)">}</text><text class="terminal-4007273841-r2" x="573.4" y="142" textLength="12.2" clip-path="url(#terminal-4007273841-line-5)">]</text><text class="terminal-4007273841-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-4007273841-line-5)"> +</text><text class="terminal-4007273841-r2" x="183" y="166.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-6)">[</text><text class="terminal-4007273841-r1" x="195.2" y="166.4" textLength="207.4" clip-path="url(#terminal-4007273841-line-6)">--increment-mode </text><text class="terminal-4007273841-r2" x="402.6" y="166.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-6)">{</text><text class="terminal-4007273841-r1" x="414.8" y="166.4" textLength="146.4" clip-path="url(#terminal-4007273841-line-6)">linear,exact</text><text class="terminal-4007273841-r2" x="561.2" y="166.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-6)">}</text><text class="terminal-4007273841-r2" x="573.4" y="166.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-6)">]</text><text class="terminal-4007273841-r2" x="597.8" y="166.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-6)">[</text><text class="terminal-4007273841-r1" x="610" y="166.4" textLength="231.8" clip-path="url(#terminal-4007273841-line-6)">--check-consistency</text><text class="terminal-4007273841-r2" x="841.8" y="166.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-6)">]</text><text class="terminal-4007273841-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-6)"> +</text><text class="terminal-4007273841-r2" x="183" y="190.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-7)">[</text><text class="terminal-4007273841-r1" x="195.2" y="190.8" textLength="183" clip-path="url(#terminal-4007273841-line-7)">--annotated-tag</text><text class="terminal-4007273841-r2" x="378.2" y="190.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-7)">]</text><text class="terminal-4007273841-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-7)"> +</text><text class="terminal-4007273841-r2" x="183" y="215.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-8)">[</text><text class="terminal-4007273841-r1" x="195.2" y="215.2" textLength="549" clip-path="url(#terminal-4007273841-line-8)">--annotated-tag-message ANNOTATED_TAG_MESSAGE</text><text class="terminal-4007273841-r2" x="744.2" y="215.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-8)">]</text><text class="terminal-4007273841-r2" x="768.6" y="215.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-8)">[</text><text class="terminal-4007273841-r1" x="780.8" y="215.2" textLength="122" clip-path="url(#terminal-4007273841-line-8)">--gpg-sign</text><text class="terminal-4007273841-r2" x="902.8" y="215.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-8)">]</text><text class="terminal-4007273841-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-8)"> +</text><text class="terminal-4007273841-r2" x="183" y="239.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-9)">[</text><text class="terminal-4007273841-r1" x="195.2" y="239.6" textLength="256.2" clip-path="url(#terminal-4007273841-line-9)">--changelog-to-stdout</text><text class="terminal-4007273841-r2" x="451.4" y="239.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-9)">]</text><text class="terminal-4007273841-r2" x="475.8" y="239.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-9)">[</text><text class="terminal-4007273841-r1" x="488" y="239.6" textLength="268.4" clip-path="url(#terminal-4007273841-line-9)">--git-output-to-stderr</text><text class="terminal-4007273841-r2" x="756.4" y="239.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-9)">]</text><text class="terminal-4007273841-r2" x="780.8" y="239.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-9)">[</text><text class="terminal-4007273841-r1" x="793" y="239.6" textLength="85.4" clip-path="url(#terminal-4007273841-line-9)">--retry</text><text class="terminal-4007273841-r2" x="878.4" y="239.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-9)">]</text><text class="terminal-4007273841-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-9)"> +</text><text class="terminal-4007273841-r2" x="183" y="264" textLength="12.2" clip-path="url(#terminal-4007273841-line-10)">[</text><text class="terminal-4007273841-r1" x="195.2" y="264" textLength="244" clip-path="url(#terminal-4007273841-line-10)">--major-version-zero</text><text class="terminal-4007273841-r2" x="439.2" y="264" textLength="12.2" clip-path="url(#terminal-4007273841-line-10)">]</text><text class="terminal-4007273841-r2" x="463.6" y="264" textLength="12.2" clip-path="url(#terminal-4007273841-line-10)">[</text><text class="terminal-4007273841-r1" x="475.8" y="264" textLength="231.8" clip-path="url(#terminal-4007273841-line-10)">--template TEMPLATE</text><text class="terminal-4007273841-r2" x="707.6" y="264" textLength="12.2" clip-path="url(#terminal-4007273841-line-10)">]</text><text class="terminal-4007273841-r2" x="732" y="264" textLength="12.2" clip-path="url(#terminal-4007273841-line-10)">[</text><text class="terminal-4007273841-r1" x="744.2" y="264" textLength="158.6" clip-path="url(#terminal-4007273841-line-10)">--extra EXTRA</text><text class="terminal-4007273841-r2" x="902.8" y="264" textLength="12.2" clip-path="url(#terminal-4007273841-line-10)">]</text><text class="terminal-4007273841-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-4007273841-line-10)"> +</text><text class="terminal-4007273841-r2" x="183" y="288.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-11)">[</text><text class="terminal-4007273841-r1" x="195.2" y="288.4" textLength="256.2" clip-path="url(#terminal-4007273841-line-11)">--file-name FILE_NAME</text><text class="terminal-4007273841-r2" x="451.4" y="288.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-11)">]</text><text class="terminal-4007273841-r2" x="475.8" y="288.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-11)">[</text><text class="terminal-4007273841-r1" x="488" y="288.4" textLength="451.4" clip-path="url(#terminal-4007273841-line-11)">--prerelease-offset PRERELEASE_OFFSET</text><text class="terminal-4007273841-r2" x="939.4" y="288.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-11)">]</text><text class="terminal-4007273841-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-11)"> +</text><text class="terminal-4007273841-r2" x="183" y="312.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-12)">[</text><text class="terminal-4007273841-r1" x="195.2" y="312.8" textLength="207.4" clip-path="url(#terminal-4007273841-line-12)">--version-scheme </text><text class="terminal-4007273841-r2" x="402.6" y="312.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-12)">{</text><text class="terminal-4007273841-r1" x="414.8" y="312.8" textLength="256.2" clip-path="url(#terminal-4007273841-line-12)">pep440,semver,semver2</text><text class="terminal-4007273841-r2" x="671" y="312.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-12)">}</text><text class="terminal-4007273841-r2" x="683.2" y="312.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-12)">]</text><text class="terminal-4007273841-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-12)"> +</text><text class="terminal-4007273841-r2" x="183" y="337.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-13)">[</text><text class="terminal-4007273841-r1" x="195.2" y="337.2" textLength="183" clip-path="url(#terminal-4007273841-line-13)">--version-type </text><text class="terminal-4007273841-r2" x="378.2" y="337.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-13)">{</text><text class="terminal-4007273841-r1" x="390.4" y="337.2" textLength="256.2" clip-path="url(#terminal-4007273841-line-13)">pep440,semver,semver2</text><text class="terminal-4007273841-r2" x="646.6" y="337.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-13)">}</text><text class="terminal-4007273841-r2" x="658.8" y="337.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-13)">]</text><text class="terminal-4007273841-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-13)"> +</text><text class="terminal-4007273841-r2" x="183" y="361.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-14)">[</text><text class="terminal-4007273841-r1" x="195.2" y="361.6" textLength="378.2" clip-path="url(#terminal-4007273841-line-14)">--build-metadata BUILD_METADATA</text><text class="terminal-4007273841-r2" x="573.4" y="361.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-14)">]</text><text class="terminal-4007273841-r2" x="597.8" y="361.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-14)">[</text><text class="terminal-4007273841-r1" x="610" y="361.6" textLength="122" clip-path="url(#terminal-4007273841-line-14)">--get-next</text><text class="terminal-4007273841-r2" x="732" y="361.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-14)">]</text><text class="terminal-4007273841-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-14)"> +</text><text class="terminal-4007273841-r2" x="183" y="386" textLength="12.2" clip-path="url(#terminal-4007273841-line-15)">[</text><text class="terminal-4007273841-r1" x="195.2" y="386" textLength="207.4" clip-path="url(#terminal-4007273841-line-15)">--allow-no-commit</text><text class="terminal-4007273841-r2" x="402.6" y="386" textLength="12.2" clip-path="url(#terminal-4007273841-line-15)">]</text><text class="terminal-4007273841-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-4007273841-line-15)"> +</text><text class="terminal-4007273841-r2" x="183" y="410.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-16)">[</text><text class="terminal-4007273841-r1" x="195.2" y="410.4" textLength="170.8" clip-path="url(#terminal-4007273841-line-16)">MANUAL_VERSION</text><text class="terminal-4007273841-r2" x="366" y="410.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-16)">]</text><text class="terminal-4007273841-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-16)"> +</text><text class="terminal-4007273841-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-17)"> +</text><text class="terminal-4007273841-r1" x="0" y="459.2" textLength="512.4" clip-path="url(#terminal-4007273841-line-18)">Bump semantic version based on the git log</text><text class="terminal-4007273841-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-18)"> +</text><text class="terminal-4007273841-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-19)"> +</text><text class="terminal-4007273841-r1" x="0" y="508" textLength="256.2" clip-path="url(#terminal-4007273841-line-20)">positional arguments:</text><text class="terminal-4007273841-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-4007273841-line-20)"> +</text><text class="terminal-4007273841-r1" x="0" y="532.4" textLength="610" clip-path="url(#terminal-4007273841-line-21)">  MANUAL_VERSION        Bump to the given version </text><text class="terminal-4007273841-r2" x="610" y="532.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-21)">(</text><text class="terminal-4007273841-r1" x="622.2" y="532.4" textLength="73.2" clip-path="url(#terminal-4007273841-line-21)">e.g., </text><text class="terminal-4007273841-r3" x="695.4" y="532.4" textLength="36.6" clip-path="url(#terminal-4007273841-line-21)">1.5</text><text class="terminal-4007273841-r1" x="732" y="532.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-21)">.</text><text class="terminal-4007273841-r3" x="744.2" y="532.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-21)">3</text><text class="terminal-4007273841-r2" x="756.4" y="532.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-21)">)</text><text class="terminal-4007273841-r1" x="768.6" y="532.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-21)">.</text><text class="terminal-4007273841-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-21)"> +</text><text class="terminal-4007273841-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-22)"> +</text><text class="terminal-4007273841-r1" x="0" y="581.2" textLength="97.6" clip-path="url(#terminal-4007273841-line-23)">options:</text><text class="terminal-4007273841-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-23)"> +</text><text class="terminal-4007273841-r1" x="0" y="605.6" textLength="671" clip-path="url(#terminal-4007273841-line-24)">  -h, --help            show this help message and exit</text><text class="terminal-4007273841-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-24)"> +</text><text class="terminal-4007273841-r1" x="0" y="630" textLength="902.8" clip-path="url(#terminal-4007273841-line-25)">  --dry-run             Perform a dry run, without committing or modifying</text><text class="terminal-4007273841-r1" x="976" y="630" textLength="12.2" clip-path="url(#terminal-4007273841-line-25)"> +</text><text class="terminal-4007273841-r1" x="0" y="654.4" textLength="366" clip-path="url(#terminal-4007273841-line-26)">                        files.</text><text class="terminal-4007273841-r1" x="976" y="654.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-26)"> +</text><text class="terminal-4007273841-r1" x="0" y="678.8" textLength="927.2" clip-path="url(#terminal-4007273841-line-27)">  --files-only          Bump version in the `version_files` specified in the</text><text class="terminal-4007273841-r1" x="976" y="678.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-27)"> +</text><text class="terminal-4007273841-r1" x="0" y="703.2" textLength="524.6" clip-path="url(#terminal-4007273841-line-28)">                        configuration file </text><text class="terminal-4007273841-r4" x="524.6" y="703.2" textLength="48.8" clip-path="url(#terminal-4007273841-line-28)">only</text><text class="terminal-4007273841-r2" x="573.4" y="703.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-28)">(</text><text class="terminal-4007273841-r1" x="585.6" y="703.2" textLength="317.2" clip-path="url(#terminal-4007273841-line-28)">deprecated; use --version-</text><text class="terminal-4007273841-r1" x="976" y="703.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-28)"> +</text><text class="terminal-4007273841-r1" x="0" y="727.6" textLength="512.4" clip-path="url(#terminal-4007273841-line-29)">                        files-only instead</text><text class="terminal-4007273841-r2" x="512.4" y="727.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-29)">)</text><text class="terminal-4007273841-r1" x="524.6" y="727.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-29)">.</text><text class="terminal-4007273841-r1" x="976" y="727.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-29)"> +</text><text class="terminal-4007273841-r1" x="0" y="752" textLength="793" clip-path="url(#terminal-4007273841-line-30)">  --version-files-only  Bump version in the files from the config</text><text class="terminal-4007273841-r1" x="976" y="752" textLength="12.2" clip-path="url(#terminal-4007273841-line-30)"> +</text><text class="terminal-4007273841-r1" x="0" y="776.4" textLength="829.6" clip-path="url(#terminal-4007273841-line-31)">  --local-version       Bump version only the local version portion </text><text class="terminal-4007273841-r2" x="829.6" y="776.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-31)">(</text><text class="terminal-4007273841-r1" x="841.8" y="776.4" textLength="97.6" clip-path="url(#terminal-4007273841-line-31)">ignoring</text><text class="terminal-4007273841-r1" x="976" y="776.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-31)"> +</text><text class="terminal-4007273841-r1" x="0" y="800.8" textLength="512.4" clip-path="url(#terminal-4007273841-line-32)">                        the public version</text><text class="terminal-4007273841-r2" x="512.4" y="800.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-32)">)</text><text class="terminal-4007273841-r1" x="524.6" y="800.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-32)">.</text><text class="terminal-4007273841-r1" x="976" y="800.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-32)"> +</text><text class="terminal-4007273841-r1" x="0" y="825.2" textLength="854" clip-path="url(#terminal-4007273841-line-33)">  --changelog, -ch      Generate the changelog for the latest version.</text><text class="terminal-4007273841-r1" x="976" y="825.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-33)"> +</text><text class="terminal-4007273841-r1" x="0" y="849.6" textLength="817.4" clip-path="url(#terminal-4007273841-line-34)">  --no-verify           Bypass the pre-commit and commit-msg hooks.</text><text class="terminal-4007273841-r1" x="976" y="849.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-34)"> +</text><text class="terminal-4007273841-r1" x="0" y="874" textLength="780.8" clip-path="url(#terminal-4007273841-line-35)">  --yes                 Accept automatically answered questions.</text><text class="terminal-4007273841-r1" x="976" y="874" textLength="12.2" clip-path="url(#terminal-4007273841-line-35)"> +</text><text class="terminal-4007273841-r1" x="0" y="898.4" textLength="305" clip-path="url(#terminal-4007273841-line-36)">  --tag-format TAG_FORMAT</text><text class="terminal-4007273841-r1" x="976" y="898.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-36)"> +</text><text class="terminal-4007273841-r1" x="0" y="922.8" textLength="939.4" clip-path="url(#terminal-4007273841-line-37)">                        The format used to tag the commit and read it. Use it</text><text class="terminal-4007273841-r1" x="976" y="922.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-37)"> +</text><text class="terminal-4007273841-r1" x="0" y="947.2" textLength="927.2" clip-path="url(#terminal-4007273841-line-38)">                        in existing projects, and wrap around simple quotes.</text><text class="terminal-4007273841-r1" x="976" y="947.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-38)"> +</text><text class="terminal-4007273841-r1" x="0" y="971.6" textLength="353.8" clip-path="url(#terminal-4007273841-line-39)">  --bump-message BUMP_MESSAGE</text><text class="terminal-4007273841-r1" x="976" y="971.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-39)"> +</text><text class="terminal-4007273841-r1" x="0" y="996" textLength="902.8" clip-path="url(#terminal-4007273841-line-40)">                        Template used to create the release commit, useful</text><text class="terminal-4007273841-r1" x="976" y="996" textLength="12.2" clip-path="url(#terminal-4007273841-line-40)"> +</text><text class="terminal-4007273841-r1" x="0" y="1020.4" textLength="549" clip-path="url(#terminal-4007273841-line-41)">                        when working with CI.</text><text class="terminal-4007273841-r1" x="976" y="1020.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-41)"> +</text><text class="terminal-4007273841-r1" x="0" y="1044.8" textLength="183" clip-path="url(#terminal-4007273841-line-42)">  --prerelease </text><text class="terminal-4007273841-r2" x="183" y="1044.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-42)">{</text><text class="terminal-4007273841-r1" x="195.2" y="1044.8" textLength="158.6" clip-path="url(#terminal-4007273841-line-42)">alpha,beta,rc</text><text class="terminal-4007273841-r2" x="353.8" y="1044.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-42)">}</text><text class="terminal-4007273841-r1" x="366" y="1044.8" textLength="73.2" clip-path="url(#terminal-4007273841-line-42)">, -pr </text><text class="terminal-4007273841-r2" x="439.2" y="1044.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-42)">{</text><text class="terminal-4007273841-r1" x="451.4" y="1044.8" textLength="158.6" clip-path="url(#terminal-4007273841-line-42)">alpha,beta,rc</text><text class="terminal-4007273841-r2" x="610" y="1044.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-42)">}</text><text class="terminal-4007273841-r1" x="976" y="1044.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-42)"> +</text><text class="terminal-4007273841-r1" x="0" y="1069.2" textLength="524.6" clip-path="url(#terminal-4007273841-line-43)">                        Type of prerelease.</text><text class="terminal-4007273841-r1" x="976" y="1069.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-43)"> +</text><text class="terminal-4007273841-r1" x="0" y="1093.6" textLength="488" clip-path="url(#terminal-4007273841-line-44)">  --devrelease DEVRELEASE, -d DEVRELEASE</text><text class="terminal-4007273841-r1" x="976" y="1093.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-44)"> +</text><text class="terminal-4007273841-r1" x="0" y="1118" textLength="841.8" clip-path="url(#terminal-4007273841-line-45)">                        Specify non-negative integer for dev release.</text><text class="terminal-4007273841-r1" x="976" y="1118" textLength="12.2" clip-path="url(#terminal-4007273841-line-45)"> +</text><text class="terminal-4007273841-r1" x="0" y="1142.4" textLength="170.8" clip-path="url(#terminal-4007273841-line-46)">  --increment </text><text class="terminal-4007273841-r2" x="170.8" y="1142.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-46)">{</text><text class="terminal-4007273841-r1" x="183" y="1142.4" textLength="207.4" clip-path="url(#terminal-4007273841-line-46)">MAJOR,MINOR,PATCH</text><text class="terminal-4007273841-r2" x="390.4" y="1142.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-46)">}</text><text class="terminal-4007273841-r1" x="976" y="1142.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-46)"> +</text><text class="terminal-4007273841-r1" x="0" y="1166.8" textLength="658.8" clip-path="url(#terminal-4007273841-line-47)">                        Specify the desired increment.</text><text class="terminal-4007273841-r1" x="976" y="1166.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-47)"> +</text><text class="terminal-4007273841-r1" x="0" y="1191.2" textLength="231.8" clip-path="url(#terminal-4007273841-line-48)">  --increment-mode </text><text class="terminal-4007273841-r2" x="231.8" y="1191.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-48)">{</text><text class="terminal-4007273841-r1" x="244" y="1191.2" textLength="146.4" clip-path="url(#terminal-4007273841-line-48)">linear,exact</text><text class="terminal-4007273841-r2" x="390.4" y="1191.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-48)">}</text><text class="terminal-4007273841-r1" x="976" y="1191.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-48)"> +</text><text class="terminal-4007273841-r1" x="0" y="1215.6" textLength="902.8" clip-path="url(#terminal-4007273841-line-49)">                        Set the method by which the new version is chosen.</text><text class="terminal-4007273841-r1" x="976" y="1215.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-49)"> +</text><text class="terminal-4007273841-r5" x="292.8" y="1240" textLength="97.6" clip-path="url(#terminal-4007273841-line-50)">'linear'</text><text class="terminal-4007273841-r2" x="402.6" y="1240" textLength="12.2" clip-path="url(#terminal-4007273841-line-50)">(</text><text class="terminal-4007273841-r1" x="414.8" y="1240" textLength="85.4" clip-path="url(#terminal-4007273841-line-50)">default</text><text class="terminal-4007273841-r2" x="500.2" y="1240" textLength="12.2" clip-path="url(#terminal-4007273841-line-50)">)</text><text class="terminal-4007273841-r1" x="512.4" y="1240" textLength="427" clip-path="url(#terminal-4007273841-line-50)"> resolves the next version based on</text><text class="terminal-4007273841-r1" x="976" y="1240" textLength="12.2" clip-path="url(#terminal-4007273841-line-50)"> +</text><text class="terminal-4007273841-r1" x="0" y="1264.4" textLength="951.6" clip-path="url(#terminal-4007273841-line-51)">                        typical linear version progression, where bumping of a</text><text class="terminal-4007273841-r1" x="976" y="1264.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-51)"> +</text><text class="terminal-4007273841-r1" x="0" y="1288.8" textLength="902.8" clip-path="url(#terminal-4007273841-line-52)">                        pre-release with lower precedence than the current</text><text class="terminal-4007273841-r1" x="976" y="1288.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-52)"> +</text><text class="terminal-4007273841-r1" x="0" y="1313.2" textLength="878.4" clip-path="url(#terminal-4007273841-line-53)">                        pre-release phase maintains the current phase of</text><text class="terminal-4007273841-r1" x="976" y="1313.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-53)"> +</text><text class="terminal-4007273841-r1" x="0" y="1337.6" textLength="524.6" clip-path="url(#terminal-4007273841-line-54)">                        higher precedence. </text><text class="terminal-4007273841-r5" x="524.6" y="1337.6" textLength="85.4" clip-path="url(#terminal-4007273841-line-54)">'exact'</text><text class="terminal-4007273841-r1" x="610" y="1337.6" textLength="305" clip-path="url(#terminal-4007273841-line-54)"> applies the changes that</text><text class="terminal-4007273841-r1" x="976" y="1337.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-54)"> +</text><text class="terminal-4007273841-r1" x="0" y="1362" textLength="536.8" clip-path="url(#terminal-4007273841-line-55)">                        have been specified </text><text class="terminal-4007273841-r2" x="536.8" y="1362" textLength="12.2" clip-path="url(#terminal-4007273841-line-55)">(</text><text class="terminal-4007273841-r1" x="549" y="1362" textLength="353.8" clip-path="url(#terminal-4007273841-line-55)">or determined from the commit</text><text class="terminal-4007273841-r1" x="976" y="1362" textLength="12.2" clip-path="url(#terminal-4007273841-line-55)"> +</text><text class="terminal-4007273841-r1" x="0" y="1386.4" textLength="329.4" clip-path="url(#terminal-4007273841-line-56)">                        log</text><text class="terminal-4007273841-r2" x="329.4" y="1386.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-56)">)</text><text class="terminal-4007273841-r1" x="341.6" y="1386.4" textLength="573.4" clip-path="url(#terminal-4007273841-line-56)"> without interpretation, ensuring the increment</text><text class="terminal-4007273841-r1" x="976" y="1386.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-56)"> +</text><text class="terminal-4007273841-r1" x="0" y="1410.8" textLength="719.8" clip-path="url(#terminal-4007273841-line-57)">                        and pre-release are always honored.</text><text class="terminal-4007273841-r1" x="976" y="1410.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-57)"> +</text><text class="terminal-4007273841-r1" x="0" y="1435.2" textLength="317.2" clip-path="url(#terminal-4007273841-line-58)">  --check-consistency, -cc</text><text class="terminal-4007273841-r1" x="976" y="1435.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-58)"> +</text><text class="terminal-4007273841-r1" x="0" y="1459.6" textLength="951.6" clip-path="url(#terminal-4007273841-line-59)">                        Check consistency among versions defined in Commitizen</text><text class="terminal-4007273841-r1" x="976" y="1459.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-59)"> +</text><text class="terminal-4007273841-r1" x="0" y="1484" textLength="768.6" clip-path="url(#terminal-4007273841-line-60)">                        configuration file and `version_files`.</text><text class="terminal-4007273841-r1" x="976" y="1484" textLength="12.2" clip-path="url(#terminal-4007273841-line-60)"> +</text><text class="terminal-4007273841-r1" x="0" y="1508.4" textLength="878.4" clip-path="url(#terminal-4007273841-line-61)">  --annotated-tag, -at  Create annotated tag instead of lightweight one.</text><text class="terminal-4007273841-r1" x="976" y="1508.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-61)"> +</text><text class="terminal-4007273841-r1" x="0" y="1532.8" textLength="915" clip-path="url(#terminal-4007273841-line-62)">  --annotated-tag-message ANNOTATED_TAG_MESSAGE, -atm ANNOTATED_TAG_MESSAGE</text><text class="terminal-4007273841-r1" x="976" y="1532.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-62)"> +</text><text class="terminal-4007273841-r1" x="0" y="1557.2" textLength="646.6" clip-path="url(#terminal-4007273841-line-63)">                        Create annotated tag message.</text><text class="terminal-4007273841-r1" x="976" y="1557.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-63)"> +</text><text class="terminal-4007273841-r1" x="0" y="1581.6" textLength="732" clip-path="url(#terminal-4007273841-line-64)">  --gpg-sign, -s        Sign tag instead of lightweight one.</text><text class="terminal-4007273841-r1" x="976" y="1581.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-64)"> +</text><text class="terminal-4007273841-r1" x="0" y="1606" textLength="280.6" clip-path="url(#terminal-4007273841-line-65)">  --changelog-to-stdout</text><text class="terminal-4007273841-r1" x="976" y="1606" textLength="12.2" clip-path="url(#terminal-4007273841-line-65)"> +</text><text class="terminal-4007273841-r1" x="0" y="1630.4" textLength="622.2" clip-path="url(#terminal-4007273841-line-66)">                        Output changelog to stdout.</text><text class="terminal-4007273841-r1" x="976" y="1630.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-66)"> +</text><text class="terminal-4007273841-r1" x="0" y="1654.8" textLength="292.8" clip-path="url(#terminal-4007273841-line-67)">  --git-output-to-stderr</text><text class="terminal-4007273841-r1" x="976" y="1654.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-67)"> +</text><text class="terminal-4007273841-r1" x="0" y="1679.2" textLength="658.8" clip-path="url(#terminal-4007273841-line-68)">                        Redirect git output to stderr.</text><text class="terminal-4007273841-r1" x="976" y="1679.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-68)"> +</text><text class="terminal-4007273841-r1" x="0" y="1703.6" textLength="829.6" clip-path="url(#terminal-4007273841-line-69)">  --retry               Retry commit if it fails for the first time.</text><text class="terminal-4007273841-r1" x="976" y="1703.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-69)"> +</text><text class="terminal-4007273841-r1" x="0" y="1728" textLength="951.6" clip-path="url(#terminal-4007273841-line-70)">  --major-version-zero  Keep major version at zero, even for breaking changes.</text><text class="terminal-4007273841-r1" x="976" y="1728" textLength="12.2" clip-path="url(#terminal-4007273841-line-70)"> +</text><text class="terminal-4007273841-r1" x="0" y="1752.4" textLength="414.8" clip-path="url(#terminal-4007273841-line-71)">  --template TEMPLATE, -t TEMPLATE</text><text class="terminal-4007273841-r1" x="976" y="1752.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-71)"> +</text><text class="terminal-4007273841-r1" x="0" y="1776.8" textLength="646.6" clip-path="url(#terminal-4007273841-line-72)">                        Changelog template file name </text><text class="terminal-4007273841-r2" x="646.6" y="1776.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-72)">(</text><text class="terminal-4007273841-r1" x="658.8" y="1776.8" textLength="280.6" clip-path="url(#terminal-4007273841-line-72)">relative to the current</text><text class="terminal-4007273841-r1" x="976" y="1776.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-72)"> +</text><text class="terminal-4007273841-r1" x="0" y="1801.2" textLength="500.2" clip-path="url(#terminal-4007273841-line-73)">                        working directory</text><text class="terminal-4007273841-r2" x="500.2" y="1801.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-73)">)</text><text class="terminal-4007273841-r1" x="512.4" y="1801.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-73)">.</text><text class="terminal-4007273841-r1" x="976" y="1801.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-73)"> +</text><text class="terminal-4007273841-r1" x="0" y="1825.6" textLength="305" clip-path="url(#terminal-4007273841-line-74)">  --extra EXTRA, -e EXTRA</text><text class="terminal-4007273841-r1" x="976" y="1825.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-74)"> +</text><text class="terminal-4007273841-r1" x="0" y="1850" textLength="610" clip-path="url(#terminal-4007273841-line-75)">                        Changelog extra variables </text><text class="terminal-4007273841-r2" x="610" y="1850" textLength="12.2" clip-path="url(#terminal-4007273841-line-75)">(</text><text class="terminal-4007273841-r1" x="622.2" y="1850" textLength="146.4" clip-path="url(#terminal-4007273841-line-75)">in the form </text><text class="terminal-4007273841-r5" x="768.6" y="1850" textLength="12.2" clip-path="url(#terminal-4007273841-line-75)">'</text><text class="terminal-4007273841-r5" x="780.8" y="1850" textLength="36.6" clip-path="url(#terminal-4007273841-line-75)">key</text><text class="terminal-4007273841-r5" x="817.4" y="1850" textLength="12.2" clip-path="url(#terminal-4007273841-line-75)">=</text><text class="terminal-4007273841-r5" x="829.6" y="1850" textLength="61" clip-path="url(#terminal-4007273841-line-75)">value</text><text class="terminal-4007273841-r5" x="890.6" y="1850" textLength="12.2" clip-path="url(#terminal-4007273841-line-75)">'</text><text class="terminal-4007273841-r2" x="902.8" y="1850" textLength="12.2" clip-path="url(#terminal-4007273841-line-75)">)</text><text class="terminal-4007273841-r1" x="915" y="1850" textLength="12.2" clip-path="url(#terminal-4007273841-line-75)">.</text><text class="terminal-4007273841-r1" x="976" y="1850" textLength="12.2" clip-path="url(#terminal-4007273841-line-75)"> +</text><text class="terminal-4007273841-r1" x="0" y="1874.4" textLength="280.6" clip-path="url(#terminal-4007273841-line-76)">  --file-name FILE_NAME</text><text class="terminal-4007273841-r1" x="976" y="1874.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-76)"> +</text><text class="terminal-4007273841-r1" x="0" y="1898.8" textLength="573.4" clip-path="url(#terminal-4007273841-line-77)">                        File name of changelog </text><text class="terminal-4007273841-r2" x="573.4" y="1898.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-77)">(</text><text class="terminal-4007273841-r1" x="585.6" y="1898.8" textLength="109.8" clip-path="url(#terminal-4007273841-line-77)">default: </text><text class="terminal-4007273841-r5" x="695.4" y="1898.8" textLength="170.8" clip-path="url(#terminal-4007273841-line-77)">'CHANGELOG.md'</text><text class="terminal-4007273841-r2" x="866.2" y="1898.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-77)">)</text><text class="terminal-4007273841-r1" x="878.4" y="1898.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-77)">.</text><text class="terminal-4007273841-r1" x="976" y="1898.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-77)"> +</text><text class="terminal-4007273841-r1" x="0" y="1923.2" textLength="475.8" clip-path="url(#terminal-4007273841-line-78)">  --prerelease-offset PRERELEASE_OFFSET</text><text class="terminal-4007273841-r1" x="976" y="1923.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-78)"> +</text><text class="terminal-4007273841-r1" x="0" y="1947.6" textLength="732" clip-path="url(#terminal-4007273841-line-79)">                        Start pre-releases with this offset.</text><text class="terminal-4007273841-r1" x="976" y="1947.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-79)"> +</text><text class="terminal-4007273841-r1" x="0" y="1972" textLength="231.8" clip-path="url(#terminal-4007273841-line-80)">  --version-scheme </text><text class="terminal-4007273841-r2" x="231.8" y="1972" textLength="12.2" clip-path="url(#terminal-4007273841-line-80)">{</text><text class="terminal-4007273841-r1" x="244" y="1972" textLength="256.2" clip-path="url(#terminal-4007273841-line-80)">pep440,semver,semver2</text><text class="terminal-4007273841-r2" x="500.2" y="1972" textLength="12.2" clip-path="url(#terminal-4007273841-line-80)">}</text><text class="terminal-4007273841-r1" x="976" y="1972" textLength="12.2" clip-path="url(#terminal-4007273841-line-80)"> +</text><text class="terminal-4007273841-r1" x="0" y="1996.4" textLength="561.2" clip-path="url(#terminal-4007273841-line-81)">                        Choose version scheme.</text><text class="terminal-4007273841-r1" x="976" y="1996.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-81)"> +</text><text class="terminal-4007273841-r1" x="0" y="2020.8" textLength="207.4" clip-path="url(#terminal-4007273841-line-82)">  --version-type </text><text class="terminal-4007273841-r2" x="207.4" y="2020.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-82)">{</text><text class="terminal-4007273841-r1" x="219.6" y="2020.8" textLength="256.2" clip-path="url(#terminal-4007273841-line-82)">pep440,semver,semver2</text><text class="terminal-4007273841-r2" x="475.8" y="2020.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-82)">}</text><text class="terminal-4007273841-r1" x="976" y="2020.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-82)"> +</text><text class="terminal-4007273841-r1" x="0" y="2045.2" textLength="817.4" clip-path="url(#terminal-4007273841-line-83)">                        Deprecated, use `--version-scheme` instead.</text><text class="terminal-4007273841-r1" x="976" y="2045.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-83)"> +</text><text class="terminal-4007273841-r1" x="0" y="2069.6" textLength="402.6" clip-path="url(#terminal-4007273841-line-84)">  --build-metadata BUILD_METADATA</text><text class="terminal-4007273841-r1" x="976" y="2069.6" textLength="12.2" clip-path="url(#terminal-4007273841-line-84)"> +</text><text class="terminal-4007273841-r1" x="0" y="2094" textLength="927.2" clip-path="url(#terminal-4007273841-line-85)">                        Add additional build-metadata to the version-number.</text><text class="terminal-4007273841-r1" x="976" y="2094" textLength="12.2" clip-path="url(#terminal-4007273841-line-85)"> +</text><text class="terminal-4007273841-r1" x="0" y="2118.4" textLength="866.2" clip-path="url(#terminal-4007273841-line-86)">  --get-next            Determine the next version and write to stdout.</text><text class="terminal-4007273841-r1" x="976" y="2118.4" textLength="12.2" clip-path="url(#terminal-4007273841-line-86)"> +</text><text class="terminal-4007273841-r1" x="0" y="2142.8" textLength="756.4" clip-path="url(#terminal-4007273841-line-87)">  --allow-no-commit     Bump version without eligible commits.</text><text class="terminal-4007273841-r1" x="976" y="2142.8" textLength="12.2" clip-path="url(#terminal-4007273841-line-87)"> +</text><text class="terminal-4007273841-r1" x="976" y="2167.2" textLength="12.2" clip-path="url(#terminal-4007273841-line-88)"> </text> </g> </g> diff --git a/docs/images/cli_help/cz_changelog___help.svg b/docs/images/cli_help/cz_changelog___help.svg index 8cb3fcf2fe..00031d48f6 100644 --- a/docs/images/cli_help/cz_changelog___help.svg +++ b/docs/images/cli_help/cz_changelog___help.svg @@ -1,4 +1,4 @@ -<svg class="rich-terminal" viewBox="0 0 994 1074.8" xmlns="http://www.w3.org/2000/svg"> +<svg class="rich-terminal" viewBox="0 0 994 1123.6" xmlns="http://www.w3.org/2000/svg"> <!-- Generated with Rich https://www.textualize.io --> <style> @@ -19,206 +19,214 @@ font-weight: 700; } - .terminal-2035912735-matrix { + .terminal-771491591-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2035912735-title { + .terminal-771491591-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2035912735-r1 { fill: #c5c8c6 } -.terminal-2035912735-r2 { fill: #c5c8c6;font-weight: bold } -.terminal-2035912735-r3 { fill: #68a0b3;font-weight: bold } -.terminal-2035912735-r4 { fill: #98a84b } + .terminal-771491591-r1 { fill: #c5c8c6 } +.terminal-771491591-r2 { fill: #c5c8c6;font-weight: bold } +.terminal-771491591-r3 { fill: #68a0b3;font-weight: bold } +.terminal-771491591-r4 { fill: #98a84b } </style> <defs> - <clipPath id="terminal-2035912735-clip-terminal"> - <rect x="0" y="0" width="975.0" height="1023.8" /> + <clipPath id="terminal-771491591-clip-terminal"> + <rect x="0" y="0" width="975.0" height="1072.6" /> </clipPath> - <clipPath id="terminal-2035912735-line-0"> + <clipPath id="terminal-771491591-line-0"> <rect x="0" y="1.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-1"> +<clipPath id="terminal-771491591-line-1"> <rect x="0" y="25.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-2"> +<clipPath id="terminal-771491591-line-2"> <rect x="0" y="50.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-3"> +<clipPath id="terminal-771491591-line-3"> <rect x="0" y="74.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-4"> +<clipPath id="terminal-771491591-line-4"> <rect x="0" y="99.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-5"> +<clipPath id="terminal-771491591-line-5"> <rect x="0" y="123.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-6"> +<clipPath id="terminal-771491591-line-6"> <rect x="0" y="147.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-7"> +<clipPath id="terminal-771491591-line-7"> <rect x="0" y="172.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-8"> +<clipPath id="terminal-771491591-line-8"> <rect x="0" y="196.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-9"> +<clipPath id="terminal-771491591-line-9"> <rect x="0" y="221.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-10"> +<clipPath id="terminal-771491591-line-10"> <rect x="0" y="245.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-11"> +<clipPath id="terminal-771491591-line-11"> <rect x="0" y="269.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-12"> +<clipPath id="terminal-771491591-line-12"> <rect x="0" y="294.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-13"> +<clipPath id="terminal-771491591-line-13"> <rect x="0" y="318.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-14"> +<clipPath id="terminal-771491591-line-14"> <rect x="0" y="343.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-15"> +<clipPath id="terminal-771491591-line-15"> <rect x="0" y="367.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-16"> +<clipPath id="terminal-771491591-line-16"> <rect x="0" y="391.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-17"> +<clipPath id="terminal-771491591-line-17"> <rect x="0" y="416.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-18"> +<clipPath id="terminal-771491591-line-18"> <rect x="0" y="440.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-19"> +<clipPath id="terminal-771491591-line-19"> <rect x="0" y="465.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-20"> +<clipPath id="terminal-771491591-line-20"> <rect x="0" y="489.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-21"> +<clipPath id="terminal-771491591-line-21"> <rect x="0" y="513.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-22"> +<clipPath id="terminal-771491591-line-22"> <rect x="0" y="538.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-23"> +<clipPath id="terminal-771491591-line-23"> <rect x="0" y="562.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-24"> +<clipPath id="terminal-771491591-line-24"> <rect x="0" y="587.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-25"> +<clipPath id="terminal-771491591-line-25"> <rect x="0" y="611.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-26"> +<clipPath id="terminal-771491591-line-26"> <rect x="0" y="635.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-27"> +<clipPath id="terminal-771491591-line-27"> <rect x="0" y="660.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-28"> +<clipPath id="terminal-771491591-line-28"> <rect x="0" y="684.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-29"> +<clipPath id="terminal-771491591-line-29"> <rect x="0" y="709.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-30"> +<clipPath id="terminal-771491591-line-30"> <rect x="0" y="733.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-31"> +<clipPath id="terminal-771491591-line-31"> <rect x="0" y="757.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-32"> +<clipPath id="terminal-771491591-line-32"> <rect x="0" y="782.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-33"> +<clipPath id="terminal-771491591-line-33"> <rect x="0" y="806.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-34"> +<clipPath id="terminal-771491591-line-34"> <rect x="0" y="831.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-35"> +<clipPath id="terminal-771491591-line-35"> <rect x="0" y="855.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-36"> +<clipPath id="terminal-771491591-line-36"> <rect x="0" y="879.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-37"> +<clipPath id="terminal-771491591-line-37"> <rect x="0" y="904.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-38"> +<clipPath id="terminal-771491591-line-38"> <rect x="0" y="928.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-39"> +<clipPath id="terminal-771491591-line-39"> <rect x="0" y="953.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-2035912735-line-40"> +<clipPath id="terminal-771491591-line-40"> <rect x="0" y="977.5" width="976" height="24.65"/> </clipPath> +<clipPath id="terminal-771491591-line-41"> + <rect x="0" y="1001.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-771491591-line-42"> + <rect x="0" y="1026.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="1072.8" rx="8"/> + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="1121.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-2035912735-clip-terminal)"> + <g transform="translate(9, 41)" clip-path="url(#terminal-771491591-clip-terminal)"> - <g class="terminal-2035912735-matrix"> - <text class="terminal-2035912735-r1" x="0" y="20" textLength="256.2" clip-path="url(#terminal-2035912735-line-0)">$ cz changelog --help</text><text class="terminal-2035912735-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-2035912735-line-0)"> -</text><text class="terminal-2035912735-r1" x="0" y="44.4" textLength="244" clip-path="url(#terminal-2035912735-line-1)">usage: cz changelog </text><text class="terminal-2035912735-r2" x="244" y="44.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-1)">[</text><text class="terminal-2035912735-r1" x="256.2" y="44.4" textLength="24.4" clip-path="url(#terminal-2035912735-line-1)">-h</text><text class="terminal-2035912735-r2" x="280.6" y="44.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-1)">]</text><text class="terminal-2035912735-r2" x="305" y="44.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-1)">[</text><text class="terminal-2035912735-r1" x="317.2" y="44.4" textLength="109.8" clip-path="url(#terminal-2035912735-line-1)">--dry-run</text><text class="terminal-2035912735-r2" x="427" y="44.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-1)">]</text><text class="terminal-2035912735-r2" x="451.4" y="44.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-1)">[</text><text class="terminal-2035912735-r1" x="463.6" y="44.4" textLength="256.2" clip-path="url(#terminal-2035912735-line-1)">--file-name FILE_NAME</text><text class="terminal-2035912735-r2" x="719.8" y="44.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-1)">]</text><text class="terminal-2035912735-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-1)"> -</text><text class="terminal-2035912735-r2" x="244" y="68.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-2)">[</text><text class="terminal-2035912735-r1" x="256.2" y="68.8" textLength="475.8" clip-path="url(#terminal-2035912735-line-2)">--unreleased-version UNRELEASED_VERSION</text><text class="terminal-2035912735-r2" x="732" y="68.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-2)">]</text><text class="terminal-2035912735-r2" x="756.4" y="68.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-2)">[</text><text class="terminal-2035912735-r1" x="768.6" y="68.8" textLength="158.6" clip-path="url(#terminal-2035912735-line-2)">--incremental</text><text class="terminal-2035912735-r2" x="927.2" y="68.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-2)">]</text><text class="terminal-2035912735-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-2)"> -</text><text class="terminal-2035912735-r2" x="244" y="93.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-3)">[</text><text class="terminal-2035912735-r1" x="256.2" y="93.2" textLength="256.2" clip-path="url(#terminal-2035912735-line-3)">--start-rev START_REV</text><text class="terminal-2035912735-r2" x="512.4" y="93.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-3)">]</text><text class="terminal-2035912735-r2" x="536.8" y="93.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-3)">[</text><text class="terminal-2035912735-r1" x="549" y="93.2" textLength="219.6" clip-path="url(#terminal-2035912735-line-3)">--merge-prerelease</text><text class="terminal-2035912735-r2" x="768.6" y="93.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-3)">]</text><text class="terminal-2035912735-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-3)"> -</text><text class="terminal-2035912735-r2" x="244" y="117.6" textLength="12.2" clip-path="url(#terminal-2035912735-line-4)">[</text><text class="terminal-2035912735-r1" x="256.2" y="117.6" textLength="207.4" clip-path="url(#terminal-2035912735-line-4)">--version-scheme </text><text class="terminal-2035912735-r2" x="463.6" y="117.6" textLength="12.2" clip-path="url(#terminal-2035912735-line-4)">{</text><text class="terminal-2035912735-r1" x="475.8" y="117.6" textLength="256.2" clip-path="url(#terminal-2035912735-line-4)">pep440,semver,semver2</text><text class="terminal-2035912735-r2" x="732" y="117.6" textLength="12.2" clip-path="url(#terminal-2035912735-line-4)">}</text><text class="terminal-2035912735-r2" x="744.2" y="117.6" textLength="12.2" clip-path="url(#terminal-2035912735-line-4)">]</text><text class="terminal-2035912735-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-2035912735-line-4)"> -</text><text class="terminal-2035912735-r2" x="244" y="142" textLength="12.2" clip-path="url(#terminal-2035912735-line-5)">[</text><text class="terminal-2035912735-r1" x="256.2" y="142" textLength="402.6" clip-path="url(#terminal-2035912735-line-5)">--export-template EXPORT_TEMPLATE</text><text class="terminal-2035912735-r2" x="658.8" y="142" textLength="12.2" clip-path="url(#terminal-2035912735-line-5)">]</text><text class="terminal-2035912735-r2" x="683.2" y="142" textLength="12.2" clip-path="url(#terminal-2035912735-line-5)">[</text><text class="terminal-2035912735-r1" x="695.4" y="142" textLength="231.8" clip-path="url(#terminal-2035912735-line-5)">--template TEMPLATE</text><text class="terminal-2035912735-r2" x="927.2" y="142" textLength="12.2" clip-path="url(#terminal-2035912735-line-5)">]</text><text class="terminal-2035912735-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-2035912735-line-5)"> -</text><text class="terminal-2035912735-r2" x="244" y="166.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-6)">[</text><text class="terminal-2035912735-r1" x="256.2" y="166.4" textLength="158.6" clip-path="url(#terminal-2035912735-line-6)">--extra EXTRA</text><text class="terminal-2035912735-r2" x="414.8" y="166.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-6)">]</text><text class="terminal-2035912735-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-6)"> -</text><text class="terminal-2035912735-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-7)"> -</text><text class="terminal-2035912735-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-8)"> -</text><text class="terminal-2035912735-r1" x="0" y="239.6" textLength="231.8" clip-path="url(#terminal-2035912735-line-9)">generate changelog </text><text class="terminal-2035912735-r2" x="231.8" y="239.6" textLength="12.2" clip-path="url(#terminal-2035912735-line-9)">(</text><text class="terminal-2035912735-r1" x="244" y="239.6" textLength="500.2" clip-path="url(#terminal-2035912735-line-9)">note that it will overwrite existing file</text><text class="terminal-2035912735-r2" x="744.2" y="239.6" textLength="12.2" clip-path="url(#terminal-2035912735-line-9)">)</text><text class="terminal-2035912735-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-2035912735-line-9)"> -</text><text class="terminal-2035912735-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-2035912735-line-10)"> -</text><text class="terminal-2035912735-r1" x="0" y="288.4" textLength="256.2" clip-path="url(#terminal-2035912735-line-11)">positional arguments:</text><text class="terminal-2035912735-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-11)"> -</text><text class="terminal-2035912735-r1" x="0" y="312.8" textLength="805.2" clip-path="url(#terminal-2035912735-line-12)">  rev_range             generates changelog for the given version </text><text class="terminal-2035912735-r2" x="805.2" y="312.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-12)">(</text><text class="terminal-2035912735-r1" x="817.4" y="312.8" textLength="61" clip-path="url(#terminal-2035912735-line-12)">e.g: </text><text class="terminal-2035912735-r3" x="878.4" y="312.8" textLength="36.6" clip-path="url(#terminal-2035912735-line-12)">1.5</text><text class="terminal-2035912735-r1" x="915" y="312.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-12)">.</text><text class="terminal-2035912735-r3" x="927.2" y="312.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-12)">3</text><text class="terminal-2035912735-r2" x="939.4" y="312.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-12)">)</text><text class="terminal-2035912735-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-12)"> -</text><text class="terminal-2035912735-r1" x="0" y="337.2" textLength="500.2" clip-path="url(#terminal-2035912735-line-13)">                        or version range </text><text class="terminal-2035912735-r2" x="500.2" y="337.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-13)">(</text><text class="terminal-2035912735-r1" x="512.4" y="337.2" textLength="61" clip-path="url(#terminal-2035912735-line-13)">e.g: </text><text class="terminal-2035912735-r3" x="573.4" y="337.2" textLength="36.6" clip-path="url(#terminal-2035912735-line-13)">1.5</text><text class="terminal-2035912735-r1" x="610" y="337.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-13)">.</text><text class="terminal-2035912735-r3" x="622.2" y="337.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-13)">3</text><text class="terminal-2035912735-r1" x="634.4" y="337.2" textLength="24.4" clip-path="url(#terminal-2035912735-line-13)">..</text><text class="terminal-2035912735-r3" x="658.8" y="337.2" textLength="36.6" clip-path="url(#terminal-2035912735-line-13)">1.7</text><text class="terminal-2035912735-r1" x="695.4" y="337.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-13)">.</text><text class="terminal-2035912735-r3" x="707.6" y="337.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-13)">9</text><text class="terminal-2035912735-r2" x="719.8" y="337.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-13)">)</text><text class="terminal-2035912735-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-13)"> -</text><text class="terminal-2035912735-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-2035912735-line-14)"> -</text><text class="terminal-2035912735-r1" x="0" y="386" textLength="97.6" clip-path="url(#terminal-2035912735-line-15)">options:</text><text class="terminal-2035912735-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-2035912735-line-15)"> -</text><text class="terminal-2035912735-r1" x="0" y="410.4" textLength="671" clip-path="url(#terminal-2035912735-line-16)">  -h, --help            show this help message and exit</text><text class="terminal-2035912735-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-16)"> -</text><text class="terminal-2035912735-r1" x="0" y="434.8" textLength="585.6" clip-path="url(#terminal-2035912735-line-17)">  --dry-run             show changelog to stdout</text><text class="terminal-2035912735-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-17)"> -</text><text class="terminal-2035912735-r1" x="0" y="459.2" textLength="280.6" clip-path="url(#terminal-2035912735-line-18)">  --file-name FILE_NAME</text><text class="terminal-2035912735-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-18)"> -</text><text class="terminal-2035912735-r1" x="0" y="483.6" textLength="573.4" clip-path="url(#terminal-2035912735-line-19)">                        file name of changelog </text><text class="terminal-2035912735-r2" x="573.4" y="483.6" textLength="12.2" clip-path="url(#terminal-2035912735-line-19)">(</text><text class="terminal-2035912735-r1" x="585.6" y="483.6" textLength="109.8" clip-path="url(#terminal-2035912735-line-19)">default: </text><text class="terminal-2035912735-r4" x="695.4" y="483.6" textLength="170.8" clip-path="url(#terminal-2035912735-line-19)">'CHANGELOG.md'</text><text class="terminal-2035912735-r2" x="866.2" y="483.6" textLength="12.2" clip-path="url(#terminal-2035912735-line-19)">)</text><text class="terminal-2035912735-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-2035912735-line-19)"> -</text><text class="terminal-2035912735-r1" x="0" y="508" textLength="500.2" clip-path="url(#terminal-2035912735-line-20)">  --unreleased-version UNRELEASED_VERSION</text><text class="terminal-2035912735-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-2035912735-line-20)"> -</text><text class="terminal-2035912735-r1" x="0" y="532.4" textLength="707.6" clip-path="url(#terminal-2035912735-line-21)">                        set the value for the new version </text><text class="terminal-2035912735-r2" x="707.6" y="532.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-21)">(</text><text class="terminal-2035912735-r1" x="719.8" y="532.4" textLength="207.4" clip-path="url(#terminal-2035912735-line-21)">use the tag value</text><text class="terminal-2035912735-r2" x="927.2" y="532.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-21)">)</text><text class="terminal-2035912735-r1" x="939.4" y="532.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-21)">,</text><text class="terminal-2035912735-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-21)"> -</text><text class="terminal-2035912735-r1" x="0" y="556.8" textLength="622.2" clip-path="url(#terminal-2035912735-line-22)">                        instead of using unreleased</text><text class="terminal-2035912735-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-22)"> -</text><text class="terminal-2035912735-r1" x="0" y="581.2" textLength="939.4" clip-path="url(#terminal-2035912735-line-23)">  --incremental         generates changelog from last created version, useful</text><text class="terminal-2035912735-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-23)"> -</text><text class="terminal-2035912735-r1" x="0" y="605.6" textLength="817.4" clip-path="url(#terminal-2035912735-line-24)">                        if the changelog has been manually modified</text><text class="terminal-2035912735-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-2035912735-line-24)"> -</text><text class="terminal-2035912735-r1" x="0" y="630" textLength="280.6" clip-path="url(#terminal-2035912735-line-25)">  --start-rev START_REV</text><text class="terminal-2035912735-r1" x="976" y="630" textLength="12.2" clip-path="url(#terminal-2035912735-line-25)"> -</text><text class="terminal-2035912735-r1" x="0" y="654.4" textLength="866.2" clip-path="url(#terminal-2035912735-line-26)">                        start rev of the changelog. If not set, it will</text><text class="terminal-2035912735-r1" x="976" y="654.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-26)"> -</text><text class="terminal-2035912735-r1" x="0" y="678.8" textLength="695.4" clip-path="url(#terminal-2035912735-line-27)">                        generate changelog from the start</text><text class="terminal-2035912735-r1" x="976" y="678.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-27)"> -</text><text class="terminal-2035912735-r1" x="0" y="703.2" textLength="915" clip-path="url(#terminal-2035912735-line-28)">  --merge-prerelease    collect all changes from prereleases into next non-</text><text class="terminal-2035912735-r1" x="976" y="703.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-28)"> -</text><text class="terminal-2035912735-r1" x="0" y="727.6" textLength="951.6" clip-path="url(#terminal-2035912735-line-29)">                        prerelease. If not set, it will include prereleases in</text><text class="terminal-2035912735-r1" x="976" y="727.6" textLength="12.2" clip-path="url(#terminal-2035912735-line-29)"> -</text><text class="terminal-2035912735-r1" x="0" y="752" textLength="451.4" clip-path="url(#terminal-2035912735-line-30)">                        the changelog</text><text class="terminal-2035912735-r1" x="976" y="752" textLength="12.2" clip-path="url(#terminal-2035912735-line-30)"> -</text><text class="terminal-2035912735-r1" x="0" y="776.4" textLength="231.8" clip-path="url(#terminal-2035912735-line-31)">  --version-scheme </text><text class="terminal-2035912735-r2" x="231.8" y="776.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-31)">{</text><text class="terminal-2035912735-r1" x="244" y="776.4" textLength="256.2" clip-path="url(#terminal-2035912735-line-31)">pep440,semver,semver2</text><text class="terminal-2035912735-r2" x="500.2" y="776.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-31)">}</text><text class="terminal-2035912735-r1" x="976" y="776.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-31)"> -</text><text class="terminal-2035912735-r1" x="0" y="800.8" textLength="549" clip-path="url(#terminal-2035912735-line-32)">                        choose version scheme</text><text class="terminal-2035912735-r1" x="976" y="800.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-32)"> -</text><text class="terminal-2035912735-r1" x="0" y="825.2" textLength="427" clip-path="url(#terminal-2035912735-line-33)">  --export-template EXPORT_TEMPLATE</text><text class="terminal-2035912735-r1" x="976" y="825.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-33)"> -</text><text class="terminal-2035912735-r1" x="0" y="849.6" textLength="927.2" clip-path="url(#terminal-2035912735-line-34)">                        Export the changelog template into this file instead</text><text class="terminal-2035912735-r1" x="976" y="849.6" textLength="12.2" clip-path="url(#terminal-2035912735-line-34)"> -</text><text class="terminal-2035912735-r1" x="0" y="874" textLength="475.8" clip-path="url(#terminal-2035912735-line-35)">                        of rendering it</text><text class="terminal-2035912735-r1" x="976" y="874" textLength="12.2" clip-path="url(#terminal-2035912735-line-35)"> -</text><text class="terminal-2035912735-r1" x="0" y="898.4" textLength="414.8" clip-path="url(#terminal-2035912735-line-36)">  --template TEMPLATE, -t TEMPLATE</text><text class="terminal-2035912735-r1" x="976" y="898.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-36)"> -</text><text class="terminal-2035912735-r1" x="0" y="922.8" textLength="646.6" clip-path="url(#terminal-2035912735-line-37)">                        changelog template file name </text><text class="terminal-2035912735-r2" x="646.6" y="922.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-37)">(</text><text class="terminal-2035912735-r1" x="658.8" y="922.8" textLength="280.6" clip-path="url(#terminal-2035912735-line-37)">relative to the current</text><text class="terminal-2035912735-r1" x="976" y="922.8" textLength="12.2" clip-path="url(#terminal-2035912735-line-37)"> -</text><text class="terminal-2035912735-r1" x="0" y="947.2" textLength="500.2" clip-path="url(#terminal-2035912735-line-38)">                        working directory</text><text class="terminal-2035912735-r2" x="500.2" y="947.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-38)">)</text><text class="terminal-2035912735-r1" x="976" y="947.2" textLength="12.2" clip-path="url(#terminal-2035912735-line-38)"> -</text><text class="terminal-2035912735-r1" x="0" y="971.6" textLength="305" clip-path="url(#terminal-2035912735-line-39)">  --extra EXTRA, -e EXTRA</text><text class="terminal-2035912735-r1" x="976" y="971.6" textLength="12.2" clip-path="url(#terminal-2035912735-line-39)"> -</text><text class="terminal-2035912735-r1" x="0" y="996" textLength="622.2" clip-path="url(#terminal-2035912735-line-40)">                        a changelog extra variable </text><text class="terminal-2035912735-r2" x="622.2" y="996" textLength="12.2" clip-path="url(#terminal-2035912735-line-40)">(</text><text class="terminal-2035912735-r1" x="634.4" y="996" textLength="146.4" clip-path="url(#terminal-2035912735-line-40)">in the form </text><text class="terminal-2035912735-r4" x="780.8" y="996" textLength="12.2" clip-path="url(#terminal-2035912735-line-40)">'</text><text class="terminal-2035912735-r4" x="793" y="996" textLength="36.6" clip-path="url(#terminal-2035912735-line-40)">key</text><text class="terminal-2035912735-r4" x="829.6" y="996" textLength="12.2" clip-path="url(#terminal-2035912735-line-40)">=</text><text class="terminal-2035912735-r4" x="841.8" y="996" textLength="61" clip-path="url(#terminal-2035912735-line-40)">value</text><text class="terminal-2035912735-r4" x="902.8" y="996" textLength="12.2" clip-path="url(#terminal-2035912735-line-40)">'</text><text class="terminal-2035912735-r2" x="915" y="996" textLength="12.2" clip-path="url(#terminal-2035912735-line-40)">)</text><text class="terminal-2035912735-r1" x="976" y="996" textLength="12.2" clip-path="url(#terminal-2035912735-line-40)"> -</text><text class="terminal-2035912735-r1" x="976" y="1020.4" textLength="12.2" clip-path="url(#terminal-2035912735-line-41)"> + <g class="terminal-771491591-matrix"> + <text class="terminal-771491591-r1" x="0" y="20" textLength="256.2" clip-path="url(#terminal-771491591-line-0)">$ cz changelog --help</text><text class="terminal-771491591-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-771491591-line-0)"> +</text><text class="terminal-771491591-r1" x="0" y="44.4" textLength="244" clip-path="url(#terminal-771491591-line-1)">usage: cz changelog </text><text class="terminal-771491591-r2" x="244" y="44.4" textLength="12.2" clip-path="url(#terminal-771491591-line-1)">[</text><text class="terminal-771491591-r1" x="256.2" y="44.4" textLength="24.4" clip-path="url(#terminal-771491591-line-1)">-h</text><text class="terminal-771491591-r2" x="280.6" y="44.4" textLength="12.2" clip-path="url(#terminal-771491591-line-1)">]</text><text class="terminal-771491591-r2" x="305" y="44.4" textLength="12.2" clip-path="url(#terminal-771491591-line-1)">[</text><text class="terminal-771491591-r1" x="317.2" y="44.4" textLength="109.8" clip-path="url(#terminal-771491591-line-1)">--dry-run</text><text class="terminal-771491591-r2" x="427" y="44.4" textLength="12.2" clip-path="url(#terminal-771491591-line-1)">]</text><text class="terminal-771491591-r2" x="451.4" y="44.4" textLength="12.2" clip-path="url(#terminal-771491591-line-1)">[</text><text class="terminal-771491591-r1" x="463.6" y="44.4" textLength="256.2" clip-path="url(#terminal-771491591-line-1)">--file-name FILE_NAME</text><text class="terminal-771491591-r2" x="719.8" y="44.4" textLength="12.2" clip-path="url(#terminal-771491591-line-1)">]</text><text class="terminal-771491591-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-771491591-line-1)"> +</text><text class="terminal-771491591-r2" x="244" y="68.8" textLength="12.2" clip-path="url(#terminal-771491591-line-2)">[</text><text class="terminal-771491591-r1" x="256.2" y="68.8" textLength="475.8" clip-path="url(#terminal-771491591-line-2)">--unreleased-version UNRELEASED_VERSION</text><text class="terminal-771491591-r2" x="732" y="68.8" textLength="12.2" clip-path="url(#terminal-771491591-line-2)">]</text><text class="terminal-771491591-r2" x="756.4" y="68.8" textLength="12.2" clip-path="url(#terminal-771491591-line-2)">[</text><text class="terminal-771491591-r1" x="768.6" y="68.8" textLength="158.6" clip-path="url(#terminal-771491591-line-2)">--incremental</text><text class="terminal-771491591-r2" x="927.2" y="68.8" textLength="12.2" clip-path="url(#terminal-771491591-line-2)">]</text><text class="terminal-771491591-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-771491591-line-2)"> +</text><text class="terminal-771491591-r2" x="244" y="93.2" textLength="12.2" clip-path="url(#terminal-771491591-line-3)">[</text><text class="terminal-771491591-r1" x="256.2" y="93.2" textLength="256.2" clip-path="url(#terminal-771491591-line-3)">--start-rev START_REV</text><text class="terminal-771491591-r2" x="512.4" y="93.2" textLength="12.2" clip-path="url(#terminal-771491591-line-3)">]</text><text class="terminal-771491591-r2" x="536.8" y="93.2" textLength="12.2" clip-path="url(#terminal-771491591-line-3)">[</text><text class="terminal-771491591-r1" x="549" y="93.2" textLength="219.6" clip-path="url(#terminal-771491591-line-3)">--merge-prerelease</text><text class="terminal-771491591-r2" x="768.6" y="93.2" textLength="12.2" clip-path="url(#terminal-771491591-line-3)">]</text><text class="terminal-771491591-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-771491591-line-3)"> +</text><text class="terminal-771491591-r2" x="244" y="117.6" textLength="12.2" clip-path="url(#terminal-771491591-line-4)">[</text><text class="terminal-771491591-r1" x="256.2" y="117.6" textLength="207.4" clip-path="url(#terminal-771491591-line-4)">--version-scheme </text><text class="terminal-771491591-r2" x="463.6" y="117.6" textLength="12.2" clip-path="url(#terminal-771491591-line-4)">{</text><text class="terminal-771491591-r1" x="475.8" y="117.6" textLength="256.2" clip-path="url(#terminal-771491591-line-4)">pep440,semver,semver2</text><text class="terminal-771491591-r2" x="732" y="117.6" textLength="12.2" clip-path="url(#terminal-771491591-line-4)">}</text><text class="terminal-771491591-r2" x="744.2" y="117.6" textLength="12.2" clip-path="url(#terminal-771491591-line-4)">]</text><text class="terminal-771491591-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-771491591-line-4)"> +</text><text class="terminal-771491591-r2" x="244" y="142" textLength="12.2" clip-path="url(#terminal-771491591-line-5)">[</text><text class="terminal-771491591-r1" x="256.2" y="142" textLength="402.6" clip-path="url(#terminal-771491591-line-5)">--export-template EXPORT_TEMPLATE</text><text class="terminal-771491591-r2" x="658.8" y="142" textLength="12.2" clip-path="url(#terminal-771491591-line-5)">]</text><text class="terminal-771491591-r2" x="683.2" y="142" textLength="12.2" clip-path="url(#terminal-771491591-line-5)">[</text><text class="terminal-771491591-r1" x="695.4" y="142" textLength="231.8" clip-path="url(#terminal-771491591-line-5)">--template TEMPLATE</text><text class="terminal-771491591-r2" x="927.2" y="142" textLength="12.2" clip-path="url(#terminal-771491591-line-5)">]</text><text class="terminal-771491591-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-771491591-line-5)"> +</text><text class="terminal-771491591-r2" x="244" y="166.4" textLength="12.2" clip-path="url(#terminal-771491591-line-6)">[</text><text class="terminal-771491591-r1" x="256.2" y="166.4" textLength="158.6" clip-path="url(#terminal-771491591-line-6)">--extra EXTRA</text><text class="terminal-771491591-r2" x="414.8" y="166.4" textLength="12.2" clip-path="url(#terminal-771491591-line-6)">]</text><text class="terminal-771491591-r2" x="439.2" y="166.4" textLength="12.2" clip-path="url(#terminal-771491591-line-6)">[</text><text class="terminal-771491591-r1" x="451.4" y="166.4" textLength="280.6" clip-path="url(#terminal-771491591-line-6)">--tag-format TAG_FORMAT</text><text class="terminal-771491591-r2" x="732" y="166.4" textLength="12.2" clip-path="url(#terminal-771491591-line-6)">]</text><text class="terminal-771491591-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-771491591-line-6)"> +</text><text class="terminal-771491591-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-771491591-line-7)"> +</text><text class="terminal-771491591-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-771491591-line-8)"> +</text><text class="terminal-771491591-r1" x="0" y="239.6" textLength="231.8" clip-path="url(#terminal-771491591-line-9)">Generate changelog </text><text class="terminal-771491591-r2" x="231.8" y="239.6" textLength="12.2" clip-path="url(#terminal-771491591-line-9)">(</text><text class="terminal-771491591-r1" x="244" y="239.6" textLength="512.4" clip-path="url(#terminal-771491591-line-9)">note that it will overwrite existing files</text><text class="terminal-771491591-r2" x="756.4" y="239.6" textLength="12.2" clip-path="url(#terminal-771491591-line-9)">)</text><text class="terminal-771491591-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-771491591-line-9)"> +</text><text class="terminal-771491591-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-771491591-line-10)"> +</text><text class="terminal-771491591-r1" x="0" y="288.4" textLength="256.2" clip-path="url(#terminal-771491591-line-11)">positional arguments:</text><text class="terminal-771491591-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-771491591-line-11)"> +</text><text class="terminal-771491591-r1" x="0" y="312.8" textLength="793" clip-path="url(#terminal-771491591-line-12)">  rev_range             Generate changelog for the given version </text><text class="terminal-771491591-r2" x="793" y="312.8" textLength="12.2" clip-path="url(#terminal-771491591-line-12)">(</text><text class="terminal-771491591-r1" x="805.2" y="312.8" textLength="73.2" clip-path="url(#terminal-771491591-line-12)">e.g., </text><text class="terminal-771491591-r3" x="878.4" y="312.8" textLength="36.6" clip-path="url(#terminal-771491591-line-12)">1.5</text><text class="terminal-771491591-r1" x="915" y="312.8" textLength="12.2" clip-path="url(#terminal-771491591-line-12)">.</text><text class="terminal-771491591-r3" x="927.2" y="312.8" textLength="12.2" clip-path="url(#terminal-771491591-line-12)">3</text><text class="terminal-771491591-r2" x="939.4" y="312.8" textLength="12.2" clip-path="url(#terminal-771491591-line-12)">)</text><text class="terminal-771491591-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-771491591-line-12)"> +</text><text class="terminal-771491591-r1" x="0" y="337.2" textLength="500.2" clip-path="url(#terminal-771491591-line-13)">                        or version range </text><text class="terminal-771491591-r2" x="500.2" y="337.2" textLength="12.2" clip-path="url(#terminal-771491591-line-13)">(</text><text class="terminal-771491591-r1" x="512.4" y="337.2" textLength="73.2" clip-path="url(#terminal-771491591-line-13)">e.g., </text><text class="terminal-771491591-r3" x="585.6" y="337.2" textLength="36.6" clip-path="url(#terminal-771491591-line-13)">1.5</text><text class="terminal-771491591-r1" x="622.2" y="337.2" textLength="12.2" clip-path="url(#terminal-771491591-line-13)">.</text><text class="terminal-771491591-r3" x="634.4" y="337.2" textLength="12.2" clip-path="url(#terminal-771491591-line-13)">3</text><text class="terminal-771491591-r1" x="646.6" y="337.2" textLength="24.4" clip-path="url(#terminal-771491591-line-13)">..</text><text class="terminal-771491591-r3" x="671" y="337.2" textLength="36.6" clip-path="url(#terminal-771491591-line-13)">1.7</text><text class="terminal-771491591-r1" x="707.6" y="337.2" textLength="12.2" clip-path="url(#terminal-771491591-line-13)">.</text><text class="terminal-771491591-r3" x="719.8" y="337.2" textLength="12.2" clip-path="url(#terminal-771491591-line-13)">9</text><text class="terminal-771491591-r2" x="732" y="337.2" textLength="12.2" clip-path="url(#terminal-771491591-line-13)">)</text><text class="terminal-771491591-r1" x="744.2" y="337.2" textLength="12.2" clip-path="url(#terminal-771491591-line-13)">.</text><text class="terminal-771491591-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-771491591-line-13)"> +</text><text class="terminal-771491591-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-771491591-line-14)"> +</text><text class="terminal-771491591-r1" x="0" y="386" textLength="97.6" clip-path="url(#terminal-771491591-line-15)">options:</text><text class="terminal-771491591-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-771491591-line-15)"> +</text><text class="terminal-771491591-r1" x="0" y="410.4" textLength="671" clip-path="url(#terminal-771491591-line-16)">  -h, --help            show this help message and exit</text><text class="terminal-771491591-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-771491591-line-16)"> +</text><text class="terminal-771491591-r1" x="0" y="434.8" textLength="597.8" clip-path="url(#terminal-771491591-line-17)">  --dry-run             Show changelog to stdout.</text><text class="terminal-771491591-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-771491591-line-17)"> +</text><text class="terminal-771491591-r1" x="0" y="459.2" textLength="280.6" clip-path="url(#terminal-771491591-line-18)">  --file-name FILE_NAME</text><text class="terminal-771491591-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-771491591-line-18)"> +</text><text class="terminal-771491591-r1" x="0" y="483.6" textLength="573.4" clip-path="url(#terminal-771491591-line-19)">                        File name of changelog </text><text class="terminal-771491591-r2" x="573.4" y="483.6" textLength="12.2" clip-path="url(#terminal-771491591-line-19)">(</text><text class="terminal-771491591-r1" x="585.6" y="483.6" textLength="109.8" clip-path="url(#terminal-771491591-line-19)">default: </text><text class="terminal-771491591-r4" x="695.4" y="483.6" textLength="170.8" clip-path="url(#terminal-771491591-line-19)">'CHANGELOG.md'</text><text class="terminal-771491591-r2" x="866.2" y="483.6" textLength="12.2" clip-path="url(#terminal-771491591-line-19)">)</text><text class="terminal-771491591-r1" x="878.4" y="483.6" textLength="12.2" clip-path="url(#terminal-771491591-line-19)">.</text><text class="terminal-771491591-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-771491591-line-19)"> +</text><text class="terminal-771491591-r1" x="0" y="508" textLength="500.2" clip-path="url(#terminal-771491591-line-20)">  --unreleased-version UNRELEASED_VERSION</text><text class="terminal-771491591-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-771491591-line-20)"> +</text><text class="terminal-771491591-r1" x="0" y="532.4" textLength="707.6" clip-path="url(#terminal-771491591-line-21)">                        Set the value for the new version </text><text class="terminal-771491591-r2" x="707.6" y="532.4" textLength="12.2" clip-path="url(#terminal-771491591-line-21)">(</text><text class="terminal-771491591-r1" x="719.8" y="532.4" textLength="207.4" clip-path="url(#terminal-771491591-line-21)">use the tag value</text><text class="terminal-771491591-r2" x="927.2" y="532.4" textLength="12.2" clip-path="url(#terminal-771491591-line-21)">)</text><text class="terminal-771491591-r1" x="939.4" y="532.4" textLength="12.2" clip-path="url(#terminal-771491591-line-21)">,</text><text class="terminal-771491591-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-771491591-line-21)"> +</text><text class="terminal-771491591-r1" x="0" y="556.8" textLength="744.2" clip-path="url(#terminal-771491591-line-22)">                        instead of using unreleased versions.</text><text class="terminal-771491591-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-771491591-line-22)"> +</text><text class="terminal-771491591-r1" x="0" y="581.2" textLength="890.6" clip-path="url(#terminal-771491591-line-23)">  --incremental         Generate changelog from the last created version,</text><text class="terminal-771491591-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-771491591-line-23)"> +</text><text class="terminal-771491591-r1" x="0" y="605.6" textLength="915" clip-path="url(#terminal-771491591-line-24)">                        useful if the changelog has been manually modified.</text><text class="terminal-771491591-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-771491591-line-24)"> +</text><text class="terminal-771491591-r1" x="0" y="630" textLength="280.6" clip-path="url(#terminal-771491591-line-25)">  --start-rev START_REV</text><text class="terminal-771491591-r1" x="976" y="630" textLength="12.2" clip-path="url(#terminal-771491591-line-25)"> +</text><text class="terminal-771491591-r1" x="0" y="654.4" textLength="866.2" clip-path="url(#terminal-771491591-line-26)">                        Start rev of the changelog. If not set, it will</text><text class="terminal-771491591-r1" x="976" y="654.4" textLength="12.2" clip-path="url(#terminal-771491591-line-26)"> +</text><text class="terminal-771491591-r1" x="0" y="678.8" textLength="756.4" clip-path="url(#terminal-771491591-line-27)">                        generate changelog from the beginning.</text><text class="terminal-771491591-r1" x="976" y="678.8" textLength="12.2" clip-path="url(#terminal-771491591-line-27)"> +</text><text class="terminal-771491591-r1" x="0" y="703.2" textLength="902.8" clip-path="url(#terminal-771491591-line-28)">  --merge-prerelease    Collect all changes from prereleases into the next</text><text class="terminal-771491591-r1" x="976" y="703.2" textLength="12.2" clip-path="url(#terminal-771491591-line-28)"> +</text><text class="terminal-771491591-r1" x="0" y="727.6" textLength="817.4" clip-path="url(#terminal-771491591-line-29)">                        non-prerelease. If not set, it will include</text><text class="terminal-771491591-r1" x="976" y="727.6" textLength="12.2" clip-path="url(#terminal-771491591-line-29)"> +</text><text class="terminal-771491591-r1" x="0" y="752" textLength="646.6" clip-path="url(#terminal-771491591-line-30)">                        prereleases in the changelog.</text><text class="terminal-771491591-r1" x="976" y="752" textLength="12.2" clip-path="url(#terminal-771491591-line-30)"> +</text><text class="terminal-771491591-r1" x="0" y="776.4" textLength="231.8" clip-path="url(#terminal-771491591-line-31)">  --version-scheme </text><text class="terminal-771491591-r2" x="231.8" y="776.4" textLength="12.2" clip-path="url(#terminal-771491591-line-31)">{</text><text class="terminal-771491591-r1" x="244" y="776.4" textLength="256.2" clip-path="url(#terminal-771491591-line-31)">pep440,semver,semver2</text><text class="terminal-771491591-r2" x="500.2" y="776.4" textLength="12.2" clip-path="url(#terminal-771491591-line-31)">}</text><text class="terminal-771491591-r1" x="976" y="776.4" textLength="12.2" clip-path="url(#terminal-771491591-line-31)"> +</text><text class="terminal-771491591-r1" x="0" y="800.8" textLength="561.2" clip-path="url(#terminal-771491591-line-32)">                        Choose version scheme.</text><text class="terminal-771491591-r1" x="976" y="800.8" textLength="12.2" clip-path="url(#terminal-771491591-line-32)"> +</text><text class="terminal-771491591-r1" x="0" y="825.2" textLength="427" clip-path="url(#terminal-771491591-line-33)">  --export-template EXPORT_TEMPLATE</text><text class="terminal-771491591-r1" x="976" y="825.2" textLength="12.2" clip-path="url(#terminal-771491591-line-33)"> +</text><text class="terminal-771491591-r1" x="0" y="849.6" textLength="927.2" clip-path="url(#terminal-771491591-line-34)">                        Export the changelog template into this file instead</text><text class="terminal-771491591-r1" x="976" y="849.6" textLength="12.2" clip-path="url(#terminal-771491591-line-34)"> +</text><text class="terminal-771491591-r1" x="0" y="874" textLength="488" clip-path="url(#terminal-771491591-line-35)">                        of rendering it.</text><text class="terminal-771491591-r1" x="976" y="874" textLength="12.2" clip-path="url(#terminal-771491591-line-35)"> +</text><text class="terminal-771491591-r1" x="0" y="898.4" textLength="414.8" clip-path="url(#terminal-771491591-line-36)">  --template TEMPLATE, -t TEMPLATE</text><text class="terminal-771491591-r1" x="976" y="898.4" textLength="12.2" clip-path="url(#terminal-771491591-line-36)"> +</text><text class="terminal-771491591-r1" x="0" y="922.8" textLength="646.6" clip-path="url(#terminal-771491591-line-37)">                        Changelog template file name </text><text class="terminal-771491591-r2" x="646.6" y="922.8" textLength="12.2" clip-path="url(#terminal-771491591-line-37)">(</text><text class="terminal-771491591-r1" x="658.8" y="922.8" textLength="280.6" clip-path="url(#terminal-771491591-line-37)">relative to the current</text><text class="terminal-771491591-r1" x="976" y="922.8" textLength="12.2" clip-path="url(#terminal-771491591-line-37)"> +</text><text class="terminal-771491591-r1" x="0" y="947.2" textLength="500.2" clip-path="url(#terminal-771491591-line-38)">                        working directory</text><text class="terminal-771491591-r2" x="500.2" y="947.2" textLength="12.2" clip-path="url(#terminal-771491591-line-38)">)</text><text class="terminal-771491591-r1" x="512.4" y="947.2" textLength="12.2" clip-path="url(#terminal-771491591-line-38)">.</text><text class="terminal-771491591-r1" x="976" y="947.2" textLength="12.2" clip-path="url(#terminal-771491591-line-38)"> +</text><text class="terminal-771491591-r1" x="0" y="971.6" textLength="305" clip-path="url(#terminal-771491591-line-39)">  --extra EXTRA, -e EXTRA</text><text class="terminal-771491591-r1" x="976" y="971.6" textLength="12.2" clip-path="url(#terminal-771491591-line-39)"> +</text><text class="terminal-771491591-r1" x="0" y="996" textLength="610" clip-path="url(#terminal-771491591-line-40)">                        Changelog extra variables </text><text class="terminal-771491591-r2" x="610" y="996" textLength="12.2" clip-path="url(#terminal-771491591-line-40)">(</text><text class="terminal-771491591-r1" x="622.2" y="996" textLength="146.4" clip-path="url(#terminal-771491591-line-40)">in the form </text><text class="terminal-771491591-r4" x="768.6" y="996" textLength="12.2" clip-path="url(#terminal-771491591-line-40)">'</text><text class="terminal-771491591-r4" x="780.8" y="996" textLength="36.6" clip-path="url(#terminal-771491591-line-40)">key</text><text class="terminal-771491591-r4" x="817.4" y="996" textLength="12.2" clip-path="url(#terminal-771491591-line-40)">=</text><text class="terminal-771491591-r4" x="829.6" y="996" textLength="61" clip-path="url(#terminal-771491591-line-40)">value</text><text class="terminal-771491591-r4" x="890.6" y="996" textLength="12.2" clip-path="url(#terminal-771491591-line-40)">'</text><text class="terminal-771491591-r2" x="902.8" y="996" textLength="12.2" clip-path="url(#terminal-771491591-line-40)">)</text><text class="terminal-771491591-r1" x="915" y="996" textLength="12.2" clip-path="url(#terminal-771491591-line-40)">.</text><text class="terminal-771491591-r1" x="976" y="996" textLength="12.2" clip-path="url(#terminal-771491591-line-40)"> +</text><text class="terminal-771491591-r1" x="0" y="1020.4" textLength="305" clip-path="url(#terminal-771491591-line-41)">  --tag-format TAG_FORMAT</text><text class="terminal-771491591-r1" x="976" y="1020.4" textLength="12.2" clip-path="url(#terminal-771491591-line-41)"> +</text><text class="terminal-771491591-r1" x="0" y="1044.8" textLength="890.6" clip-path="url(#terminal-771491591-line-42)">                        The format of the tag, wrap around simple quotes.</text><text class="terminal-771491591-r1" x="976" y="1044.8" textLength="12.2" clip-path="url(#terminal-771491591-line-42)"> +</text><text class="terminal-771491591-r1" x="976" y="1069.2" textLength="12.2" clip-path="url(#terminal-771491591-line-43)"> </text> </g> </g> diff --git a/docs/images/cli_help/cz_check___help.svg b/docs/images/cli_help/cz_check___help.svg index df1af46637..57dd5ee1a4 100644 --- a/docs/images/cli_help/cz_check___help.svg +++ b/docs/images/cli_help/cz_check___help.svg @@ -1,4 +1,4 @@ -<svg class="rich-terminal" viewBox="0 0 994 660.0" xmlns="http://www.w3.org/2000/svg"> +<svg class="rich-terminal" viewBox="0 0 994 830.8" xmlns="http://www.w3.org/2000/svg"> <!-- Generated with Rich https://www.textualize.io --> <style> @@ -19,138 +19,166 @@ font-weight: 700; } - .terminal-3657119390-matrix { + .terminal-3704235523-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3657119390-title { + .terminal-3704235523-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3657119390-r1 { fill: #c5c8c6 } -.terminal-3657119390-r2 { fill: #c5c8c6;font-weight: bold } -.terminal-3657119390-r3 { fill: #d0b344 } -.terminal-3657119390-r4 { fill: #68a0b3;font-weight: bold } + .terminal-3704235523-r1 { fill: #c5c8c6 } +.terminal-3704235523-r2 { fill: #c5c8c6;font-weight: bold } +.terminal-3704235523-r3 { fill: #d0b344 } +.terminal-3704235523-r4 { fill: #68a0b3;font-weight: bold } </style> <defs> - <clipPath id="terminal-3657119390-clip-terminal"> - <rect x="0" y="0" width="975.0" height="609.0" /> + <clipPath id="terminal-3704235523-clip-terminal"> + <rect x="0" y="0" width="975.0" height="779.8" /> </clipPath> - <clipPath id="terminal-3657119390-line-0"> + <clipPath id="terminal-3704235523-line-0"> <rect x="0" y="1.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-1"> +<clipPath id="terminal-3704235523-line-1"> <rect x="0" y="25.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-2"> +<clipPath id="terminal-3704235523-line-2"> <rect x="0" y="50.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-3"> +<clipPath id="terminal-3704235523-line-3"> <rect x="0" y="74.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-4"> +<clipPath id="terminal-3704235523-line-4"> <rect x="0" y="99.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-5"> +<clipPath id="terminal-3704235523-line-5"> <rect x="0" y="123.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-6"> +<clipPath id="terminal-3704235523-line-6"> <rect x="0" y="147.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-7"> +<clipPath id="terminal-3704235523-line-7"> <rect x="0" y="172.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-8"> +<clipPath id="terminal-3704235523-line-8"> <rect x="0" y="196.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-9"> +<clipPath id="terminal-3704235523-line-9"> <rect x="0" y="221.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-10"> +<clipPath id="terminal-3704235523-line-10"> <rect x="0" y="245.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-11"> +<clipPath id="terminal-3704235523-line-11"> <rect x="0" y="269.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-12"> +<clipPath id="terminal-3704235523-line-12"> <rect x="0" y="294.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-13"> +<clipPath id="terminal-3704235523-line-13"> <rect x="0" y="318.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-14"> +<clipPath id="terminal-3704235523-line-14"> <rect x="0" y="343.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-15"> +<clipPath id="terminal-3704235523-line-15"> <rect x="0" y="367.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-16"> +<clipPath id="terminal-3704235523-line-16"> <rect x="0" y="391.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-17"> +<clipPath id="terminal-3704235523-line-17"> <rect x="0" y="416.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-18"> +<clipPath id="terminal-3704235523-line-18"> <rect x="0" y="440.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-19"> +<clipPath id="terminal-3704235523-line-19"> <rect x="0" y="465.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-20"> +<clipPath id="terminal-3704235523-line-20"> <rect x="0" y="489.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-21"> +<clipPath id="terminal-3704235523-line-21"> <rect x="0" y="513.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-22"> +<clipPath id="terminal-3704235523-line-22"> <rect x="0" y="538.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3657119390-line-23"> +<clipPath id="terminal-3704235523-line-23"> <rect x="0" y="562.7" width="976" height="24.65"/> </clipPath> +<clipPath id="terminal-3704235523-line-24"> + <rect x="0" y="587.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-3704235523-line-25"> + <rect x="0" y="611.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-3704235523-line-26"> + <rect x="0" y="635.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-3704235523-line-27"> + <rect x="0" y="660.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-3704235523-line-28"> + <rect x="0" y="684.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-3704235523-line-29"> + <rect x="0" y="709.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-3704235523-line-30"> + <rect x="0" y="733.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="658" rx="8"/> + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="828.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-3657119390-clip-terminal)"> + <g transform="translate(9, 41)" clip-path="url(#terminal-3704235523-clip-terminal)"> - <g class="terminal-3657119390-matrix"> - <text class="terminal-3657119390-r1" x="0" y="20" textLength="207.4" clip-path="url(#terminal-3657119390-line-0)">$ cz check --help</text><text class="terminal-3657119390-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-3657119390-line-0)"> -</text><text class="terminal-3657119390-r1" x="0" y="44.4" textLength="195.2" clip-path="url(#terminal-3657119390-line-1)">usage: cz check </text><text class="terminal-3657119390-r2" x="195.2" y="44.4" textLength="12.2" clip-path="url(#terminal-3657119390-line-1)">[</text><text class="terminal-3657119390-r1" x="207.4" y="44.4" textLength="24.4" clip-path="url(#terminal-3657119390-line-1)">-h</text><text class="terminal-3657119390-r2" x="231.8" y="44.4" textLength="12.2" clip-path="url(#terminal-3657119390-line-1)">]</text><text class="terminal-3657119390-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-3657119390-line-1)"> -</text><text class="terminal-3657119390-r2" x="195.2" y="68.8" textLength="12.2" clip-path="url(#terminal-3657119390-line-2)">[</text><text class="terminal-3657119390-r1" x="207.4" y="68.8" textLength="768.6" clip-path="url(#terminal-3657119390-line-2)">--commit-msg-file COMMIT_MSG_FILE | --rev-range REV_RANGE | -m </text><text class="terminal-3657119390-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-3657119390-line-2)"> -</text><text class="terminal-3657119390-r1" x="0" y="93.2" textLength="85.4" clip-path="url(#terminal-3657119390-line-3)">MESSAGE</text><text class="terminal-3657119390-r2" x="85.4" y="93.2" textLength="12.2" clip-path="url(#terminal-3657119390-line-3)">]</text><text class="terminal-3657119390-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-3657119390-line-3)"> -</text><text class="terminal-3657119390-r2" x="195.2" y="117.6" textLength="12.2" clip-path="url(#terminal-3657119390-line-4)">[</text><text class="terminal-3657119390-r1" x="207.4" y="117.6" textLength="158.6" clip-path="url(#terminal-3657119390-line-4)">--allow-abort</text><text class="terminal-3657119390-r2" x="366" y="117.6" textLength="12.2" clip-path="url(#terminal-3657119390-line-4)">]</text><text class="terminal-3657119390-r2" x="390.4" y="117.6" textLength="12.2" clip-path="url(#terminal-3657119390-line-4)">[</text><text class="terminal-3657119390-r1" x="402.6" y="117.6" textLength="231.8" clip-path="url(#terminal-3657119390-line-4)">--allowed-prefixes </text><text class="terminal-3657119390-r2" x="634.4" y="117.6" textLength="12.2" clip-path="url(#terminal-3657119390-line-4)">[</text><text class="terminal-3657119390-r1" x="646.6" y="117.6" textLength="207.4" clip-path="url(#terminal-3657119390-line-4)">ALLOWED_PREFIXES </text><text class="terminal-3657119390-r3" x="854" y="117.6" textLength="36.6" clip-path="url(#terminal-3657119390-line-4)">...</text><text class="terminal-3657119390-r2" x="890.6" y="117.6" textLength="12.2" clip-path="url(#terminal-3657119390-line-4)">]</text><text class="terminal-3657119390-r2" x="902.8" y="117.6" textLength="12.2" clip-path="url(#terminal-3657119390-line-4)">]</text><text class="terminal-3657119390-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-3657119390-line-4)"> -</text><text class="terminal-3657119390-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-3657119390-line-5)"> -</text><text class="terminal-3657119390-r1" x="0" y="166.4" textLength="744.2" clip-path="url(#terminal-3657119390-line-6)">validates that a commit message matches the commitizen schema</text><text class="terminal-3657119390-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-3657119390-line-6)"> -</text><text class="terminal-3657119390-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-3657119390-line-7)"> -</text><text class="terminal-3657119390-r1" x="0" y="215.2" textLength="97.6" clip-path="url(#terminal-3657119390-line-8)">options:</text><text class="terminal-3657119390-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-3657119390-line-8)"> -</text><text class="terminal-3657119390-r1" x="0" y="239.6" textLength="671" clip-path="url(#terminal-3657119390-line-9)">  -h, --help            show this help message and exit</text><text class="terminal-3657119390-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-3657119390-line-9)"> -</text><text class="terminal-3657119390-r1" x="0" y="264" textLength="427" clip-path="url(#terminal-3657119390-line-10)">  --commit-msg-file COMMIT_MSG_FILE</text><text class="terminal-3657119390-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-3657119390-line-10)"> -</text><text class="terminal-3657119390-r1" x="0" y="288.4" textLength="915" clip-path="url(#terminal-3657119390-line-11)">                        ask for the name of the temporal file that contains</text><text class="terminal-3657119390-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-3657119390-line-11)"> -</text><text class="terminal-3657119390-r1" x="0" y="312.8" textLength="902.8" clip-path="url(#terminal-3657119390-line-12)">                        the commit message. Using it in a git hook script:</text><text class="terminal-3657119390-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-3657119390-line-12)"> -</text><text class="terminal-3657119390-r3" x="292.8" y="337.2" textLength="97.6" clip-path="url(#terminal-3657119390-line-13)">MSG_FILE</text><text class="terminal-3657119390-r1" x="390.4" y="337.2" textLength="24.4" clip-path="url(#terminal-3657119390-line-13)">=$</text><text class="terminal-3657119390-r4" x="414.8" y="337.2" textLength="12.2" clip-path="url(#terminal-3657119390-line-13)">1</text><text class="terminal-3657119390-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-3657119390-line-13)"> -</text><text class="terminal-3657119390-r1" x="0" y="361.6" textLength="280.6" clip-path="url(#terminal-3657119390-line-14)">  --rev-range REV_RANGE</text><text class="terminal-3657119390-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-3657119390-line-14)"> -</text><text class="terminal-3657119390-r1" x="0" y="386" textLength="854" clip-path="url(#terminal-3657119390-line-15)">                        a range of git rev to check. e.g, master..HEAD</text><text class="terminal-3657119390-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-3657119390-line-15)"> -</text><text class="terminal-3657119390-r1" x="0" y="410.4" textLength="378.2" clip-path="url(#terminal-3657119390-line-16)">  -m MESSAGE, --message MESSAGE</text><text class="terminal-3657119390-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-3657119390-line-16)"> -</text><text class="terminal-3657119390-r1" x="0" y="434.8" textLength="768.6" clip-path="url(#terminal-3657119390-line-17)">                        commit message that needs to be checked</text><text class="terminal-3657119390-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-3657119390-line-17)"> -</text><text class="terminal-3657119390-r1" x="0" y="459.2" textLength="927.2" clip-path="url(#terminal-3657119390-line-18)">  --allow-abort         allow empty commit messages, which typically abort a</text><text class="terminal-3657119390-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-3657119390-line-18)"> -</text><text class="terminal-3657119390-r1" x="0" y="483.6" textLength="366" clip-path="url(#terminal-3657119390-line-19)">                        commit</text><text class="terminal-3657119390-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-3657119390-line-19)"> -</text><text class="terminal-3657119390-r1" x="0" y="508" textLength="256.2" clip-path="url(#terminal-3657119390-line-20)">  --allowed-prefixes </text><text class="terminal-3657119390-r2" x="256.2" y="508" textLength="12.2" clip-path="url(#terminal-3657119390-line-20)">[</text><text class="terminal-3657119390-r1" x="268.4" y="508" textLength="207.4" clip-path="url(#terminal-3657119390-line-20)">ALLOWED_PREFIXES </text><text class="terminal-3657119390-r3" x="475.8" y="508" textLength="36.6" clip-path="url(#terminal-3657119390-line-20)">...</text><text class="terminal-3657119390-r2" x="512.4" y="508" textLength="12.2" clip-path="url(#terminal-3657119390-line-20)">]</text><text class="terminal-3657119390-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-3657119390-line-20)"> -</text><text class="terminal-3657119390-r1" x="0" y="532.4" textLength="951.6" clip-path="url(#terminal-3657119390-line-21)">                        allowed commit message prefixes. If the message starts</text><text class="terminal-3657119390-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-3657119390-line-21)"> -</text><text class="terminal-3657119390-r1" x="0" y="556.8" textLength="951.6" clip-path="url(#terminal-3657119390-line-22)">                        by one of these prefixes, the message won't be checked</text><text class="terminal-3657119390-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-3657119390-line-22)"> -</text><text class="terminal-3657119390-r1" x="0" y="581.2" textLength="500.2" clip-path="url(#terminal-3657119390-line-23)">                        against the regex</text><text class="terminal-3657119390-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-3657119390-line-23)"> -</text><text class="terminal-3657119390-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-3657119390-line-24)"> + <g class="terminal-3704235523-matrix"> + <text class="terminal-3704235523-r1" x="0" y="20" textLength="207.4" clip-path="url(#terminal-3704235523-line-0)">$ cz check --help</text><text class="terminal-3704235523-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-3704235523-line-0)"> +</text><text class="terminal-3704235523-r1" x="0" y="44.4" textLength="195.2" clip-path="url(#terminal-3704235523-line-1)">usage: cz check </text><text class="terminal-3704235523-r2" x="195.2" y="44.4" textLength="12.2" clip-path="url(#terminal-3704235523-line-1)">[</text><text class="terminal-3704235523-r1" x="207.4" y="44.4" textLength="24.4" clip-path="url(#terminal-3704235523-line-1)">-h</text><text class="terminal-3704235523-r2" x="231.8" y="44.4" textLength="12.2" clip-path="url(#terminal-3704235523-line-1)">]</text><text class="terminal-3704235523-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-3704235523-line-1)"> +</text><text class="terminal-3704235523-r2" x="195.2" y="68.8" textLength="12.2" clip-path="url(#terminal-3704235523-line-2)">[</text><text class="terminal-3704235523-r1" x="207.4" y="68.8" textLength="768.6" clip-path="url(#terminal-3704235523-line-2)">--commit-msg-file COMMIT_MSG_FILE | --rev-range REV_RANGE | -d </text><text class="terminal-3704235523-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-3704235523-line-2)"> +</text><text class="terminal-3704235523-r1" x="0" y="93.2" textLength="146.4" clip-path="url(#terminal-3704235523-line-3)">| -m MESSAGE</text><text class="terminal-3704235523-r2" x="146.4" y="93.2" textLength="12.2" clip-path="url(#terminal-3704235523-line-3)">]</text><text class="terminal-3704235523-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-3704235523-line-3)"> +</text><text class="terminal-3704235523-r2" x="195.2" y="117.6" textLength="12.2" clip-path="url(#terminal-3704235523-line-4)">[</text><text class="terminal-3704235523-r1" x="207.4" y="117.6" textLength="158.6" clip-path="url(#terminal-3704235523-line-4)">--allow-abort</text><text class="terminal-3704235523-r2" x="366" y="117.6" textLength="12.2" clip-path="url(#terminal-3704235523-line-4)">]</text><text class="terminal-3704235523-r2" x="390.4" y="117.6" textLength="12.2" clip-path="url(#terminal-3704235523-line-4)">[</text><text class="terminal-3704235523-r1" x="402.6" y="117.6" textLength="231.8" clip-path="url(#terminal-3704235523-line-4)">--allowed-prefixes </text><text class="terminal-3704235523-r2" x="634.4" y="117.6" textLength="12.2" clip-path="url(#terminal-3704235523-line-4)">[</text><text class="terminal-3704235523-r1" x="646.6" y="117.6" textLength="207.4" clip-path="url(#terminal-3704235523-line-4)">ALLOWED_PREFIXES </text><text class="terminal-3704235523-r3" x="854" y="117.6" textLength="36.6" clip-path="url(#terminal-3704235523-line-4)">...</text><text class="terminal-3704235523-r2" x="890.6" y="117.6" textLength="12.2" clip-path="url(#terminal-3704235523-line-4)">]</text><text class="terminal-3704235523-r2" x="902.8" y="117.6" textLength="12.2" clip-path="url(#terminal-3704235523-line-4)">]</text><text class="terminal-3704235523-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-3704235523-line-4)"> +</text><text class="terminal-3704235523-r2" x="195.2" y="142" textLength="12.2" clip-path="url(#terminal-3704235523-line-5)">[</text><text class="terminal-3704235523-r1" x="207.4" y="142" textLength="280.6" clip-path="url(#terminal-3704235523-line-5)">-l MESSAGE_LENGTH_LIMIT</text><text class="terminal-3704235523-r2" x="488" y="142" textLength="12.2" clip-path="url(#terminal-3704235523-line-5)">]</text><text class="terminal-3704235523-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-3704235523-line-5)"> +</text><text class="terminal-3704235523-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-3704235523-line-6)"> +</text><text class="terminal-3704235523-r1" x="0" y="190.8" textLength="732" clip-path="url(#terminal-3704235523-line-7)">Validate that a commit message matches the commitizen schema</text><text class="terminal-3704235523-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-3704235523-line-7)"> +</text><text class="terminal-3704235523-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-3704235523-line-8)"> +</text><text class="terminal-3704235523-r1" x="0" y="239.6" textLength="97.6" clip-path="url(#terminal-3704235523-line-9)">options:</text><text class="terminal-3704235523-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-3704235523-line-9)"> +</text><text class="terminal-3704235523-r1" x="0" y="264" textLength="671" clip-path="url(#terminal-3704235523-line-10)">  -h, --help            show this help message and exit</text><text class="terminal-3704235523-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-3704235523-line-10)"> +</text><text class="terminal-3704235523-r1" x="0" y="288.4" textLength="427" clip-path="url(#terminal-3704235523-line-11)">  --commit-msg-file COMMIT_MSG_FILE</text><text class="terminal-3704235523-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-3704235523-line-11)"> +</text><text class="terminal-3704235523-r1" x="0" y="312.8" textLength="927.2" clip-path="url(#terminal-3704235523-line-12)">                        Ask for the name of the temporary file that contains</text><text class="terminal-3704235523-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-3704235523-line-12)"> +</text><text class="terminal-3704235523-r1" x="0" y="337.2" textLength="878.4" clip-path="url(#terminal-3704235523-line-13)">                        the commit message. Use it in a git hook script:</text><text class="terminal-3704235523-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-3704235523-line-13)"> +</text><text class="terminal-3704235523-r3" x="292.8" y="361.6" textLength="97.6" clip-path="url(#terminal-3704235523-line-14)">MSG_FILE</text><text class="terminal-3704235523-r1" x="390.4" y="361.6" textLength="24.4" clip-path="url(#terminal-3704235523-line-14)">=$</text><text class="terminal-3704235523-r4" x="414.8" y="361.6" textLength="12.2" clip-path="url(#terminal-3704235523-line-14)">1</text><text class="terminal-3704235523-r1" x="427" y="361.6" textLength="12.2" clip-path="url(#terminal-3704235523-line-14)">.</text><text class="terminal-3704235523-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-3704235523-line-14)"> +</text><text class="terminal-3704235523-r1" x="0" y="386" textLength="280.6" clip-path="url(#terminal-3704235523-line-15)">  --rev-range REV_RANGE</text><text class="terminal-3704235523-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-3704235523-line-15)"> +</text><text class="terminal-3704235523-r1" x="0" y="410.4" textLength="915" clip-path="url(#terminal-3704235523-line-16)">                        Validate the commits in the given range of git rev,</text><text class="terminal-3704235523-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-3704235523-line-16)"> +</text><text class="terminal-3704235523-r1" x="0" y="434.8" textLength="524.6" clip-path="url(#terminal-3704235523-line-17)">                        e.g., master..HEAD.</text><text class="terminal-3704235523-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-3704235523-line-17)"> +</text><text class="terminal-3704235523-r1" x="0" y="459.2" textLength="305" clip-path="url(#terminal-3704235523-line-18)">  -d, --use-default-range</text><text class="terminal-3704235523-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-3704235523-line-18)"> +</text><text class="terminal-3704235523-r1" x="0" y="483.6" textLength="939.4" clip-path="url(#terminal-3704235523-line-19)">                        Validate the commits from the default branch to HEAD,</text><text class="terminal-3704235523-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-3704235523-line-19)"> +</text><text class="terminal-3704235523-r1" x="0" y="508" textLength="768.6" clip-path="url(#terminal-3704235523-line-20)">                        e.g., refs/remotes/origin/master..HEAD.</text><text class="terminal-3704235523-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-3704235523-line-20)"> +</text><text class="terminal-3704235523-r1" x="0" y="532.4" textLength="378.2" clip-path="url(#terminal-3704235523-line-21)">  -m MESSAGE, --message MESSAGE</text><text class="terminal-3704235523-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-3704235523-line-21)"> +</text><text class="terminal-3704235523-r1" x="0" y="556.8" textLength="707.6" clip-path="url(#terminal-3704235523-line-22)">                        Validate the given commit message.</text><text class="terminal-3704235523-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-3704235523-line-22)"> +</text><text class="terminal-3704235523-r1" x="0" y="581.2" textLength="927.2" clip-path="url(#terminal-3704235523-line-23)">  --allow-abort         Allow empty commit messages, which typically abort a</text><text class="terminal-3704235523-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-3704235523-line-23)"> +</text><text class="terminal-3704235523-r1" x="0" y="605.6" textLength="378.2" clip-path="url(#terminal-3704235523-line-24)">                        commit.</text><text class="terminal-3704235523-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-3704235523-line-24)"> +</text><text class="terminal-3704235523-r1" x="0" y="630" textLength="256.2" clip-path="url(#terminal-3704235523-line-25)">  --allowed-prefixes </text><text class="terminal-3704235523-r2" x="256.2" y="630" textLength="12.2" clip-path="url(#terminal-3704235523-line-25)">[</text><text class="terminal-3704235523-r1" x="268.4" y="630" textLength="207.4" clip-path="url(#terminal-3704235523-line-25)">ALLOWED_PREFIXES </text><text class="terminal-3704235523-r3" x="475.8" y="630" textLength="36.6" clip-path="url(#terminal-3704235523-line-25)">...</text><text class="terminal-3704235523-r2" x="512.4" y="630" textLength="12.2" clip-path="url(#terminal-3704235523-line-25)">]</text><text class="terminal-3704235523-r1" x="976" y="630" textLength="12.2" clip-path="url(#terminal-3704235523-line-25)"> +</text><text class="terminal-3704235523-r1" x="0" y="654.4" textLength="915" clip-path="url(#terminal-3704235523-line-26)">                        Skip validation for commit messages that start with</text><text class="terminal-3704235523-r1" x="976" y="654.4" textLength="12.2" clip-path="url(#terminal-3704235523-line-26)"> +</text><text class="terminal-3704235523-r1" x="0" y="678.8" textLength="573.4" clip-path="url(#terminal-3704235523-line-27)">                        the specified prefixes.</text><text class="terminal-3704235523-r1" x="976" y="678.8" textLength="12.2" clip-path="url(#terminal-3704235523-line-27)"> +</text><text class="terminal-3704235523-r1" x="0" y="703.2" textLength="854" clip-path="url(#terminal-3704235523-line-28)">  -l MESSAGE_LENGTH_LIMIT, --message-length-limit MESSAGE_LENGTH_LIMIT</text><text class="terminal-3704235523-r1" x="976" y="703.2" textLength="12.2" clip-path="url(#terminal-3704235523-line-28)"> +</text><text class="terminal-3704235523-r1" x="0" y="727.6" textLength="878.4" clip-path="url(#terminal-3704235523-line-29)">                        Restrict the length of the **first line** of the</text><text class="terminal-3704235523-r1" x="976" y="727.6" textLength="12.2" clip-path="url(#terminal-3704235523-line-29)"> +</text><text class="terminal-3704235523-r1" x="0" y="752" textLength="488" clip-path="url(#terminal-3704235523-line-30)">                        commit message; </text><text class="terminal-3704235523-r4" x="488" y="752" textLength="12.2" clip-path="url(#terminal-3704235523-line-30)">0</text><text class="terminal-3704235523-r1" x="500.2" y="752" textLength="170.8" clip-path="url(#terminal-3704235523-line-30)"> for no limit.</text><text class="terminal-3704235523-r1" x="976" y="752" textLength="12.2" clip-path="url(#terminal-3704235523-line-30)"> +</text><text class="terminal-3704235523-r1" x="976" y="776.4" textLength="12.2" clip-path="url(#terminal-3704235523-line-31)"> </text> </g> </g> diff --git a/docs/images/cli_help/cz_commit___help.svg b/docs/images/cli_help/cz_commit___help.svg index 1b7c98953c..633cea8fd0 100644 --- a/docs/images/cli_help/cz_commit___help.svg +++ b/docs/images/cli_help/cz_commit___help.svg @@ -1,4 +1,4 @@ -<svg class="rich-terminal" viewBox="0 0 994 586.8" xmlns="http://www.w3.org/2000/svg"> +<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> @@ -19,125 +19,145 @@ font-weight: 700; } - .terminal-3906085552-matrix { + .terminal-2417358551-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3906085552-title { + .terminal-2417358551-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3906085552-r1 { fill: #c5c8c6 } -.terminal-3906085552-r2 { fill: #c5c8c6;font-weight: bold } -.terminal-3906085552-r3 { fill: #68a0b3;font-weight: bold } + .terminal-2417358551-r1 { fill: #c5c8c6 } +.terminal-2417358551-r2 { fill: #c5c8c6;font-weight: bold } +.terminal-2417358551-r3 { fill: #68a0b3;font-weight: bold } </style> <defs> - <clipPath id="terminal-3906085552-clip-terminal"> - <rect x="0" y="0" width="975.0" height="535.8" /> + <clipPath id="terminal-2417358551-clip-terminal"> + <rect x="0" y="0" width="975.0" height="657.8" /> </clipPath> - <clipPath id="terminal-3906085552-line-0"> + <clipPath id="terminal-2417358551-line-0"> <rect x="0" y="1.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-1"> +<clipPath id="terminal-2417358551-line-1"> <rect x="0" y="25.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-2"> +<clipPath id="terminal-2417358551-line-2"> <rect x="0" y="50.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-3"> +<clipPath id="terminal-2417358551-line-3"> <rect x="0" y="74.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-4"> +<clipPath id="terminal-2417358551-line-4"> <rect x="0" y="99.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-5"> +<clipPath id="terminal-2417358551-line-5"> <rect x="0" y="123.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-6"> +<clipPath id="terminal-2417358551-line-6"> <rect x="0" y="147.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-7"> +<clipPath id="terminal-2417358551-line-7"> <rect x="0" y="172.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-8"> +<clipPath id="terminal-2417358551-line-8"> <rect x="0" y="196.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-9"> +<clipPath id="terminal-2417358551-line-9"> <rect x="0" y="221.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-10"> +<clipPath id="terminal-2417358551-line-10"> <rect x="0" y="245.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-11"> +<clipPath id="terminal-2417358551-line-11"> <rect x="0" y="269.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-12"> +<clipPath id="terminal-2417358551-line-12"> <rect x="0" y="294.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-13"> +<clipPath id="terminal-2417358551-line-13"> <rect x="0" y="318.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-14"> +<clipPath id="terminal-2417358551-line-14"> <rect x="0" y="343.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-15"> +<clipPath id="terminal-2417358551-line-15"> <rect x="0" y="367.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-16"> +<clipPath id="terminal-2417358551-line-16"> <rect x="0" y="391.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-17"> +<clipPath id="terminal-2417358551-line-17"> <rect x="0" y="416.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-18"> +<clipPath id="terminal-2417358551-line-18"> <rect x="0" y="440.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-19"> +<clipPath id="terminal-2417358551-line-19"> <rect x="0" y="465.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-3906085552-line-20"> +<clipPath id="terminal-2417358551-line-20"> <rect x="0" y="489.5" width="976" height="24.65"/> </clipPath> +<clipPath id="terminal-2417358551-line-21"> + <rect x="0" y="513.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2417358551-line-22"> + <rect x="0" y="538.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2417358551-line-23"> + <rect x="0" y="562.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2417358551-line-24"> + <rect x="0" y="587.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-2417358551-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="584.8" rx="8"/> + <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-3906085552-clip-terminal)"> + <g transform="translate(9, 41)" clip-path="url(#terminal-2417358551-clip-terminal)"> - <g class="terminal-3906085552-matrix"> - <text class="terminal-3906085552-r1" x="0" y="20" textLength="219.6" clip-path="url(#terminal-3906085552-line-0)">$ cz commit --help</text><text class="terminal-3906085552-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-3906085552-line-0)"> -</text><text class="terminal-3906085552-r1" x="0" y="44.4" textLength="207.4" clip-path="url(#terminal-3906085552-line-1)">usage: cz commit </text><text class="terminal-3906085552-r2" x="207.4" y="44.4" textLength="12.2" clip-path="url(#terminal-3906085552-line-1)">[</text><text class="terminal-3906085552-r1" x="219.6" y="44.4" textLength="24.4" clip-path="url(#terminal-3906085552-line-1)">-h</text><text class="terminal-3906085552-r2" x="244" y="44.4" textLength="12.2" clip-path="url(#terminal-3906085552-line-1)">]</text><text class="terminal-3906085552-r2" x="268.4" y="44.4" textLength="12.2" clip-path="url(#terminal-3906085552-line-1)">[</text><text class="terminal-3906085552-r1" x="280.6" y="44.4" textLength="85.4" clip-path="url(#terminal-3906085552-line-1)">--retry</text><text class="terminal-3906085552-r2" x="366" y="44.4" textLength="12.2" clip-path="url(#terminal-3906085552-line-1)">]</text><text class="terminal-3906085552-r2" x="390.4" y="44.4" textLength="12.2" clip-path="url(#terminal-3906085552-line-1)">[</text><text class="terminal-3906085552-r1" x="402.6" y="44.4" textLength="122" clip-path="url(#terminal-3906085552-line-1)">--no-retry</text><text class="terminal-3906085552-r2" x="524.6" y="44.4" textLength="12.2" clip-path="url(#terminal-3906085552-line-1)">]</text><text class="terminal-3906085552-r2" x="549" y="44.4" textLength="12.2" clip-path="url(#terminal-3906085552-line-1)">[</text><text class="terminal-3906085552-r1" x="561.2" y="44.4" textLength="109.8" clip-path="url(#terminal-3906085552-line-1)">--dry-run</text><text class="terminal-3906085552-r2" x="671" y="44.4" textLength="12.2" clip-path="url(#terminal-3906085552-line-1)">]</text><text class="terminal-3906085552-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-3906085552-line-1)"> -</text><text class="terminal-3906085552-r2" x="207.4" y="68.8" textLength="12.2" clip-path="url(#terminal-3906085552-line-2)">[</text><text class="terminal-3906085552-r1" x="219.6" y="68.8" textLength="402.6" clip-path="url(#terminal-3906085552-line-2)">--write-message-to-file FILE_PATH</text><text class="terminal-3906085552-r2" x="622.2" y="68.8" textLength="12.2" clip-path="url(#terminal-3906085552-line-2)">]</text><text class="terminal-3906085552-r2" x="646.6" y="68.8" textLength="12.2" clip-path="url(#terminal-3906085552-line-2)">[</text><text class="terminal-3906085552-r1" x="658.8" y="68.8" textLength="24.4" clip-path="url(#terminal-3906085552-line-2)">-s</text><text class="terminal-3906085552-r2" x="683.2" y="68.8" textLength="12.2" clip-path="url(#terminal-3906085552-line-2)">]</text><text class="terminal-3906085552-r2" x="707.6" y="68.8" textLength="12.2" clip-path="url(#terminal-3906085552-line-2)">[</text><text class="terminal-3906085552-r1" x="719.8" y="68.8" textLength="24.4" clip-path="url(#terminal-3906085552-line-2)">-a</text><text class="terminal-3906085552-r2" x="744.2" y="68.8" textLength="12.2" clip-path="url(#terminal-3906085552-line-2)">]</text><text class="terminal-3906085552-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-3906085552-line-2)"> -</text><text class="terminal-3906085552-r2" x="207.4" y="93.2" textLength="12.2" clip-path="url(#terminal-3906085552-line-3)">[</text><text class="terminal-3906085552-r1" x="219.6" y="93.2" textLength="280.6" clip-path="url(#terminal-3906085552-line-3)">-l MESSAGE_LENGTH_LIMIT</text><text class="terminal-3906085552-r2" x="500.2" y="93.2" textLength="12.2" clip-path="url(#terminal-3906085552-line-3)">]</text><text class="terminal-3906085552-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-3906085552-line-3)"> -</text><text class="terminal-3906085552-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-3906085552-line-4)"> -</text><text class="terminal-3906085552-r1" x="0" y="142" textLength="207.4" clip-path="url(#terminal-3906085552-line-5)">create new commit</text><text class="terminal-3906085552-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-3906085552-line-5)"> -</text><text class="terminal-3906085552-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-3906085552-line-6)"> -</text><text class="terminal-3906085552-r1" x="0" y="190.8" textLength="97.6" clip-path="url(#terminal-3906085552-line-7)">options:</text><text class="terminal-3906085552-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-3906085552-line-7)"> -</text><text class="terminal-3906085552-r1" x="0" y="215.2" textLength="671" clip-path="url(#terminal-3906085552-line-8)">  -h, --help            show this help message and exit</text><text class="terminal-3906085552-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-3906085552-line-8)"> -</text><text class="terminal-3906085552-r1" x="0" y="239.6" textLength="500.2" clip-path="url(#terminal-3906085552-line-9)">  --retry               retry last commit</text><text class="terminal-3906085552-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-3906085552-line-9)"> -</text><text class="terminal-3906085552-r1" x="0" y="264" textLength="878.4" clip-path="url(#terminal-3906085552-line-10)">  --no-retry            skip retry if retry_after_failure is set to true</text><text class="terminal-3906085552-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-3906085552-line-10)"> -</text><text class="terminal-3906085552-r1" x="0" y="288.4" textLength="915" clip-path="url(#terminal-3906085552-line-11)">  --dry-run             show output to stdout, no commit, no modified files</text><text class="terminal-3906085552-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-3906085552-line-11)"> -</text><text class="terminal-3906085552-r1" x="0" y="312.8" textLength="427" clip-path="url(#terminal-3906085552-line-12)">  --write-message-to-file FILE_PATH</text><text class="terminal-3906085552-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-3906085552-line-12)"> -</text><text class="terminal-3906085552-r1" x="0" y="337.2" textLength="780.8" clip-path="url(#terminal-3906085552-line-13)">                        write message to file before committing </text><text class="terminal-3906085552-r2" x="780.8" y="337.2" textLength="12.2" clip-path="url(#terminal-3906085552-line-13)">(</text><text class="terminal-3906085552-r1" x="793" y="337.2" textLength="73.2" clip-path="url(#terminal-3906085552-line-13)">can be</text><text class="terminal-3906085552-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-3906085552-line-13)"> -</text><text class="terminal-3906085552-r1" x="0" y="361.6" textLength="573.4" clip-path="url(#terminal-3906085552-line-14)">                        combined with --dry-run</text><text class="terminal-3906085552-r2" x="573.4" y="361.6" textLength="12.2" clip-path="url(#terminal-3906085552-line-14)">)</text><text class="terminal-3906085552-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-3906085552-line-14)"> -</text><text class="terminal-3906085552-r1" x="0" y="386" textLength="524.6" clip-path="url(#terminal-3906085552-line-15)">  -s, --signoff         sign off the commit</text><text class="terminal-3906085552-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-3906085552-line-15)"> -</text><text class="terminal-3906085552-r1" x="0" y="410.4" textLength="902.8" clip-path="url(#terminal-3906085552-line-16)">  -a, --all             Tell the command to automatically stage files that</text><text class="terminal-3906085552-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-3906085552-line-16)"> -</text><text class="terminal-3906085552-r1" x="0" y="434.8" textLength="951.6" clip-path="url(#terminal-3906085552-line-17)">                        have been modified and deleted, but new files you have</text><text class="terminal-3906085552-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-3906085552-line-17)"> -</text><text class="terminal-3906085552-r1" x="0" y="459.2" textLength="732" clip-path="url(#terminal-3906085552-line-18)">                        not told Git about are not affected.</text><text class="terminal-3906085552-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-3906085552-line-18)"> -</text><text class="terminal-3906085552-r1" x="0" y="483.6" textLength="854" clip-path="url(#terminal-3906085552-line-19)">  -l MESSAGE_LENGTH_LIMIT, --message-length-limit MESSAGE_LENGTH_LIMIT</text><text class="terminal-3906085552-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-3906085552-line-19)"> -</text><text class="terminal-3906085552-r1" x="0" y="508" textLength="732" clip-path="url(#terminal-3906085552-line-20)">                        length limit of the commit message; </text><text class="terminal-3906085552-r3" x="732" y="508" textLength="12.2" clip-path="url(#terminal-3906085552-line-20)">0</text><text class="terminal-3906085552-r1" x="744.2" y="508" textLength="158.6" clip-path="url(#terminal-3906085552-line-20)"> for no limit</text><text class="terminal-3906085552-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-3906085552-line-20)"> -</text><text class="terminal-3906085552-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-3906085552-line-21)"> + <g class="terminal-2417358551-matrix"> + <text class="terminal-2417358551-r1" x="0" y="20" textLength="219.6" clip-path="url(#terminal-2417358551-line-0)">$ cz commit --help</text><text class="terminal-2417358551-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-2417358551-line-0)"> +</text><text class="terminal-2417358551-r1" x="0" y="44.4" textLength="207.4" clip-path="url(#terminal-2417358551-line-1)">usage: cz commit </text><text class="terminal-2417358551-r2" x="207.4" y="44.4" textLength="12.2" clip-path="url(#terminal-2417358551-line-1)">[</text><text class="terminal-2417358551-r1" x="219.6" y="44.4" textLength="24.4" clip-path="url(#terminal-2417358551-line-1)">-h</text><text class="terminal-2417358551-r2" x="244" y="44.4" textLength="12.2" clip-path="url(#terminal-2417358551-line-1)">]</text><text class="terminal-2417358551-r2" x="268.4" y="44.4" textLength="12.2" clip-path="url(#terminal-2417358551-line-1)">[</text><text class="terminal-2417358551-r1" x="280.6" y="44.4" textLength="85.4" clip-path="url(#terminal-2417358551-line-1)">--retry</text><text class="terminal-2417358551-r2" x="366" y="44.4" textLength="12.2" clip-path="url(#terminal-2417358551-line-1)">]</text><text class="terminal-2417358551-r2" x="390.4" y="44.4" textLength="12.2" clip-path="url(#terminal-2417358551-line-1)">[</text><text class="terminal-2417358551-r1" x="402.6" y="44.4" textLength="122" clip-path="url(#terminal-2417358551-line-1)">--no-retry</text><text class="terminal-2417358551-r2" x="524.6" y="44.4" textLength="12.2" clip-path="url(#terminal-2417358551-line-1)">]</text><text class="terminal-2417358551-r2" x="549" y="44.4" textLength="12.2" clip-path="url(#terminal-2417358551-line-1)">[</text><text class="terminal-2417358551-r1" x="561.2" y="44.4" textLength="109.8" clip-path="url(#terminal-2417358551-line-1)">--dry-run</text><text class="terminal-2417358551-r2" x="671" y="44.4" textLength="12.2" clip-path="url(#terminal-2417358551-line-1)">]</text><text class="terminal-2417358551-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-2417358551-line-1)"> +</text><text class="terminal-2417358551-r2" x="207.4" y="68.8" textLength="12.2" clip-path="url(#terminal-2417358551-line-2)">[</text><text class="terminal-2417358551-r1" x="219.6" y="68.8" textLength="402.6" clip-path="url(#terminal-2417358551-line-2)">--write-message-to-file FILE_PATH</text><text class="terminal-2417358551-r2" x="622.2" y="68.8" textLength="12.2" clip-path="url(#terminal-2417358551-line-2)">]</text><text class="terminal-2417358551-r2" x="646.6" y="68.8" textLength="12.2" clip-path="url(#terminal-2417358551-line-2)">[</text><text class="terminal-2417358551-r1" x="658.8" y="68.8" textLength="24.4" clip-path="url(#terminal-2417358551-line-2)">-s</text><text class="terminal-2417358551-r2" x="683.2" y="68.8" textLength="12.2" clip-path="url(#terminal-2417358551-line-2)">]</text><text class="terminal-2417358551-r2" x="707.6" y="68.8" textLength="12.2" clip-path="url(#terminal-2417358551-line-2)">[</text><text class="terminal-2417358551-r1" x="719.8" y="68.8" textLength="24.4" clip-path="url(#terminal-2417358551-line-2)">-a</text><text class="terminal-2417358551-r2" x="744.2" y="68.8" textLength="12.2" clip-path="url(#terminal-2417358551-line-2)">]</text><text class="terminal-2417358551-r2" x="768.6" y="68.8" textLength="12.2" clip-path="url(#terminal-2417358551-line-2)">[</text><text class="terminal-2417358551-r1" x="780.8" y="68.8" textLength="24.4" clip-path="url(#terminal-2417358551-line-2)">-e</text><text class="terminal-2417358551-r2" x="805.2" y="68.8" textLength="12.2" clip-path="url(#terminal-2417358551-line-2)">]</text><text class="terminal-2417358551-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-2417358551-line-2)"> +</text><text class="terminal-2417358551-r2" x="207.4" y="93.2" textLength="12.2" clip-path="url(#terminal-2417358551-line-3)">[</text><text class="terminal-2417358551-r1" x="219.6" y="93.2" textLength="280.6" clip-path="url(#terminal-2417358551-line-3)">-l MESSAGE_LENGTH_LIMIT</text><text class="terminal-2417358551-r2" x="500.2" y="93.2" textLength="12.2" clip-path="url(#terminal-2417358551-line-3)">]</text><text class="terminal-2417358551-r2" x="524.6" y="93.2" textLength="12.2" clip-path="url(#terminal-2417358551-line-3)">[</text><text class="terminal-2417358551-r1" x="536.8" y="93.2" textLength="24.4" clip-path="url(#terminal-2417358551-line-3)">--</text><text class="terminal-2417358551-r2" x="561.2" y="93.2" textLength="12.2" clip-path="url(#terminal-2417358551-line-3)">]</text><text class="terminal-2417358551-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-2417358551-line-3)"> +</text><text class="terminal-2417358551-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-2417358551-line-4)"> +</text><text class="terminal-2417358551-r1" x="0" y="142" textLength="207.4" clip-path="url(#terminal-2417358551-line-5)">Create new commit</text><text class="terminal-2417358551-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-2417358551-line-5)"> +</text><text class="terminal-2417358551-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-2417358551-line-6)"> +</text><text class="terminal-2417358551-r1" x="0" y="190.8" textLength="97.6" clip-path="url(#terminal-2417358551-line-7)">options:</text><text class="terminal-2417358551-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-2417358551-line-7)"> +</text><text class="terminal-2417358551-r1" x="0" y="215.2" textLength="671" clip-path="url(#terminal-2417358551-line-8)">  -h, --help            show this help message and exit</text><text class="terminal-2417358551-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-2417358551-line-8)"> +</text><text class="terminal-2417358551-r1" x="0" y="239.6" textLength="561.2" clip-path="url(#terminal-2417358551-line-9)">  --retry               Retry the last commit.</text><text class="terminal-2417358551-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-2417358551-line-9)"> +</text><text class="terminal-2417358551-r1" x="0" y="264" textLength="939.4" clip-path="url(#terminal-2417358551-line-10)">  --no-retry            Skip retry if --retry or `retry_after_failure` is set</text><text class="terminal-2417358551-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-2417358551-line-10)"> +</text><text class="terminal-2417358551-r1" x="0" y="288.4" textLength="390.4" clip-path="url(#terminal-2417358551-line-11)">                        to true.</text><text class="terminal-2417358551-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-2417358551-line-11)"> +</text><text class="terminal-2417358551-r1" x="0" y="312.8" textLength="902.8" clip-path="url(#terminal-2417358551-line-12)">  --dry-run             Perform a dry run, without committing or modifying</text><text class="terminal-2417358551-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-2417358551-line-12)"> +</text><text class="terminal-2417358551-r1" x="0" y="337.2" textLength="366" clip-path="url(#terminal-2417358551-line-13)">                        files.</text><text class="terminal-2417358551-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-2417358551-line-13)"> +</text><text class="terminal-2417358551-r1" x="0" y="361.6" textLength="427" clip-path="url(#terminal-2417358551-line-14)">  --write-message-to-file FILE_PATH</text><text class="terminal-2417358551-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-2417358551-line-14)"> +</text><text class="terminal-2417358551-r1" x="0" y="386" textLength="841.8" clip-path="url(#terminal-2417358551-line-15)">                        Write message to FILE_PATH before committing </text><text class="terminal-2417358551-r2" x="841.8" y="386" textLength="12.2" clip-path="url(#terminal-2417358551-line-15)">(</text><text class="terminal-2417358551-r1" x="854" y="386" textLength="73.2" clip-path="url(#terminal-2417358551-line-15)">can be</text><text class="terminal-2417358551-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-2417358551-line-15)"> +</text><text class="terminal-2417358551-r1" x="0" y="410.4" textLength="524.6" clip-path="url(#terminal-2417358551-line-16)">                        used with --dry-run</text><text class="terminal-2417358551-r2" x="524.6" y="410.4" textLength="12.2" clip-path="url(#terminal-2417358551-line-16)">)</text><text class="terminal-2417358551-r1" x="536.8" y="410.4" textLength="12.2" clip-path="url(#terminal-2417358551-line-16)">.</text><text class="terminal-2417358551-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-2417358551-line-16)"> +</text><text class="terminal-2417358551-r1" x="0" y="434.8" textLength="805.2" clip-path="url(#terminal-2417358551-line-17)">  -s, --signoff         Deprecated, use `cz commit -- -s` instead.</text><text class="terminal-2417358551-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-2417358551-line-17)"> +</text><text class="terminal-2417358551-r1" x="0" y="459.2" textLength="939.4" clip-path="url(#terminal-2417358551-line-18)">  -a, --all             Automatically stage files that have been modified and</text><text class="terminal-2417358551-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-2417358551-line-18)"> +</text><text class="terminal-2417358551-r1" x="0" y="483.6" textLength="951.6" clip-path="url(#terminal-2417358551-line-19)">                        deleted, but new files you have not told Git about are</text><text class="terminal-2417358551-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-2417358551-line-19)"> +</text><text class="terminal-2417358551-r1" x="0" y="508" textLength="451.4" clip-path="url(#terminal-2417358551-line-20)">                        not affected.</text><text class="terminal-2417358551-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-2417358551-line-20)"> +</text><text class="terminal-2417358551-r1" x="0" y="532.4" textLength="805.2" clip-path="url(#terminal-2417358551-line-21)">  -e, --edit            Edit the commit message before committing.</text><text class="terminal-2417358551-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-2417358551-line-21)"> +</text><text class="terminal-2417358551-r1" x="0" y="556.8" textLength="854" clip-path="url(#terminal-2417358551-line-22)">  -l MESSAGE_LENGTH_LIMIT, --message-length-limit MESSAGE_LENGTH_LIMIT</text><text class="terminal-2417358551-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-2417358551-line-22)"> +</text><text class="terminal-2417358551-r1" x="0" y="581.2" textLength="829.6" clip-path="url(#terminal-2417358551-line-23)">                        Set the length limit of the commit message; </text><text class="terminal-2417358551-r3" x="829.6" y="581.2" textLength="12.2" clip-path="url(#terminal-2417358551-line-23)">0</text><text class="terminal-2417358551-r1" x="841.8" y="581.2" textLength="85.4" clip-path="url(#terminal-2417358551-line-23)"> for no</text><text class="terminal-2417358551-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-2417358551-line-23)"> +</text><text class="terminal-2417358551-r1" x="0" y="605.6" textLength="366" clip-path="url(#terminal-2417358551-line-24)">                        limit.</text><text class="terminal-2417358551-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-2417358551-line-24)"> +</text><text class="terminal-2417358551-r1" x="0" y="630" textLength="671" clip-path="url(#terminal-2417358551-line-25)">  --                    Positional arguments separator </text><text class="terminal-2417358551-r2" x="671" y="630" textLength="12.2" clip-path="url(#terminal-2417358551-line-25)">(</text><text class="terminal-2417358551-r1" x="683.2" y="630" textLength="134.2" clip-path="url(#terminal-2417358551-line-25)">recommended</text><text class="terminal-2417358551-r2" x="817.4" y="630" textLength="12.2" clip-path="url(#terminal-2417358551-line-25)">)</text><text class="terminal-2417358551-r1" x="829.6" y="630" textLength="12.2" clip-path="url(#terminal-2417358551-line-25)">.</text><text class="terminal-2417358551-r1" x="976" y="630" textLength="12.2" clip-path="url(#terminal-2417358551-line-25)"> +</text><text class="terminal-2417358551-r1" x="976" y="654.4" textLength="12.2" clip-path="url(#terminal-2417358551-line-26)"> </text> </g> </g> diff --git a/docs/images/cli_help/cz_example___help.svg b/docs/images/cli_help/cz_example___help.svg index 9fe4fd659a..5ac29a22e3 100644 --- a/docs/images/cli_help/cz_example___help.svg +++ b/docs/images/cli_help/cz_example___help.svg @@ -19,46 +19,46 @@ font-weight: 700; } - .terminal-1643610534-matrix { + .terminal-703430360-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1643610534-title { + .terminal-703430360-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1643610534-r1 { fill: #c5c8c6 } -.terminal-1643610534-r2 { fill: #c5c8c6;font-weight: bold } + .terminal-703430360-r1 { fill: #c5c8c6 } +.terminal-703430360-r2 { fill: #c5c8c6;font-weight: bold } </style> <defs> - <clipPath id="terminal-1643610534-clip-terminal"> + <clipPath id="terminal-703430360-clip-terminal"> <rect x="0" y="0" width="975.0" height="194.2" /> </clipPath> - <clipPath id="terminal-1643610534-line-0"> + <clipPath id="terminal-703430360-line-0"> <rect x="0" y="1.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1643610534-line-1"> +<clipPath id="terminal-703430360-line-1"> <rect x="0" y="25.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1643610534-line-2"> +<clipPath id="terminal-703430360-line-2"> <rect x="0" y="50.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1643610534-line-3"> +<clipPath id="terminal-703430360-line-3"> <rect x="0" y="74.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1643610534-line-4"> +<clipPath id="terminal-703430360-line-4"> <rect x="0" y="99.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1643610534-line-5"> +<clipPath id="terminal-703430360-line-5"> <rect x="0" y="123.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1643610534-line-6"> +<clipPath id="terminal-703430360-line-6"> <rect x="0" y="147.9" width="976" height="24.65"/> </clipPath> </defs> @@ -70,17 +70,17 @@ <circle cx="44" cy="0" r="7" fill="#28c840"/> </g> - <g transform="translate(9, 41)" clip-path="url(#terminal-1643610534-clip-terminal)"> + <g transform="translate(9, 41)" clip-path="url(#terminal-703430360-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)"> + <g class="terminal-703430360-matrix"> + <text class="terminal-703430360-r1" x="0" y="20" textLength="231.8" clip-path="url(#terminal-703430360-line-0)">$ cz example --help</text><text class="terminal-703430360-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-703430360-line-0)"> +</text><text class="terminal-703430360-r1" x="0" y="44.4" textLength="219.6" clip-path="url(#terminal-703430360-line-1)">usage: cz example </text><text class="terminal-703430360-r2" x="219.6" y="44.4" textLength="12.2" clip-path="url(#terminal-703430360-line-1)">[</text><text class="terminal-703430360-r1" x="231.8" y="44.4" textLength="24.4" clip-path="url(#terminal-703430360-line-1)">-h</text><text class="terminal-703430360-r2" x="256.2" y="44.4" textLength="12.2" clip-path="url(#terminal-703430360-line-1)">]</text><text class="terminal-703430360-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-703430360-line-1)"> +</text><text class="terminal-703430360-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-703430360-line-2)"> +</text><text class="terminal-703430360-r1" x="0" y="93.2" textLength="231.8" clip-path="url(#terminal-703430360-line-3)">Show commit example</text><text class="terminal-703430360-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-703430360-line-3)"> +</text><text class="terminal-703430360-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-703430360-line-4)"> +</text><text class="terminal-703430360-r1" x="0" y="142" textLength="97.6" clip-path="url(#terminal-703430360-line-5)">options:</text><text class="terminal-703430360-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-703430360-line-5)"> +</text><text class="terminal-703430360-r1" x="0" y="166.4" textLength="549" clip-path="url(#terminal-703430360-line-6)">  -h, --help  show this help message and exit</text><text class="terminal-703430360-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-703430360-line-6)"> +</text><text class="terminal-703430360-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-703430360-line-7)"> </text> </g> </g> diff --git a/docs/images/cli_help/cz_info___help.svg b/docs/images/cli_help/cz_info___help.svg index b8827e34c2..63ce1ee394 100644 --- a/docs/images/cli_help/cz_info___help.svg +++ b/docs/images/cli_help/cz_info___help.svg @@ -19,46 +19,46 @@ font-weight: 700; } - .terminal-4196041424-matrix { + .terminal-3108929538-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4196041424-title { + .terminal-3108929538-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4196041424-r1 { fill: #c5c8c6 } -.terminal-4196041424-r2 { fill: #c5c8c6;font-weight: bold } + .terminal-3108929538-r1 { fill: #c5c8c6 } +.terminal-3108929538-r2 { fill: #c5c8c6;font-weight: bold } </style> <defs> - <clipPath id="terminal-4196041424-clip-terminal"> + <clipPath id="terminal-3108929538-clip-terminal"> <rect x="0" y="0" width="975.0" height="194.2" /> </clipPath> - <clipPath id="terminal-4196041424-line-0"> + <clipPath id="terminal-3108929538-line-0"> <rect x="0" y="1.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4196041424-line-1"> +<clipPath id="terminal-3108929538-line-1"> <rect x="0" y="25.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4196041424-line-2"> +<clipPath id="terminal-3108929538-line-2"> <rect x="0" y="50.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4196041424-line-3"> +<clipPath id="terminal-3108929538-line-3"> <rect x="0" y="74.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4196041424-line-4"> +<clipPath id="terminal-3108929538-line-4"> <rect x="0" y="99.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4196041424-line-5"> +<clipPath id="terminal-3108929538-line-5"> <rect x="0" y="123.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4196041424-line-6"> +<clipPath id="terminal-3108929538-line-6"> <rect x="0" y="147.9" width="976" height="24.65"/> </clipPath> </defs> @@ -70,17 +70,17 @@ <circle cx="44" cy="0" r="7" fill="#28c840"/> </g> - <g transform="translate(9, 41)" clip-path="url(#terminal-4196041424-clip-terminal)"> + <g transform="translate(9, 41)" clip-path="url(#terminal-3108929538-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)"> + <g class="terminal-3108929538-matrix"> + <text class="terminal-3108929538-r1" x="0" y="20" textLength="195.2" clip-path="url(#terminal-3108929538-line-0)">$ cz info --help</text><text class="terminal-3108929538-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-3108929538-line-0)"> +</text><text class="terminal-3108929538-r1" x="0" y="44.4" textLength="183" clip-path="url(#terminal-3108929538-line-1)">usage: cz info </text><text class="terminal-3108929538-r2" x="183" y="44.4" textLength="12.2" clip-path="url(#terminal-3108929538-line-1)">[</text><text class="terminal-3108929538-r1" x="195.2" y="44.4" textLength="24.4" clip-path="url(#terminal-3108929538-line-1)">-h</text><text class="terminal-3108929538-r2" x="219.6" y="44.4" textLength="12.2" clip-path="url(#terminal-3108929538-line-1)">]</text><text class="terminal-3108929538-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-3108929538-line-1)"> +</text><text class="terminal-3108929538-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-3108929538-line-2)"> +</text><text class="terminal-3108929538-r1" x="0" y="93.2" textLength="353.8" clip-path="url(#terminal-3108929538-line-3)">Show information about the cz</text><text class="terminal-3108929538-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-3108929538-line-3)"> +</text><text class="terminal-3108929538-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-3108929538-line-4)"> +</text><text class="terminal-3108929538-r1" x="0" y="142" textLength="97.6" clip-path="url(#terminal-3108929538-line-5)">options:</text><text class="terminal-3108929538-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-3108929538-line-5)"> +</text><text class="terminal-3108929538-r1" x="0" y="166.4" textLength="549" clip-path="url(#terminal-3108929538-line-6)">  -h, --help  show this help message and exit</text><text class="terminal-3108929538-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-3108929538-line-6)"> +</text><text class="terminal-3108929538-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-3108929538-line-7)"> </text> </g> </g> diff --git a/docs/images/cli_help/cz_init___help.svg b/docs/images/cli_help/cz_init___help.svg index 41a950ebdb..daf7d90cd7 100644 --- a/docs/images/cli_help/cz_init___help.svg +++ b/docs/images/cli_help/cz_init___help.svg @@ -19,46 +19,46 @@ font-weight: 700; } - .terminal-1838121835-matrix { + .terminal-2562163483-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1838121835-title { + .terminal-2562163483-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1838121835-r1 { fill: #c5c8c6 } -.terminal-1838121835-r2 { fill: #c5c8c6;font-weight: bold } + .terminal-2562163483-r1 { fill: #c5c8c6 } +.terminal-2562163483-r2 { fill: #c5c8c6;font-weight: bold } </style> <defs> - <clipPath id="terminal-1838121835-clip-terminal"> + <clipPath id="terminal-2562163483-clip-terminal"> <rect x="0" y="0" width="975.0" height="194.2" /> </clipPath> - <clipPath id="terminal-1838121835-line-0"> + <clipPath id="terminal-2562163483-line-0"> <rect x="0" y="1.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1838121835-line-1"> +<clipPath id="terminal-2562163483-line-1"> <rect x="0" y="25.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1838121835-line-2"> +<clipPath id="terminal-2562163483-line-2"> <rect x="0" y="50.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1838121835-line-3"> +<clipPath id="terminal-2562163483-line-3"> <rect x="0" y="74.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1838121835-line-4"> +<clipPath id="terminal-2562163483-line-4"> <rect x="0" y="99.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1838121835-line-5"> +<clipPath id="terminal-2562163483-line-5"> <rect x="0" y="123.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1838121835-line-6"> +<clipPath id="terminal-2562163483-line-6"> <rect x="0" y="147.9" width="976" height="24.65"/> </clipPath> </defs> @@ -70,17 +70,17 @@ <circle cx="44" cy="0" r="7" fill="#28c840"/> </g> - <g transform="translate(9, 41)" clip-path="url(#terminal-1838121835-clip-terminal)"> + <g transform="translate(9, 41)" clip-path="url(#terminal-2562163483-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)"> + <g class="terminal-2562163483-matrix"> + <text class="terminal-2562163483-r1" x="0" y="20" textLength="195.2" clip-path="url(#terminal-2562163483-line-0)">$ cz init --help</text><text class="terminal-2562163483-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-2562163483-line-0)"> +</text><text class="terminal-2562163483-r1" x="0" y="44.4" textLength="183" clip-path="url(#terminal-2562163483-line-1)">usage: cz init </text><text class="terminal-2562163483-r2" x="183" y="44.4" textLength="12.2" clip-path="url(#terminal-2562163483-line-1)">[</text><text class="terminal-2562163483-r1" x="195.2" y="44.4" textLength="24.4" clip-path="url(#terminal-2562163483-line-1)">-h</text><text class="terminal-2562163483-r2" x="219.6" y="44.4" textLength="12.2" clip-path="url(#terminal-2562163483-line-1)">]</text><text class="terminal-2562163483-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-2562163483-line-1)"> +</text><text class="terminal-2562163483-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-2562163483-line-2)"> +</text><text class="terminal-2562163483-r1" x="0" y="93.2" textLength="427" clip-path="url(#terminal-2562163483-line-3)">Initialize commitizen configuration</text><text class="terminal-2562163483-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-2562163483-line-3)"> +</text><text class="terminal-2562163483-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-2562163483-line-4)"> +</text><text class="terminal-2562163483-r1" x="0" y="142" textLength="97.6" clip-path="url(#terminal-2562163483-line-5)">options:</text><text class="terminal-2562163483-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-2562163483-line-5)"> +</text><text class="terminal-2562163483-r1" x="0" y="166.4" textLength="549" clip-path="url(#terminal-2562163483-line-6)">  -h, --help  show this help message and exit</text><text class="terminal-2562163483-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-2562163483-line-6)"> +</text><text class="terminal-2562163483-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-2562163483-line-7)"> </text> </g> </g> diff --git a/docs/images/cli_help/cz_ls___help.svg b/docs/images/cli_help/cz_ls___help.svg index 3ec3532ef1..27bd6b760a 100644 --- a/docs/images/cli_help/cz_ls___help.svg +++ b/docs/images/cli_help/cz_ls___help.svg @@ -19,46 +19,46 @@ font-weight: 700; } - .terminal-589791338-matrix { + .terminal-3771170172-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-589791338-title { + .terminal-3771170172-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-589791338-r1 { fill: #c5c8c6 } -.terminal-589791338-r2 { fill: #c5c8c6;font-weight: bold } + .terminal-3771170172-r1 { fill: #c5c8c6 } +.terminal-3771170172-r2 { fill: #c5c8c6;font-weight: bold } </style> <defs> - <clipPath id="terminal-589791338-clip-terminal"> + <clipPath id="terminal-3771170172-clip-terminal"> <rect x="0" y="0" width="975.0" height="194.2" /> </clipPath> - <clipPath id="terminal-589791338-line-0"> + <clipPath id="terminal-3771170172-line-0"> <rect x="0" y="1.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-589791338-line-1"> +<clipPath id="terminal-3771170172-line-1"> <rect x="0" y="25.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-589791338-line-2"> +<clipPath id="terminal-3771170172-line-2"> <rect x="0" y="50.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-589791338-line-3"> +<clipPath id="terminal-3771170172-line-3"> <rect x="0" y="74.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-589791338-line-4"> +<clipPath id="terminal-3771170172-line-4"> <rect x="0" y="99.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-589791338-line-5"> +<clipPath id="terminal-3771170172-line-5"> <rect x="0" y="123.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-589791338-line-6"> +<clipPath id="terminal-3771170172-line-6"> <rect x="0" y="147.9" width="976" height="24.65"/> </clipPath> </defs> @@ -70,17 +70,17 @@ <circle cx="44" cy="0" r="7" fill="#28c840"/> </g> - <g transform="translate(9, 41)" clip-path="url(#terminal-589791338-clip-terminal)"> + <g transform="translate(9, 41)" clip-path="url(#terminal-3771170172-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)"> + <g class="terminal-3771170172-matrix"> + <text class="terminal-3771170172-r1" x="0" y="20" textLength="170.8" clip-path="url(#terminal-3771170172-line-0)">$ cz ls --help</text><text class="terminal-3771170172-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-3771170172-line-0)"> +</text><text class="terminal-3771170172-r1" x="0" y="44.4" textLength="158.6" clip-path="url(#terminal-3771170172-line-1)">usage: cz ls </text><text class="terminal-3771170172-r2" x="158.6" y="44.4" textLength="12.2" clip-path="url(#terminal-3771170172-line-1)">[</text><text class="terminal-3771170172-r1" x="170.8" y="44.4" textLength="24.4" clip-path="url(#terminal-3771170172-line-1)">-h</text><text class="terminal-3771170172-r2" x="195.2" y="44.4" textLength="12.2" clip-path="url(#terminal-3771170172-line-1)">]</text><text class="terminal-3771170172-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-3771170172-line-1)"> +</text><text class="terminal-3771170172-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-3771170172-line-2)"> +</text><text class="terminal-3771170172-r1" x="0" y="93.2" textLength="317.2" clip-path="url(#terminal-3771170172-line-3)">Show available Commitizens</text><text class="terminal-3771170172-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-3771170172-line-3)"> +</text><text class="terminal-3771170172-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-3771170172-line-4)"> +</text><text class="terminal-3771170172-r1" x="0" y="142" textLength="97.6" clip-path="url(#terminal-3771170172-line-5)">options:</text><text class="terminal-3771170172-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-3771170172-line-5)"> +</text><text class="terminal-3771170172-r1" x="0" y="166.4" textLength="549" clip-path="url(#terminal-3771170172-line-6)">  -h, --help  show this help message and exit</text><text class="terminal-3771170172-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-3771170172-line-6)"> +</text><text class="terminal-3771170172-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-3771170172-line-7)"> </text> </g> </g> diff --git a/docs/images/cli_help/cz_schema___help.svg b/docs/images/cli_help/cz_schema___help.svg index afe8982e9a..3a0098b2db 100644 --- a/docs/images/cli_help/cz_schema___help.svg +++ b/docs/images/cli_help/cz_schema___help.svg @@ -19,46 +19,46 @@ font-weight: 700; } - .terminal-1497071669-matrix { + .terminal-721452391-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1497071669-title { + .terminal-721452391-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1497071669-r1 { fill: #c5c8c6 } -.terminal-1497071669-r2 { fill: #c5c8c6;font-weight: bold } + .terminal-721452391-r1 { fill: #c5c8c6 } +.terminal-721452391-r2 { fill: #c5c8c6;font-weight: bold } </style> <defs> - <clipPath id="terminal-1497071669-clip-terminal"> + <clipPath id="terminal-721452391-clip-terminal"> <rect x="0" y="0" width="975.0" height="194.2" /> </clipPath> - <clipPath id="terminal-1497071669-line-0"> + <clipPath id="terminal-721452391-line-0"> <rect x="0" y="1.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1497071669-line-1"> +<clipPath id="terminal-721452391-line-1"> <rect x="0" y="25.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1497071669-line-2"> +<clipPath id="terminal-721452391-line-2"> <rect x="0" y="50.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1497071669-line-3"> +<clipPath id="terminal-721452391-line-3"> <rect x="0" y="74.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1497071669-line-4"> +<clipPath id="terminal-721452391-line-4"> <rect x="0" y="99.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1497071669-line-5"> +<clipPath id="terminal-721452391-line-5"> <rect x="0" y="123.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-1497071669-line-6"> +<clipPath id="terminal-721452391-line-6"> <rect x="0" y="147.9" width="976" height="24.65"/> </clipPath> </defs> @@ -70,17 +70,17 @@ <circle cx="44" cy="0" r="7" fill="#28c840"/> </g> - <g transform="translate(9, 41)" clip-path="url(#terminal-1497071669-clip-terminal)"> + <g transform="translate(9, 41)" clip-path="url(#terminal-721452391-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)"> + <g class="terminal-721452391-matrix"> + <text class="terminal-721452391-r1" x="0" y="20" textLength="219.6" clip-path="url(#terminal-721452391-line-0)">$ cz schema --help</text><text class="terminal-721452391-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-721452391-line-0)"> +</text><text class="terminal-721452391-r1" x="0" y="44.4" textLength="207.4" clip-path="url(#terminal-721452391-line-1)">usage: cz schema </text><text class="terminal-721452391-r2" x="207.4" y="44.4" textLength="12.2" clip-path="url(#terminal-721452391-line-1)">[</text><text class="terminal-721452391-r1" x="219.6" y="44.4" textLength="24.4" clip-path="url(#terminal-721452391-line-1)">-h</text><text class="terminal-721452391-r2" x="244" y="44.4" textLength="12.2" clip-path="url(#terminal-721452391-line-1)">]</text><text class="terminal-721452391-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-721452391-line-1)"> +</text><text class="terminal-721452391-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-721452391-line-2)"> +</text><text class="terminal-721452391-r1" x="0" y="93.2" textLength="219.6" clip-path="url(#terminal-721452391-line-3)">Show commit schema</text><text class="terminal-721452391-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-721452391-line-3)"> +</text><text class="terminal-721452391-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-721452391-line-4)"> +</text><text class="terminal-721452391-r1" x="0" y="142" textLength="97.6" clip-path="url(#terminal-721452391-line-5)">options:</text><text class="terminal-721452391-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-721452391-line-5)"> +</text><text class="terminal-721452391-r1" x="0" y="166.4" textLength="549" clip-path="url(#terminal-721452391-line-6)">  -h, --help  show this help message and exit</text><text class="terminal-721452391-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-721452391-line-6)"> +</text><text class="terminal-721452391-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-721452391-line-7)"> </text> </g> </g> diff --git a/docs/images/cli_help/cz_version___help.svg b/docs/images/cli_help/cz_version___help.svg index c7777db4df..20331e0972 100644 --- a/docs/images/cli_help/cz_version___help.svg +++ b/docs/images/cli_help/cz_version___help.svg @@ -1,4 +1,4 @@ -<svg class="rich-terminal" viewBox="0 0 994 391.59999999999997" xmlns="http://www.w3.org/2000/svg"> +<svg class="rich-terminal" viewBox="0 0 994 538.0" xmlns="http://www.w3.org/2000/svg"> <!-- Generated with Rich https://www.textualize.io --> <style> @@ -19,92 +19,116 @@ font-weight: 700; } - .terminal-4023877003-matrix { + .terminal-1926777403-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4023877003-title { + .terminal-1926777403-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4023877003-r1 { fill: #c5c8c6 } -.terminal-4023877003-r2 { fill: #c5c8c6;font-weight: bold } + .terminal-1926777403-r1 { fill: #c5c8c6 } +.terminal-1926777403-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 id="terminal-1926777403-clip-terminal"> + <rect x="0" y="0" width="975.0" height="487.0" /> </clipPath> - <clipPath id="terminal-4023877003-line-0"> + <clipPath id="terminal-1926777403-line-0"> <rect x="0" y="1.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4023877003-line-1"> +<clipPath id="terminal-1926777403-line-1"> <rect x="0" y="25.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4023877003-line-2"> +<clipPath id="terminal-1926777403-line-2"> <rect x="0" y="50.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4023877003-line-3"> +<clipPath id="terminal-1926777403-line-3"> <rect x="0" y="74.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4023877003-line-4"> +<clipPath id="terminal-1926777403-line-4"> <rect x="0" y="99.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4023877003-line-5"> +<clipPath id="terminal-1926777403-line-5"> <rect x="0" y="123.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4023877003-line-6"> +<clipPath id="terminal-1926777403-line-6"> <rect x="0" y="147.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4023877003-line-7"> +<clipPath id="terminal-1926777403-line-7"> <rect x="0" y="172.3" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4023877003-line-8"> +<clipPath id="terminal-1926777403-line-8"> <rect x="0" y="196.7" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4023877003-line-9"> +<clipPath id="terminal-1926777403-line-9"> <rect x="0" y="221.1" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4023877003-line-10"> +<clipPath id="terminal-1926777403-line-10"> <rect x="0" y="245.5" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4023877003-line-11"> +<clipPath id="terminal-1926777403-line-11"> <rect x="0" y="269.9" width="976" height="24.65"/> </clipPath> -<clipPath id="terminal-4023877003-line-12"> +<clipPath id="terminal-1926777403-line-12"> <rect x="0" y="294.3" width="976" height="24.65"/> </clipPath> +<clipPath id="terminal-1926777403-line-13"> + <rect x="0" y="318.7" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1926777403-line-14"> + <rect x="0" y="343.1" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1926777403-line-15"> + <rect x="0" y="367.5" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1926777403-line-16"> + <rect x="0" y="391.9" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1926777403-line-17"> + <rect x="0" y="416.3" width="976" height="24.65"/> + </clipPath> +<clipPath id="terminal-1926777403-line-18"> + <rect x="0" y="440.7" 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"/> + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="536" 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 transform="translate(9, 41)" clip-path="url(#terminal-1926777403-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)"> + <g class="terminal-1926777403-matrix"> + <text class="terminal-1926777403-r1" x="0" y="20" textLength="231.8" clip-path="url(#terminal-1926777403-line-0)">$ cz version --help</text><text class="terminal-1926777403-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-1926777403-line-0)"> +</text><text class="terminal-1926777403-r1" x="0" y="44.4" textLength="219.6" clip-path="url(#terminal-1926777403-line-1)">usage: cz version </text><text class="terminal-1926777403-r2" x="219.6" y="44.4" textLength="12.2" clip-path="url(#terminal-1926777403-line-1)">[</text><text class="terminal-1926777403-r1" x="231.8" y="44.4" textLength="24.4" clip-path="url(#terminal-1926777403-line-1)">-h</text><text class="terminal-1926777403-r2" x="256.2" y="44.4" textLength="12.2" clip-path="url(#terminal-1926777403-line-1)">]</text><text class="terminal-1926777403-r2" x="280.6" y="44.4" textLength="12.2" clip-path="url(#terminal-1926777403-line-1)">[</text><text class="terminal-1926777403-r1" x="292.8" y="44.4" textLength="207.4" clip-path="url(#terminal-1926777403-line-1)">-r | -p | -c | -v</text><text class="terminal-1926777403-r2" x="500.2" y="44.4" textLength="12.2" clip-path="url(#terminal-1926777403-line-1)">]</text><text class="terminal-1926777403-r2" x="524.6" y="44.4" textLength="12.2" clip-path="url(#terminal-1926777403-line-1)">[</text><text class="terminal-1926777403-r1" x="536.8" y="44.4" textLength="305" clip-path="url(#terminal-1926777403-line-1)">--major | --minor | --tag</text><text class="terminal-1926777403-r2" x="841.8" y="44.4" textLength="12.2" clip-path="url(#terminal-1926777403-line-1)">]</text><text class="terminal-1926777403-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-1926777403-line-1)"> +</text><text class="terminal-1926777403-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-1926777403-line-2)"> +</text><text class="terminal-1926777403-r1" x="0" y="93.2" textLength="817.4" clip-path="url(#terminal-1926777403-line-3)">Get the version of the installed commitizen or the current project </text><text class="terminal-1926777403-r2" x="817.4" y="93.2" textLength="12.2" clip-path="url(#terminal-1926777403-line-3)">(</text><text class="terminal-1926777403-r1" x="829.6" y="93.2" textLength="97.6" clip-path="url(#terminal-1926777403-line-3)">default:</text><text class="terminal-1926777403-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-1926777403-line-3)"> +</text><text class="terminal-1926777403-r1" x="0" y="117.6" textLength="244" clip-path="url(#terminal-1926777403-line-4)">installed commitizen</text><text class="terminal-1926777403-r2" x="244" y="117.6" textLength="12.2" clip-path="url(#terminal-1926777403-line-4)">)</text><text class="terminal-1926777403-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-1926777403-line-4)"> +</text><text class="terminal-1926777403-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-1926777403-line-5)"> +</text><text class="terminal-1926777403-r1" x="0" y="166.4" textLength="97.6" clip-path="url(#terminal-1926777403-line-6)">options:</text><text class="terminal-1926777403-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-1926777403-line-6)"> +</text><text class="terminal-1926777403-r1" x="0" y="190.8" textLength="622.2" clip-path="url(#terminal-1926777403-line-7)">  -h, --help        show this help message and exit</text><text class="terminal-1926777403-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-1926777403-line-7)"> +</text><text class="terminal-1926777403-r1" x="0" y="215.2" textLength="841.8" clip-path="url(#terminal-1926777403-line-8)">  -r, --report      Output the system information for reporting bugs.</text><text class="terminal-1926777403-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-1926777403-line-8)"> +</text><text class="terminal-1926777403-r1" x="0" y="239.6" textLength="756.4" clip-path="url(#terminal-1926777403-line-9)">  -p, --project     Output the version of the current project.</text><text class="terminal-1926777403-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-1926777403-line-9)"> +</text><text class="terminal-1926777403-r1" x="0" y="264" textLength="817.4" clip-path="url(#terminal-1926777403-line-10)">  -c, --commitizen  Output the version of the installed commitizen.</text><text class="terminal-1926777403-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-1926777403-line-10)"> +</text><text class="terminal-1926777403-r1" x="0" y="288.4" textLength="915" clip-path="url(#terminal-1926777403-line-11)">  -v, --verbose     Output the version of both the installed commitizen and</text><text class="terminal-1926777403-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-1926777403-line-11)"> +</text><text class="terminal-1926777403-r1" x="0" y="312.8" textLength="488" clip-path="url(#terminal-1926777403-line-12)">                    the current project.</text><text class="terminal-1926777403-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-1926777403-line-12)"> +</text><text class="terminal-1926777403-r1" x="0" y="337.2" textLength="951.6" clip-path="url(#terminal-1926777403-line-13)">  --major           Output just the major version. Must be used with --project</text><text class="terminal-1926777403-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-1926777403-line-13)"> +</text><text class="terminal-1926777403-r1" x="0" y="361.6" textLength="402.6" clip-path="url(#terminal-1926777403-line-14)">                    or --verbose.</text><text class="terminal-1926777403-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-1926777403-line-14)"> +</text><text class="terminal-1926777403-r1" x="0" y="386" textLength="951.6" clip-path="url(#terminal-1926777403-line-15)">  --minor           Output just the minor version. Must be used with --project</text><text class="terminal-1926777403-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-1926777403-line-15)"> +</text><text class="terminal-1926777403-r1" x="0" y="410.4" textLength="402.6" clip-path="url(#terminal-1926777403-line-16)">                    or --verbose.</text><text class="terminal-1926777403-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-1926777403-line-16)"> +</text><text class="terminal-1926777403-r1" x="0" y="434.8" textLength="890.6" clip-path="url(#terminal-1926777403-line-17)">  --tag             get the version with tag prefix. Need to be used with</text><text class="terminal-1926777403-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-1926777403-line-17)"> +</text><text class="terminal-1926777403-r1" x="0" y="459.2" textLength="524.6" clip-path="url(#terminal-1926777403-line-18)">                    --project or --verbose.</text><text class="terminal-1926777403-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-1926777403-line-18)"> +</text><text class="terminal-1926777403-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-1926777403-line-19)"> </text> </g> </g> diff --git a/docs/images/cli_interactive/bump.gif b/docs/images/cli_interactive/bump.gif new file mode 100644 index 0000000000..aa3704be09 Binary files /dev/null and b/docs/images/cli_interactive/bump.gif differ diff --git a/docs/images/cli_interactive/commit.gif b/docs/images/cli_interactive/commit.gif new file mode 100644 index 0000000000..3726f93c73 Binary files /dev/null and b/docs/images/cli_interactive/commit.gif differ diff --git a/docs/images/cli_interactive/init.gif b/docs/images/cli_interactive/init.gif new file mode 100644 index 0000000000..eccd972a88 Binary files /dev/null and b/docs/images/cli_interactive/init.gif differ diff --git a/docs/images/cli_interactive/shortcut_custom.gif b/docs/images/cli_interactive/shortcut_custom.gif new file mode 100644 index 0000000000..ee65dea37a Binary files /dev/null and b/docs/images/cli_interactive/shortcut_custom.gif differ diff --git a/docs/images/cli_interactive/shortcut_default.gif b/docs/images/cli_interactive/shortcut_default.gif new file mode 100644 index 0000000000..45369686e9 Binary files /dev/null and b/docs/images/cli_interactive/shortcut_default.gif differ diff --git a/docs/images/commit.tape b/docs/images/commit.tape new file mode 100644 index 0000000000..f05a354e7b --- /dev/null +++ b/docs/images/commit.tape @@ -0,0 +1,120 @@ +Output cli_interactive/commit.gif + +Require cz + +# Use bash for cross-platform compatibility (macOS, Linux, Windows) +Set Shell bash + +Set FontSize 16 +Set Width 878 +Set Height 568 +Set Padding 20 +Set TypingSpeed 50ms + +Set Theme { + "name": "Commitizen", + "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", + "background": "#1e1e2e", + "foreground": "#afafaf", + "cursor": "#c7c7c7" +} + +# Hide initial shell prompt +Hide + +# Wait for terminal to be ready +Sleep 1s + +# Set a clean, simple prompt (while hidden) +Type "PS1='$ '" +Enter +Sleep 300ms + +# Create a clean temporary directory for recording +Type "rm -rf /tmp/commitizen-demo && mkdir -p /tmp/commitizen-demo && cd /tmp/commitizen-demo" +Enter +Sleep 500ms + +# Initialize git repository +Type "git init" +Enter +Type "git config user.email 'you@example.com'" +Enter +Type "git config user.name 'Your Name'" +Enter +Sleep 500ms + +Type "git checkout -b awesome-feature" +Enter +Sleep 500ms + +# Create a dummy file to commit +Type "echo 'test content' > example.py" +Enter +Sleep 300ms + +Type "git add example.py" +Enter +Sleep 300ms + +# Clear the screen to start fresh +Type "clear" +Enter +Sleep 500ms + +# Show commands from here +Show + +# Now run cz commit +Type "cz commit" +Sleep 500ms +Enter + +# Wait for first prompt to appear +Sleep 1s + +# Question 1: Select the type of change (move down to "feat") +Down +Sleep 500ms +Enter +Sleep 1s + +# Question 2: Scope (optional, skip) +Enter +Sleep 1s + +# Question 3: Subject +Type "awesome new feature" +Sleep 500ms +Enter +Sleep 1s + +# Question 4: Is this a BREAKING CHANGE? (No) +Enter +Sleep 1s + +# Question 5: Body (optional, skip) +Enter +Sleep 1s + +# Question 6: Footer (optional, skip) +Enter +Sleep 1s + +# Wait for commit success message +Sleep 2s diff --git a/docs/images/cz_logo.png b/docs/images/cz_logo.png new file mode 100644 index 0000000000..2f5f0244ec Binary files /dev/null and b/docs/images/cz_logo.png differ diff --git a/docs/images/demo.gif b/docs/images/demo.gif deleted file mode 100644 index 39dcdc9e91..0000000000 Binary files a/docs/images/demo.gif and /dev/null differ diff --git a/docs/images/init.gif b/docs/images/init.gif deleted file mode 100644 index f2cdf6b310..0000000000 Binary files a/docs/images/init.gif and /dev/null differ diff --git a/docs/images/init.tape b/docs/images/init.tape new file mode 100644 index 0000000000..cd115c6abb --- /dev/null +++ b/docs/images/init.tape @@ -0,0 +1,111 @@ +Output cli_interactive/init.gif + +Require cz + +# Use bash for cross-platform compatibility (macOS, Linux, Windows) +Set Shell bash + +Set FontSize 16 +Set Width 878 +Set Height 568 +Set Padding 20 +Set TypingSpeed 50ms + +Set Theme { + "name": "Commitizen", + "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", + "background": "#1e1e2e", + "foreground": "#afafaf", + "cursor": "#c7c7c7" +} + +# Hide initial shell prompt +Hide + +# Wait for terminal to be ready +Sleep 1s + +# Set a clean, simple prompt +Type "PS1='$ '" +Enter +Sleep 300ms + + +# Create a clean temporary directory for recording +Type "rm -rf /tmp/commitizen-example && mkdir -p /tmp/commitizen-example && cd /tmp/commitizen-example" +Enter +Sleep 500ms + +# Clear the screen to start fresh +Type "clear" +Enter +Sleep 500ms + +# Show commands from here +Show + +# Now run cz init in the clean environment +Type "cz init" +Sleep 500ms +Enter + +# Wait for welcome message and first prompt +Sleep 500ms +Sleep 1s +# Question 1: Please choose a supported config file +# Default is .cz.toml, just press Enter +Enter +Sleep 1s + +# Question 2: Please choose a cz (commit rule) +# Default is cz_conventional_commits, just press Enter +Enter +Sleep 1s + +# Question 3: Choose the source of the version +# Default is "commitizen: Fetch and set version in commitizen config, just press Enter" +Enter +Sleep 1s + +# Question 4: Choose version scheme +# Default is semver, just press Enter +Enter +Sleep 1s + +# Question 5: Please enter the correct version format +# Default is "$version", just press Enter +Enter +Sleep 1s + +# Question 6: Create changelog automatically on bump +# Default is Yes, just press Enter +Enter +Sleep 1s + +# Question 7: Keep major version zero (0.x) during breaking changes +# Default is Yes, just press Enter +Enter +Sleep 1s + +# Question 8: What types of pre-commit hook you want to install? +# Default is [commit-msg], just press Enter to accept +Enter +Sleep 1s + +# Wait for completion message +Sleep 3s diff --git a/docs/images/shortcut_custom.tape b/docs/images/shortcut_custom.tape new file mode 100644 index 0000000000..39f1c5838f --- /dev/null +++ b/docs/images/shortcut_custom.tape @@ -0,0 +1,159 @@ +Output cli_interactive/shortcut_custom.gif + +Require cz + +# Use bash for cross-platform compatibility (macOS, Linux, Windows) +Set Shell bash + +Set FontSize 16 +Set Width 878 +Set Height 568 +Set Padding 20 +Set TypingSpeed 50ms + +Set Theme { + "name": "Commitizen", + "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", + "background": "#1e1e2e", + "foreground": "#afafaf", + "cursor": "#c7c7c7" +} + +# Hide initial shell prompt +Hide + +# Wait for terminal to be ready +Sleep 1s + +# Set a clean, simple prompt (while hidden) +Type "PS1='$ '" +Enter +Sleep 300ms + +# Create a clean temporary directory for recording +Type "rm -rf /tmp/commitizen-example && mkdir -p /tmp/commitizen-example && cd /tmp/commitizen-example" +Enter +Sleep 500ms + +# Initialize git repository +Type "git init" +Enter +Type "git config user.email 'you@example.com'" +Enter +Type "git config user.name 'Your Name'" +Enter +Sleep 500ms + +Type "git checkout -b awesome-docs" +Enter +Sleep 500ms + +Type `cat > pyproject.toml << 'EOF'` +Enter +Type `[tool.commitizen]` +Enter +Type `name = "cz_customize"` +Enter +Type `use_shortcuts = true` +Enter +Type `` +Enter +Type `[tool.commitizen.customize]` +Enter +Type `message_template = "{{prefix}}: {{message}}"` +Enter +Type `schema = "<type>: <body>"` +Enter +Type `schema_pattern = "(feat|fix|docs|test):(\\s.*)"` +Enter +Type `` +Enter +Type `[[tool.commitizen.customize.questions]]` +Enter +Type `type = "list"` +Enter +Type `name = "prefix"` +Enter +Type `message = "Select the type of change you are committing"` +Enter +Type `choices = [` +Enter +Type ` { value = "feat", name = "feat: A new feature.", key = "f" },` +Enter +Type ` { value = "fix", name = "fix: A bug fix.", key = "x" },` +Enter +Type ` { value = "docs", name = "docs: Documentation only changes", key = "d" },` +Enter +Type ` { value = "test", name = "test: Adding or correcting tests", key = "t" },` +Enter +Type `]` +Enter +Type `` +Enter +Type `[[tool.commitizen.customize.questions]]` +Enter +Type `type = "input"` +Enter +Type `name = "message"` +Enter +Type `message = "Commit body: "` +Enter +Type "EOF" +Enter +Sleep 300ms + +# Create a dummy file to commit +Type "echo 'test content' > README.md" +Enter +Sleep 300ms + +Type "git add README.md" +Enter +Sleep 300ms + +# Clear the screen to start fresh +Type "clear" +Enter +Sleep 500ms + +# Show commands from here +Show + +# Now run cz commit +Type "cz commit" +Sleep 500ms +Enter + +# Wait for first prompt to appear +Sleep 2s + +# Question 1: Select the type of change (press d to "docs") +Sleep 1s +Type "d" +Sleep 2s +Enter +Sleep 1s + +# Question 2: Commit body +Type "demo with custom keys" +Sleep 500ms +Enter +Sleep 500ms + +# Wait for commit success message +Sleep 1s diff --git a/docs/images/shortcut_default.tape b/docs/images/shortcut_default.tape new file mode 100644 index 0000000000..204a18a473 --- /dev/null +++ b/docs/images/shortcut_default.tape @@ -0,0 +1,138 @@ +Output cli_interactive/shortcut_default.gif + +Require cz + +# Use bash for cross-platform compatibility (macOS, Linux, Windows) +Set Shell bash + +Set FontSize 16 +Set Width 878 +Set Height 568 +Set Padding 20 +Set TypingSpeed 50ms + +Set Theme { + "name": "Commitizen", + "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", + "background": "#1e1e2e", + "foreground": "#afafaf", + "cursor": "#c7c7c7" +} + +# Hide initial shell prompt +Hide + +# Wait for terminal to be ready +Sleep 1s + +# Set a clean, simple prompt (while hidden) +Type "PS1='$ '" +Enter +Sleep 300ms + +# Create a clean temporary directory for recording +Type "rm -rf /tmp/commitizen-example && mkdir -p /tmp/commitizen-example && cd /tmp/commitizen-example" +Enter +Sleep 500ms + +# Initialize git repository +Type "git init" +Enter +Type "git config user.email 'you@example.com'" +Enter +Type "git config user.name 'Your Name'" +Enter +Sleep 500ms + +Type "git checkout -b awesome-docs" +Enter +Sleep 500ms + +# Initialize commitizen config with shortcuts enabled +Type `cat > pyproject.toml << 'EOF'` +Enter +Sleep 100ms +Type `[tool.commitizen]` +Enter +Sleep 100ms +Type `name = "cz_conventional_commits"` +Enter +Sleep 100ms +Type `use_shortcuts = true` +Enter +Sleep 100ms +Type "EOF" +Enter +Sleep 300ms + +# Create a dummy file to commit +Type "echo 'test content' > README.md" +Enter +Sleep 300ms + +Type "git add README.md" +Enter +Sleep 300ms + +# Clear the screen to start fresh +Type "clear" +Enter +Sleep 500ms + +# Show commands from here +Show + +# Now run cz commit +Type "cz commit" +Sleep 500ms +Enter + +# Wait for first prompt to appear +Sleep 2s + +# Question 1: Select the type of change (press d to "docs") +Sleep 1s +Type "d" +Sleep 2s +Enter +Sleep 1s + +# Question 2: Scope (optional, skip) +Enter +Sleep 1s + +# Question 3: Subject +Type "demo with custom keys" +Sleep 500ms +Enter +Sleep 500ms + +# Question 4: Is this a BREAKING CHANGE? (No) +Enter +Sleep 500ms + +# Question 5: Body (optional, skip) +Enter +Sleep 500ms + +# Question 6: Footer (optional, skip) +Enter +Sleep 500ms + +# Wait for commit success message +Sleep 1s diff --git a/docs/third-party-commitizen.md b/docs/third-party-commitizen.md deleted file mode 100644 index dbbd879434..0000000000 --- a/docs/third-party-commitizen.md +++ /dev/null @@ -1,86 +0,0 @@ -## 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 - -### [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 - -```bash -pip install cz-conventional-gitmoji -``` - -### Usage - -```bash -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. - -It can be installed with `pip install commitizen-emoji`. - -Usage: `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 - -`cz_legacy` can be installed with `pip install cz_legacy` - -See the [README][1] for instructions on configuration - - [1]: https://pypi.org/project/cz_legacy diff --git a/docs/third-party-plugins/about.md b/docs/third-party-plugins/about.md new file mode 100644 index 0000000000..9711386fc8 --- /dev/null +++ b/docs/third-party-plugins/about.md @@ -0,0 +1,73 @@ +# What are third-party plugins? + +Third-party plugins are a way to extend Commitizen with additional customized features. + +These plugins are available as PyPI packages (installable with `pip`). + +!!! note + New plugins are welcome! Once you published your plugins, please send us a PR to update this page. + +!!! note "Historical notes" + This section was originally called "Third-Party Commitizen Templates", but has been renamed to "Third-Party Commitizen Plugins" to better reflect the content. + +## Plugin features + +<!-- TODO: provide more details about the features --> +<!-- We can move the details from customization pages to here --> + +### Commit message convention + +Includes the rules to validate and generate commit messages. + +### Version scheme + +Includes the rules to generate version numbers. + +### Version provider + +Read and write version from data sources. + +### Changelog format + +Generate changelog in customized formats. + +<!-- TODO: complete the following section --> +<!-- ## How do I create a new plugin? --> + +## How to help us update the list of plugins? + +Please document what features the plugin provides: + +- a convention +- a scheme +- a provider +- a `changelog_format` + +Of course, a plugin can provide multiple features. +You may have noticed that `commitizen` itself can be viewed as a plugin that provides all the above features. + +Please see [cz-path](./cz-path.md) for a detailed example. + +## New plugin documentation template + + # [Package name](https://github.com/author/package-name) + + <!-- Description of the plugin. --> + + <!-- What features does the plugin provide? --> + + ## Installation + + ```sh + pip install package-name + ``` + + ## Usage + + ```sh + cz --name package-name commit + ``` + + ## Example + + <!-- Example usage of the plugin. --> diff --git a/docs/third-party-plugins/commitizen-deno-provider.md b/docs/third-party-plugins/commitizen-deno-provider.md new file mode 100644 index 0000000000..77f7d2e177 --- /dev/null +++ b/docs/third-party-plugins/commitizen-deno-provider.md @@ -0,0 +1,28 @@ +# [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. + +<!-- TODO: What features does the plugin provide? --> + +## 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/third-party-plugins/commitizen-emoji.md b/docs/third-party-plugins/commitizen-emoji.md new file mode 100644 index 0000000000..e8e2654516 --- /dev/null +++ b/docs/third-party-plugins/commitizen-emoji.md @@ -0,0 +1,17 @@ +# [Commitizen emoji](https://pypi.org/project/commitizen-emoji/) (Unmaintained) + +Just like *conventional commit* format, but with emojis and optionally time spent and related tasks. + +<!-- TODO: What features does the plugin provide? --> + +## Installation + +```sh +pip install commitizen-emoji +``` + +## Usage + +```sh +cz --name cz_commitizen_emoji commit +``` diff --git a/docs/third-party-plugins/conventional-jira.md b/docs/third-party-plugins/conventional-jira.md new file mode 100644 index 0000000000..99505a2743 --- /dev/null +++ b/docs/third-party-plugins/conventional-jira.md @@ -0,0 +1,13 @@ +# [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. + +<!-- TODO: What features does the plugin provide? --> + +## Installation + +```sh +pip install conventional-JIRA +``` diff --git a/docs/third-party-plugins/cz-ai.md b/docs/third-party-plugins/cz-ai.md new file mode 100644 index 0000000000..e9ced907bf --- /dev/null +++ b/docs/third-party-plugins/cz-ai.md @@ -0,0 +1,17 @@ +# [cz-ai](https://github.com/watadarkstar/cz_ai) + +A Commitizen plugin that leverages OpenAI's GPT-4o to automatically generate clear, concise, and conventional commit messages based on your staged git changes. + +<!-- TODO: What features does the plugin provide? --> + +## Installation + +```sh +pip install cz-ai +``` + +## Usage + +```sh +cz --name cz_ai commit +``` diff --git a/docs/third-party-plugins/cz-conventional-gitmoji.md b/docs/third-party-plugins/cz-conventional-gitmoji.md new file mode 100644 index 0000000000..f4157105e7 --- /dev/null +++ b/docs/third-party-plugins/cz-conventional-gitmoji.md @@ -0,0 +1,19 @@ +# [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. + +<!-- TODO: What features does the plugin provide? --> + +## Installation + +```sh +pip install cz-conventional-gitmoji +``` + +## Usage + +```sh +cz --name cz_gitmoji commit +``` diff --git a/docs/third-party-plugins/cz-emoji.md b/docs/third-party-plugins/cz-emoji.md new file mode 100644 index 0000000000..c638f4bfa1 --- /dev/null +++ b/docs/third-party-plugins/cz-emoji.md @@ -0,0 +1,17 @@ +# [cz-emoji](https://github.com/adam-grant-hendry/cz-emoji) + +_conventional commit_ format, but with emojis. + +<!-- TODO: What features does the plugin provide? --> + +## Installation + +```sh +pip install cz-emoji +``` + +## Usage + +```sh +cz --name cz_emoji commit +``` diff --git a/docs/third-party-plugins/cz-legacy.md b/docs/third-party-plugins/cz-legacy.md new file mode 100644 index 0000000000..4f0a2e1221 --- /dev/null +++ b/docs/third-party-plugins/cz-legacy.md @@ -0,0 +1,19 @@ +# [Conventional Legacy (cz_legacy)][cz_legacy] + +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. + +<!-- TODO: What features does the plugin provide? --> + +## Installation + +```sh +pip install cz_legacy +``` + +## Usage + +See the [Conventional Legacy README][cz_legacy] for instructions on configuration. + +[cz_legacy]: https://pypi.org/project/cz_legacy diff --git a/docs/third-party-plugins/cz-path.md b/docs/third-party-plugins/cz-path.md new file mode 100644 index 0000000000..7a2ba59176 --- /dev/null +++ b/docs/third-party-plugins/cz-path.md @@ -0,0 +1,54 @@ +# [cz-path](https://pypi.org/project/cz-path/) + +Provides prefix choices for commit messages based on staged files (Git only). +For example, if the staged files are `component/z/a.ts` and `component/z/b.ts`, +the path prefix option will be `component/z` and commit message might look like: +`component/z/: description of changes`. If only one file is staged, the extension +is removed in the prefix. + +This plugins provides a commitizen convention for commit messages. + +## Installation + +```sh +pip install cz-path +``` + +## Usage + +Add `cz-path` to your configuration file. + +Example for `.cz.json`: + +```json +{ + "commitizen": { + "name": "cz_path", + "remove_path_prefixes": ["src", "module_name"] + } +} +``` + +The default value for `remove_path_prefixes` is `["src"]`. Adding `/` to the +prefixes is not required. + +## Example session + +```plain + $ git add .vscode/ + $ cz -n cz_path c +? Prefix: (Use arrow keys) + » .vscode + .vscode/ + project + (empty) +? Prefix: .vscode +? Commit title: adjust settings + +.vscode: adjust settings + +[main 0000000] .vscode: adjust settings + 2 files changed, 1 insertion(+), 11 deletions(-) + +Commit successful! +``` diff --git a/docs/third-party-plugins/github-jira-conventional.md b/docs/third-party-plugins/github-jira-conventional.md new file mode 100644 index 0000000000..6032b8e7ff --- /dev/null +++ b/docs/third-party-plugins/github-jira-conventional.md @@ -0,0 +1,17 @@ +# [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 `CHANGELOG.md` +- creating links to JIRA issues in `CHANGELOG.md` + +<!-- TODO: What features does the plugin provide? --> + +## Installation + +```sh +pip install cz-github-jira-conventional +``` + +For installation instructions (configuration and pre-commit), please visit the [GitHub repository](https://github.com/apheris/cz-github-jira-conventional). diff --git a/docs/tutorials/auto_check.md b/docs/tutorials/auto_check.md index ede8759e68..dc2b6a90c2 100644 --- a/docs/tutorials/auto_check.md +++ b/docs/tutorials/auto_check.md @@ -2,46 +2,71 @@ ## 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). +To automatically check a commit message before committing, use a [Git hook](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks). This ensures all commit messages match your project's commitizen format before they are accepted into the repository. + +When a commit message fails validation, Git rejects the commit and displays an error explaining what went wrong. Update the message to the required format before trying again. ## How to -There are two common methods for installing the hook: +There are two common methods for installing the hooks: + +### Method 1: Using pre-commit hook frameworks (Recommended) + +Using pre-commit hook frameworks is the recommended approach because hook installation, updates, and execution are handled automatically. +Two common frameworks are: + +1. [prek](https://prek.j178.dev) (faster) +2. [pre-commit](https://pre-commit.com/) + + +In the steps below, we'll use `prek`. -### Method 1: Add git hook through [pre-commit](https://pre-commit.com/) -- Step 1: Install [pre-commit](https://pre-commit.com/) +!!! tip "Using pre-commit framework" + If you use pre-commit instead of prek, you can run the same commands. Simply replace prek with pre-commit in the steps below. + + + +#### Step 1: Install prek ```sh -python -m pip install pre-commit +python -m pip install prek ``` -- Step 2: Create `.pre-commit-config.yaml` at your root directory with the following content +#### Step 2: Create `.pre-commit-config.yaml` + +Create a `.pre-commit-config.yaml` file in your project root directory with the following content: ```yaml --- repos: - repo: https://github.com/commitizen-tools/commitizen - rev: v1.17.0 + rev: v4.10.0 # Use the latest version or a specific version hooks: - id: commitizen + stages: [commit-msg] ``` -- Step 3: Install the configuration into git hook through `pre-commit` +!!! tip "Using the latest version" + Replace `v4.10.0` with the latest commitizen version. You can find the latest version on [GitHub releases](https://github.com/commitizen-tools/commitizen/releases) or use a specific commit SHA for pinning to an exact version. + +#### Step 3: Install the hook + +Install the configuration into Git's hook system: ```bash -pre-commit install --hook-type commit-msg +prek install --hook-type commit-msg ``` -### Method 2: Manually add git hook +The hook is now active! Every time you create a commit, commitizen will automatically validate your commit message. -The command might be included inside of a Git hook (inside of `.git/hooks/` at the root of the project). +### Method 2: Manual Git hook installation -The selected hook might be the file called commit-msg. +If you prefer not to use a pre-commit framework, you can manually create a Git hook. This gives you full control over the hook script but requires manual maintenance. -This example shows how to use the check command inside of commit-msg. +#### Step 1: Create the commit-msg hook -At the root of the project: +Navigate to your project's `.git/hooks` directory and create the `commit-msg` hook file: ```bash cd .git/hooks @@ -49,19 +74,89 @@ touch commit-msg chmod +x commit-msg ``` -Open the file and edit it: +#### Step 2: Add the commitizen check command -```sh +Open the `commit-msg` file in your editor and add the following content: + +```bash #!/bin/bash -MSG_FILE=$1 -cz check --allow-abort --commit-msg-file $MSG_FILE +cz check --allow-abort --commit-msg-file $1 +``` + +Where: + +- `$1` is the temporary file path that Git provides, containing the current commit message +- `--allow-abort` allows empty commit messages (which typically instruct Git to abort a commit) +- `--commit-msg-file` tells commitizen to read the commit message from the specified file + +The hook is now active! Each time you create a commit, this hook will automatically validate your commit message. + +## Testing the hook + +After installing the hook, you can test it by attempting to commit with an invalid message: + +```bash +# This should fail with an invalid message +git commit -m "invalid commit message" + +# This should succeed with a valid message +git commit -m "feat: add new feature" ``` -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. +If the hook is working correctly, invalid commit messages are rejected with an error explaining what's wrong. + +## What happens when validation fails? + +When a commit message fails validation: + +1. Git will abort the commit +2. An error message will be displayed showing: + - Which commit failed (if checking multiple commits) + - The invalid commit message + - The expected pattern/format +3. Your changes remain staged, so you can simply amend the commit message and try again + +Example error output: + +<!-- DEPENDENCY: InvalidCommitMessageError --> + +``` +commit validation: failed! +please enter a commit message in the commitizen format. +commit "abc123": "invalid message" +pattern: ^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?: .{1,} +``` + +## Troubleshooting + +### Hook not running + +- **Verify the hook file exists**: Check that `.git/hooks/commit-msg` exists and is executable +- **Check file permissions**: Ensure the hook has execute permissions (`chmod +x .git/hooks/commit-msg`) +- **Verify commitizen is installed**: Run `cz --version` to confirm commitizen is available in your PATH +- **Check Git version**: Ensure you're using a recent version of Git that supports hooks + +### Prek hook not working + +- **Verify installation**: Run `prek --version` to confirm pre-commit is installed +- **Reinstall the hook**: Try running `prek install --hook-type commit-msg` again +- **Check configuration**: Verify your `.pre-commit-config.yaml` file is valid YAML and in the project root +- **Update hooks**: Run `prek autoupdate` to update to the latest versions + +### Bypassing the hook (when needed) + +If you need to bypass the hook temporarily (e.g., for merge commits or special cases), you can use: + +```bash +git commit --no-verify -m "your message" +``` -The `--commit-msg-file` flag is required, not optional. +!!! warning "Use with caution" + Only bypass hooks when absolutely necessary, as it defeats the purpose of automated validation. -Each time you create a commit, automatically, this hook will analyze it. -If the commit message is invalid, it'll be rejected. +## Additional resources -The commit should follow the given committing rules; otherwise, it won't be accepted. +- Learn more about [Git hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) +- See the [check command documentation](../commands/check.md) for more validation options +- Check out [prek documentation](https://prek.j178.dev/) for advanced hook management +- Check out [pre-commit documentation](https://pre-commit.com/) for advanced hook management diff --git a/docs/tutorials/auto_prepare_commit_message.md b/docs/tutorials/auto_prepare_commit_message.md index 3011142679..84ac62b689 100644 --- a/docs/tutorials/auto_prepare_commit_message.md +++ b/docs/tutorials/auto_prepare_commit_message.md @@ -2,7 +2,7 @@ ## About -It can be desirable to use commitizen for all types of commits (i.e. regular, merge, +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`. @@ -18,14 +18,14 @@ To automatically perform arbitrary cleanup steps after a successful commit you c > 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 +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. +Commitizen and use the generated commit message for the commit. ## Installation -Copy the hooks from [here](https://github.com/commitizen-tools/hooks) into the `.git/hooks` folder and make them +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 diff --git a/docs/tutorials/dev_releases.md b/docs/tutorials/dev_releases.md index 8142334754..e2b29fb191 100644 --- a/docs/tutorials/dev_releases.md +++ b/docs/tutorials/dev_releases.md @@ -5,9 +5,7 @@ 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. +If multiple active branches attempt to create a tag relative to the main branch, there is a possibility that they 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: @@ -19,9 +17,7 @@ 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"_. + while developmental releases help avoid the situation described above, they can be _"difficult to parse for human readers"_ depending on the value passed as the developmental release. ## How to @@ -64,8 +60,7 @@ 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: +This approach could potentially create a collision if two builds occur at precisely the same second, but it may be sufficient for many use cases: ```sh --devrelease $(date +%s) diff --git a/docs/tutorials/github_actions.md b/docs/tutorials/github_actions.md index 7a98abe2be..c3b06336a9 100644 --- a/docs/tutorials/github_actions.md +++ b/docs/tutorials/github_actions.md @@ -1,26 +1,49 @@ -## Create a new release with Github Actions +## Create a new release with GitHub Actions -### Automatic bumping of version +This guide shows you how to automatically bump versions, create changelogs, and publish releases using Commitizen in GitHub Actions. -To execute `cz bump` in your CI, and push the new commit and -the new tag, back to your master branch, we have to: +### Prerequisites -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. +Before setting up the workflow, you'll need: -!!! 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. +1. A personal access token with repository write permissions +2. Commitizen configured in your project (see [configuration documentation](../config/configuration_file.md)) -```yaml +### Automatic version bumping + +To automatically execute `cz bump` in your CI and push the new commit and tag back to your repository, follow these steps: + +#### Step 1: Create a personal access token + +1. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) +2. Click "Generate new token (classic)" +3. Give it a descriptive name (e.g., "Commitizen CI") +4. Select the `repo` scope to grant full repository access +5. Click "Generate token" and **copy the token immediately** (you won't be able to see it again) + +!!! warning "Important: Use Personal Access Token, not GITHUB_TOKEN" + If you use `GITHUB_TOKEN` instead of `PERSONAL_ACCESS_TOKEN`, the workflow won't trigger another workflow run. This is a GitHub security feature to prevent infinite loops. The `GITHUB_TOKEN` is treated like using `[skip ci]` in other CI systems. + +#### Step 2: Add the token as a repository secret + +1. Go to your repository on GitHub +2. Navigate to `Settings > Secrets and variables > Actions` +3. Click "New repository secret" +4. Name it `PERSONAL_ACCESS_TOKEN` +5. Paste the token you copied in Step 1 +6. Click "Add secret" + +#### Step 3: Create the workflow file + +Create a new file `.github/workflows/bumpversion.yml` in your repository with the following content: + +```yaml title=".github/workflows/bumpversion.yml" name: Bump version on: push: branches: - - master + - master # or 'main' if that's your default branch jobs: bump-version: @@ -29,7 +52,7 @@ jobs: name: "Bump version and create changelog with commitizen" steps: - name: Check out - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" fetch-depth: 0 @@ -39,62 +62,106 @@ jobs: github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} ``` -Push to master and that's it. +#### How it works -### Creating a github release +- **Trigger**: The workflow runs on every push to the `master` branch (or `main` if you change it) +- **Conditional check**: The `if` condition prevents infinite loops by skipping the job if the commit message starts with `bump:` +- **Checkout**: Uses your personal access token to check out the repository with full history (`fetch-depth: 0`) +- **Bump**: The `commitizen-action` automatically: + - Determines the version increment based on your commit messages + - Updates version files (as configured in your `pyproject.toml` or other config) + - Creates a new git tag + - Generates/updates the changelog + - Pushes the commit and tag back to the repository -You can modify the previous action. +Once you push this workflow file to your repository, it will automatically run on the next push to your default branch. -Add the variable `changelog_increment_filename` in the `commitizen-action`, specifying -where to output the content of the changelog for the newly created version. +Check out [commitizen-action](https://github.com/commitizen-tools/commitizen-action) for more details. -And then add a step using a github action to create the release: `softprops/action-gh-release` +### Creating a GitHub release -The commitizen action creates an env variable called `REVISION`, containing the -newely created version. +To automatically create a GitHub release when a new version is bumped, you can extend the workflow above. -```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 }} +The `commitizen-action` creates an environment variable called `REVISION` containing the newly created version. You can use this to create a release with the changelog content. + +```yaml title=".github/workflows/bumpversion.yml" +name: Bump version + +on: + push: + branches: + - master # or 'main' if that's your default branch + +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@v6 + 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 }} + changelog_increment_filename: body.md + - name: Release + uses: ncipollo/release-action@v1 + with: + tag: v${{ env.REVISION }} + bodyFile: "body.md" + skipIfReleaseExists: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` -### Publishing a python package +You can find the complete workflow in our repository at [bumpversion.yml](https://github.com/commitizen-tools/commitizen/blob/master/.github/workflows/bumpversion.yml). -Once the new tag is created, triggering an automatic publish command would be desired. +### Publishing a Python package -In order to do so, the credential needs to be added with the information of our PyPI account. +After a new version tag is created by the bump workflow, you can automatically publish your package to PyPI. -Instead of using username and password, we suggest using [api token](https://pypi.org/help/#apitoken) generated from PyPI. +#### Step 1: Create a PyPI API token -After generate api token, use the token as the PyPI password and `__token__` as the username. +1. Go to [PyPI Account Settings](https://pypi.org/manage/account/) +2. Scroll to the "API tokens" section +3. Click "Add API token" +4. Give it a name (e.g., "GitHub Actions") +5. Set the scope (project-specific or account-wide) +6. Click "Add token" and **copy the token immediately** -Go to `Settings > Secrets > Add new secret` and add the secret: `PYPI_PASSWORD`. +!!! tip "Using trusted publishing (recommended)" + Instead of API tokens, consider using [PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/) with OpenID Connect (OIDC). This is more secure as it doesn't require storing secrets. The `pypa/gh-action-pypi-publish` action supports trusted publishing when you configure it in your PyPI project settings. -Create a file in `.github/workflows/pythonpublish.yaml` with the following content: +#### Step 2: Add the token as a repository secret -```yaml +1. Go to your repository on GitHub +2. Navigate to `Settings > Secrets and variables > Actions` +3. Click "New repository secret" +4. Name it `PYPI_PASSWORD` +5. Paste the PyPI token +6. Click "Add secret" + +#### Step 3: Create the publish workflow + +Create a new file `.github/workflows/pythonpublish.yml` that triggers on tag pushes: + +```yaml title=".github/workflows/pythonpublish.yml" name: Upload Python Package on: push: tags: - - "*" # Will trigger for every tag, alternative: 'v*' + - "*" # Will trigger for every tag, alternative: 'v*' jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python @@ -113,13 +180,12 @@ jobs: poetry install - name: Build and publish env: - PYPI_USERNAME: __token__ - PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - ./scripts/publish + POETRY_HTTP_BASIC_PYPI_USERNAME: __token__ + POETRY_HTTP_BASIC_PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: poetry publish --build ``` -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. +This workflow uses Poetry to build and publish the package. You can find the complete workflow in our repository at [pythonpublish.yml](https://github.com/commitizen-tools/commitizen/blob/master/.github/workflows/pythonpublish.yml). -Push the changes and that's it. +!!! note "Alternative publishing methods" + You can also use [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) or other build tools like `setuptools`, `flit`, or `hatchling` to publish your package. diff --git a/docs/tutorials/gitlab_ci.md b/docs/tutorials/gitlab_ci.md index de1336b675..0af05c4e7b 100644 --- a/docs/tutorials/gitlab_ci.md +++ b/docs/tutorials/gitlab_ci.md @@ -12,9 +12,9 @@ _Goal_: Bump a new version every time that a change occurs on the `master` branc 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 +### 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. +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: @@ -38,7 +38,7 @@ The latest step is to create a `deploy key.` To do this, we should create it und 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. +Tip: If the CI raise some errors, try to unprotect the private key. ### Defining GitLab CI Pipeline @@ -74,12 +74,12 @@ test: auto-bump: stage: auto-bump - image: python:3.8 + image: python:3.10 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 + - pip3 install -U commitizen # install commitizen - mkdir -p ~/.ssh - chmod 700 ~/.ssh - echo "$SSH_PUBLIC_KEY" >> ~/.ssh/id_rsa.pub @@ -105,9 +105,9 @@ auto-bump: - 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. +So, every time that a developer pushes to any branch, the `test` job is executed. If the branch is `master` and the test jobs succeed, 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: +After merging the new changes 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 index fb87820c4c..2b9ad173d3 100644 --- a/docs/tutorials/jenkins_pipeline.md +++ b/docs/tutorials/jenkins_pipeline.md @@ -47,7 +47,7 @@ def useCz(String authorName = 'Jenkins CI Server', String authorEmail = 'your-je ``` !!! warning - Using jenkins pipeline with any git plugin may require many different configurations, + Using jenkins pipeline with any git plugin may require many 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 0000000000..f863a9e531 --- /dev/null +++ b/docs/tutorials/monorepo_guidance.md @@ -0,0 +1,87 @@ +# Configuring Commitizen in a monorepo + +This tutorial assumes that your monorepo is structured with multiple components that can be released independently of each other. +It also assumes that you are using conventional commits with scopes. + +Here is a step-by-step example using two libraries, `library-b` and `library-z`: + +1. **Organize your monorepo** + + For example, you might have one of these layouts: + + ```shell-session + . + ├── library-b + │   └── .cz.toml + └── library-z + └── .cz.toml + ``` + + ```shell-session + src + ├── library-b + │   └── .cz.toml + └── library-z + └── .cz.toml + ``` + +2. **Add a Commitizen configuration 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 + ``` + +3. **Bump each component independently** + + ```sh + cz --config library-b/.cz.toml bump --yes + cz --config library-z/.cz.toml bump --yes + ``` + + +## Changelog per component + +To filter the correct commits for each component, you'll need to define a strategy. + +For example: + +- Trigger the pipeline based on the changed path. This can have some downsides, as you'll rely on the developer not including files from unrelated components. + - [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 commits by a specific pattern in the commit message (recommended) + + +### Example with scope in conventional commits + +In this example, we want `library-b`'s changelog to only include commits that use the `library-b` scope. +To achieve this, we configure Commitizen to match only commit messages with that scope. + +Here is an example configuration for `library-b`: + +```toml +[tool.commitizen.customize] +changelog_pattern = "^(feat|fix)\\(library-b\\)(!)?:" # the type pattern can be a wildcard or any types you wish to include +``` + +With this configuration, a commit message like the following would be included in `library-b`'s changelog: + +```text +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 0000000000..2d439e38ca --- /dev/null +++ b/docs/tutorials/tag_format.md @@ -0,0 +1,100 @@ +# 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, 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 [`tag_format`](../commands/bump.md#-tag-format) and [`version_scheme`](../commands/bump.md#-version-scheme) settings for more details. + +## 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's 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 lose your version history, you'll still be able to generate your changelog properly, +and on the next version bump, your last version in the form `v${version}` will be properly recognized 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, or 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/bump.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 detects an unknown tag, you can do so 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 index 9ba151cc37..b8aa7f30f0 100644 --- a/docs/tutorials/writing_commits.md +++ b/docs/tutorials/writing_commits.md @@ -1,46 +1,160 @@ -For this project to work well in your pipeline, a commit convention must be followed. +# Commit Message Best Practices -By default commitizen uses the known [conventional commits][conventional_commits], but -you can create your own following the docs information over at -[customization][customization]. +## About -## Conventional commits +For Commitizen to work effectively in your development pipeline, commits must follow a consistent convention. By default, Commitizen uses the [Conventional Commits][conventional_commits] specification, which provides a standardized format for commit messages that enables automatic versioning and changelog generation. -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. +You can also create your own custom commit convention by following the [customization documentation][customization]. -## Writing commits +## Conventional Commits Format -Now to the important part, when writing commits, it's important to think about: +The Conventional Commits specification follows this structure: -- Your future self -- Your colleagues +``` +<type>[optional scope]: <description> -You may think this is trivial, but it's not. It's important for the reader to -understand what happened. +[optional body] -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]. +[optional footer] +``` -### Recommendations +### Commit Types -- **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. +Commit types categorize the nature of your changes. The most important types for semantic versioning are: -| Do's | Don'ts | -| ---- | ------ | +- **`feat`**: Introduces a new feature (correlates with **MINOR** version increment) +- **`fix`**: Patches a bug (correlates with **PATCH** version increment) + +Other commonly used types include: + +- **`docs`**: Documentation only changes +- **`style`**: Code style changes (formatting, missing semicolons, etc.) +- **`refactor`**: Code refactoring without changing functionality +- **`perf`**: Performance improvements +- **`test`**: Adding or updating tests +- **`build`**: Changes to build system or dependencies +- **`ci`**: Changes to CI configuration files +- **`chore`**: Other changes that don't modify source or test files + +!!! note + While `feat` and `fix` directly affect semantic versioning, other types (like `build`, `chore`, `docs`) typically don't trigger version bumps unless they include a `BREAKING CHANGE`. + +### Breaking Changes + +Breaking changes trigger a **MAJOR** version increment. You can indicate breaking changes in two ways: + +1. **In the commit body or footer**: + ``` + feat(api): change authentication method + + BREAKING CHANGE: The authentication API now requires OAuth2 instead of API keys. + ``` + +2. **In the commit title** (when enabled): + ``` + feat!: change authentication method + ``` + + To enable this syntax, set `breaking_change_exclamation_in_title = true` in your configuration. [Read more][breaking-change-config] + +### Scope + +An optional scope can be added to provide additional context about the area of the codebase affected: + +``` +feat(parser): add support for JSON arrays +fix(api): handle null response gracefully +``` + +## Writing Effective Commit Messages + +Well-written commit messages are crucial for maintaining a clear project history. When writing commits, consider: + +- **Your future self**: You'll need to understand these changes months later +- **Your team**: Clear messages help colleagues understand the codebase evolution +- **Automated tools**: Good messages enable better changelog generation and versioning + +### Best Practices + +#### 1. Keep the Subject Line Concise + +The subject line should be clear and concise (aim for ~50 characters). It should summarize what the commit does in one line. + +**Good:** +``` +fix(commands): handle missing user input gracefully +feat(api): add pagination support +``` + +**Avoid:** +``` +fix: stuff +feat: commit command introduced +``` + +#### 2. Use Imperative Mood + +Write commit messages in the imperative mood, as if completing the sentence: "If applied, this commit will..." + +**Good:** +``` +feat: add user authentication +fix: resolve memory leak in parser +``` + +**Avoid:** +``` +feat: added user authentication +fix: resolved memory leak in parser +``` + +#### 3. Think About the Changelog + +Your commits will likely appear in the automatically generated changelog. Write messages that make sense in that context. If you want to exclude a commit from the changelog, use types like `build`, `chore`, or `ci`. + +#### 4. One Feature Per Commit + +Keep commits focused on a single change. If you introduce multiple related changes, consider squashing them into a single commit. This makes the history cleaner and improves changelog generation. + +#### 5. Use the Body for Context + +For complex changes, use the commit body to explain: + +- **Why** the change was made +- **What** was changed (if not obvious from the subject) +- **How** it differs from previous behavior + +``` +feat(api): add rate limiting + +Implement rate limiting to prevent API abuse. The system now +enforces a maximum of 100 requests per minute per IP address. +Exceeding this limit returns a 429 status code. +``` + +### Examples + +| ✅ Good Examples | ❌ Poor Examples | +| ---------------- | ---------------- | | `fix(commands): bump error when no user provided` | `fix: stuff` | -| `feat: add new commit command` | `feat: commit command introduced` | +| `feat(api): add pagination to user list endpoint` | `feat: commit command introduced` | +| `docs: update installation instructions` | `docs: changes` | +| `refactor(parser): simplify token extraction logic` | `refactor: code cleanup` | + +## Character Encoding + +Commitizen supports Unicode characters (including emojis) in commit messages. This is useful if you're using commit message formats that include emojis, such as [cz-emoji][cz_emoji]. + +By default, Commitizen uses `utf-8` encoding. You can configure a different encoding through the `encoding` [configuration option][configuration]. + +## Related Documentation + +- [Conventional Commits Specification][conventional_commits] +- [Custom Commit Conventions][customization] +- [Commit Configuration Options](../config/commit.md) -[customization]: ../customization.md +[customization]: ../customization/config_file.md [conventional_commits]: https://www.conventionalcommits.org -[cz_emoji]: https://commitizen-tools.github.io/commitizen/third-party-commitizen/#cz-emoji -[configuration]: ../config.md#encoding +[cz_emoji]: ../third-party-plugins/cz-emoji.md +[configuration]: ../config/commit.md#encoding +[breaking-change-config]: ../config/commit.md#breaking_change_exclamation_in_title diff --git a/hooks/post-commit.py b/hooks/post-commit.py index 6444e5ca84..d242b6b354 100755 --- a/hooks/post-commit.py +++ b/hooks/post-commit.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -from pathlib import Path try: from commitizen.cz.utils import get_backup_file_path @@ -8,13 +7,14 @@ exit(1) -def post_commit(): - backup_file = Path(get_backup_file_path()) +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 backup_file_path.is_file(): + backup_file_path.unlink() if __name__ == "__main__": - exit(post_commit()) + post_commit() + exit(0) diff --git a/hooks/prepare-commit-msg.py b/hooks/prepare-commit-msg.py index d1ccf169cf..017ecc28ea 100755 --- a/hooks/prepare-commit-msg.py +++ b/hooks/prepare-commit-msg.py @@ -3,7 +3,6 @@ import subprocess import sys from pathlib import Path -from subprocess import CalledProcessError try: from commitizen.cz.utils import get_backup_file_path @@ -13,51 +12,51 @@ exit(1) -def prepare_commit_msg(commit_msg_file: Path) -> int: +def prepare_commit_msg(commit_msg_file: str) -> int: # check if the commit message needs to be generated using commitizen - if ( - subprocess.run( - [ - "cz", - "check", - "--commit-msg-file", - commit_msg_file, - ], - capture_output=True, - ).returncode - != 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) + exit_code = subprocess.run( + [ + "cz", + "check", + "--commit-msg-file", + commit_msg_file, + ], + capture_output=True, + ).returncode + if exit_code == 0: + return 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 + exit_code = subprocess.run( + [ + "cz", + "commit", + "--dry-run", + "--write-message-to-file", + commit_msg_file, + ], + stdin=sys.stdin, + stdout=sys.stdout, + ).returncode + if exit_code: + return exit_code + + # 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])) + exit_code = prepare_commit_msg(sys.argv[1]) + exit(exit_code) diff --git a/lychee.toml b/lychee.toml new file mode 100644 index 0000000000..f434f0727f --- /dev/null +++ b/lychee.toml @@ -0,0 +1,6 @@ +exclude_path = ["./tests"] + +exclude = [ + "https://conventionalcommits.org/", # Sometimes it's benign network error + "https://blog.devgenius.io/continuous-delivery-made-easy-in-python-c085e9c82e69" # Membership only article, sometimes it's 403 forbidden +] diff --git a/mkdocs.yml b/mkdocs.yml index a0fb57fde2..21d6376c50 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,10 +1,14 @@ site_name: Commitizen +site_url: https://commitizen-tools.github.io/commitizen/ site_description: commit rules, semantic version, conventional commits theme: - name: "material" + name: material + logo: images/cz_logo.png + favicon: images/cz_logo.png + features: + - content.code.copy palette: - - primary: 'deep purple' # Palette toggle for automatic mode - media: "(prefers-color-scheme)" toggle: @@ -14,6 +18,7 @@ theme: # Palette toggle for light mode - media: "(prefers-color-scheme: light)" scheme: default + primary: deep purple toggle: icon: material/brightness-7 name: Switch to dark mode @@ -21,6 +26,7 @@ theme: # Palette toggle for dark mode - media: "(prefers-color-scheme: dark)" scheme: slate + primary: deep purple toggle: icon: material/brightness-4 name: Switch to system preference @@ -31,32 +37,59 @@ 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" + - 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: + - Configuration File: "config/configuration_file.md" + - Version Provider: "config/version_provider.md" + - bump: "config/bump.md" + - commit: "config/commit.md" + - check: "config/check.md" + - changelog: "config/changelog.md" + - Misc Options: "config/option.md" + - Advanced Customization: + - Customize via config file: "customization/config_file.md" + - Customized Python Class: "customization/python_class.md" + - Changelog Template: "customization/changelog_template.md" - Tutorials: - - Writing commits: "tutorials/writing_commits.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" + - Commit Message Best Practices: "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" + - "features_wont_add.md" - Exit Codes: "exit_codes.md" - - Third-Party Commitizen Templates: "third-party-commitizen.md" - - Contributing: "contributing.md" + - Third-Party Commitizen Plugins: + - About: "third-party-plugins/about.md" + # Please sort the plugins alphabetically + - "third-party-plugins/commitizen-deno-provider.md" + - "third-party-plugins/commitizen-emoji.md" + - "third-party-plugins/conventional-jira.md" + - "third-party-plugins/cz-ai.md" + - "third-party-plugins/cz-conventional-gitmoji.md" + - "third-party-plugins/cz-emoji.md" + - "third-party-plugins/cz-legacy.md" + - "third-party-plugins/cz-path.md" + - "third-party-plugins/github-jira-conventional.md" + - Contributing: + - "contributing/contributing_tldr.md" + - "contributing/contributing.md" + - "contributing/pull_request.md" + - "history.md" - Resources: "external_links.md" markdown_extensions: @@ -74,3 +107,9 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + +plugins: + - search + - git-revision-date-localized diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 04763e736e..0000000000 --- a/poetry.lock +++ /dev/null @@ -1,1788 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. - -[[package]] -name = "appnope" -version = "0.1.4" -description = "Disable App Nap on macOS >= 10.9" -optional = false -python-versions = ">=3.6" -files = [ - {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, - {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, -] - -[[package]] -name = "argcomplete" -version = "3.4.0" -description = "Bash tab completion for argparse" -optional = false -python-versions = ">=3.8" -files = [ - {file = "argcomplete-3.4.0-py3-none-any.whl", hash = "sha256:69a79e083a716173e5532e0fa3bef45f793f4e61096cf52b5a42c0211c8b8aa5"}, - {file = "argcomplete-3.4.0.tar.gz", hash = "sha256:c2abcdfe1be8ace47ba777d4fce319eb13bf8ad9dace8d085dcad6eded88057f"}, -] - -[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 = "*" -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.15.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -files = [ - {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, - {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, -] - -[package.dependencies] -pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} - -[package.extras] -dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] - -[[package]] -name = "backcall" -version = "0.2.0" -description = "Specifications for callback functions passed in to an API" -optional = false -python-versions = "*" -files = [ - {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, - {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, -] - -[[package]] -name = "certifi" -version = "2024.7.4" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, -] - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -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 = "charset-normalizer" -version = "3.3.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, -] - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -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" -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.5.1" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"}, - {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"}, - {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"}, - {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"}, - {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"}, - {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"}, - {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"}, - {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"}, - {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"}, - {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"}, - {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"}, - {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"}, - {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"}, - {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"}, - {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"}, - {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"}, - {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"}, - {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"}, - {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"}, - {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"}, - {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"}, - {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"}, - {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"}, - {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"}, - {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"}, - {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"}, - {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"}, - {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"}, - {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"}, - {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"}, - {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"}, - {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"}, - {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"}, - {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"}, - {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"}, - {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"}, - {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"}, - {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"}, - {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"}, - {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"}, - {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"}, - {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"}, - {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"}, - {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"}, - {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"}, - {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"}, -] - -[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" -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" -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.14" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, - {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, -] - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] - -[[package]] -name = "distlib" -version = "0.3.8" -description = "Distribution utilities" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.2.1" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, - {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "execnet" -version = "2.1.1" -description = "execnet: rapid multi-Python deployment" -optional = false -python-versions = ">=3.8" -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.0.1" -description = "Get the currently executing AST node of a frame, and other information" -optional = false -python-versions = ">=3.5" -files = [ - {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, - {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, -] - -[package.extras] -tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] - -[[package]] -name = "filelock" -version = "3.14.0" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.8" -files = [ - {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, - {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] -typing = ["typing-extensions (>=4.8)"] - -[[package]] -name = "freezegun" -version = "1.5.1" -description = "Let your Python tests travel through time" -optional = false -python-versions = ">=3.7" -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 = "*" -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.5.36" -description = "File identification library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, - {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.7" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, -] - -[[package]] -name = "importlib-metadata" -version = "8.0.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, - {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -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.12.3" -description = "IPython: Productive Interactive Computing" -optional = false -python-versions = ">=3.8" -files = [ - {file = "ipython-8.12.3-py3-none-any.whl", hash = "sha256:b0340d46a933d27c657b211a329d0be23793c36595acf9e6ef4164bc01a1804c"}, - {file = "ipython-8.12.3.tar.gz", hash = "sha256:3910c4b54543c2ad73d06579aa771041b7d5707b033bd488669b4cf544e3b363"}, -] - -[package.dependencies] -appnope = {version = "*", markers = "sys_platform == \"darwin\""} -backcall = "*" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -decorator = "*" -jedi = ">=0.16" -matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -pickleshare = "*" -prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<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", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] -black = ["black"] -doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "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 = ["pytest (<7.1)", "pytest-asyncio", "testpath"] -test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] - -[[package]] -name = "jedi" -version = "0.19.1" -description = "An autocompletion tool for Python that can be used for text editors." -optional = false -python-versions = ">=3.6" -files = [ - {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, - {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, -] - -[package.dependencies] -parso = ">=0.8.3,<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 (<7.0.0)"] - -[[package]] -name = "jinja2" -version = "3.1.4" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "markdown" -version = "3.6" -description = "Python implementation of John Gruber's Markdown." -optional = false -python-versions = ">=3.8" -files = [ - {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, - {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, -] - -[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" -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 = "2.1.5" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, -] - -[[package]] -name = "matplotlib-inline" -version = "0.1.7" -description = "Inline Matplotlib backend for Jupyter" -optional = false -python-versions = ">=3.8" -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" -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" -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.0" -description = "Project documentation with Markdown." -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocs-1.6.0-py3-none-any.whl", hash = "sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7"}, - {file = "mkdocs-1.6.0.tar.gz", hash = "sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512"}, -] - -[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" -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.28" -description = "Documentation that simply works" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mkdocs_material-9.5.28-py3-none-any.whl", hash = "sha256:ff48b11b2a9f705dd210409ec3b418ab443dd36d96915bcba45a41f10ea27bfd"}, - {file = "mkdocs_material-9.5.28.tar.gz", hash = "sha256:9cba305283ad1600e3d0a67abe72d7a058b54793b47be39930911a588fe0336b"}, -] - -[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,<2.0)", "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" -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.10.1" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, - {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, - {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, - {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, - {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, - {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, - {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, - {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, - {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, - {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, - {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, - {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, - {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, - {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, - {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, - {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, - {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, - {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, - {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, - {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, - {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, - {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, - {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, - {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, - {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, - {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, - {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, -] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.1.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -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" -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.8.0" -description = "Node.js virtual environment builder" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "packaging" -version = "24.1" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, -] - -[[package]] -name = "paginate" -version = "0.5.6" -description = "Divides large result sets into pages for easier browsing" -optional = false -python-versions = "*" -files = [ - {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, -] - -[[package]] -name = "parso" -version = "0.8.4" -description = "A Python Parser" -optional = false -python-versions = ">=3.6" -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" -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 = "*" -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 = "pickleshare" -version = "0.7.5" -description = "Tiny 'shelve'-like database with concurrency support" -optional = false -python-versions = "*" -files = [ - {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, - {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, -] - -[[package]] -name = "platformdirs" -version = "4.2.2" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -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 = "3.5.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, - {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, -] - -[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.36" -description = "Library for building powerful interactive command lines in Python" -optional = false -python-versions = ">=3.6.2" -files = [ - {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, - {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, -] - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -optional = false -python-versions = "*" -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.2" -description = "Safely evaluate AST nodes without side effects" -optional = false -python-versions = "*" -files = [ - {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, - {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, -] - -[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" -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.8.1" -description = "Extension pack for Python Markdown." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pymdown_extensions-10.8.1-py3-none-any.whl", hash = "sha256:f938326115884f48c6059c67377c46cf631c733ef3629b6eed1349989d1b30cb"}, - {file = "pymdown_extensions-10.8.1.tar.gz", hash = "sha256:3ab1db5c9e21728dabf75192d71471f8e50f216627e9a1fa9535ecb0231b9940"}, -] - -[package.dependencies] -markdown = ">=3.6" -pyyaml = "*" - -[package.extras] -extra = ["pygments (>=2.12)"] - -[[package]] -name = "pytest" -version = "8.2.2" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, - {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2.0" -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 = "5.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", 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" -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.8" -description = "Pytest plugin providing a fixture interface for spulec/freezegun" -optional = false -python-versions = ">= 3.6" -files = [ - {file = "pytest_freezer-0.4.8-py3-none-any.whl", hash = "sha256:644ce7ddb8ba52b92a1df0a80a699bad2b93514c55cf92e9f2517b68ebe74814"}, - {file = "pytest_freezer-0.4.8.tar.gz", hash = "sha256:8ee2f724b3ff3540523fa355958a22e6f4c1c819928b78a7a183ae4248ce6ee6"}, -] - -[package.dependencies] -freezegun = ">=1.0" -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" -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.5.0" -description = "Easy to use fixtures to write regression tests." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-regressions-2.5.0.tar.gz", hash = "sha256:818c7884c1cff3babf89eebb02cbc27b307856b1985427c24d52cb812a106fd9"}, - {file = "pytest_regressions-2.5.0-py3-none-any.whl", hash = "sha256:8c4e5c4037325fdb0825bc1fdcb75e17e03adf3407049f0cb704bb996d496255"}, -] - -[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", "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" -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" -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 = "pytz" -version = "2024.1" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[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" -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.0.1" -description = "Python library to build pretty command line user prompts ⭐️" -optional = false -python-versions = ">=3.8" -files = [ - {file = "questionary-2.0.1-py3-none-any.whl", hash = "sha256:8ab9a01d0b91b68444dff7f6652c1e754105533f083cbe27597c8110ecc230a2"}, - {file = "questionary-2.0.1.tar.gz", hash = "sha256:bcce898bf3dbb446ff62830c86c5c6fb9a22a54146f0f5597d3da43b10d8fc8b"}, -] - -[package.dependencies] -prompt_toolkit = ">=2.0,<=3.0.36" - -[[package]] -name = "regex" -version = "2024.5.15" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.8" -files = [ - {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f"}, - {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6"}, - {file = "regex-2024.5.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0bd000c6e266927cb7a1bc39d55be95c4b4f65c5be53e659537537e019232b1"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eaa7ddaf517aa095fa8da0b5015c44d03da83f5bd49c87961e3c997daed0de7"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba68168daedb2c0bab7fd7e00ced5ba90aebf91024dea3c88ad5063c2a562cca"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e8d717bca3a6e2064fc3a08df5cbe366369f4b052dcd21b7416e6d71620dca1"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1337b7dbef9b2f71121cdbf1e97e40de33ff114801263b275aafd75303bd62b5"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9ebd0a36102fcad2f03696e8af4ae682793a5d30b46c647eaf280d6cfb32796"}, - {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9efa1a32ad3a3ea112224897cdaeb6aa00381627f567179c0314f7b65d354c62"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1595f2d10dff3d805e054ebdc41c124753631b6a471b976963c7b28543cf13b0"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b802512f3e1f480f41ab5f2cfc0e2f761f08a1f41092d6718868082fc0d27143"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a0981022dccabca811e8171f913de05720590c915b033b7e601f35ce4ea7019f"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:19068a6a79cf99a19ccefa44610491e9ca02c2be3305c7760d3831d38a467a6f"}, - {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1b5269484f6126eee5e687785e83c6b60aad7663dafe842b34691157e5083e53"}, - {file = "regex-2024.5.15-cp310-cp310-win32.whl", hash = "sha256:ada150c5adfa8fbcbf321c30c751dc67d2f12f15bd183ffe4ec7cde351d945b3"}, - {file = "regex-2024.5.15-cp310-cp310-win_amd64.whl", hash = "sha256:ac394ff680fc46b97487941f5e6ae49a9f30ea41c6c6804832063f14b2a5a145"}, - {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f5b1dff3ad008dccf18e652283f5e5339d70bf8ba7c98bf848ac33db10f7bc7a"}, - {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c6a2b494a76983df8e3d3feea9b9ffdd558b247e60b92f877f93a1ff43d26656"}, - {file = "regex-2024.5.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a32b96f15c8ab2e7d27655969a23895eb799de3665fa94349f3b2fbfd547236f"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10002e86e6068d9e1c91eae8295ef690f02f913c57db120b58fdd35a6bb1af35"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec54d5afa89c19c6dd8541a133be51ee1017a38b412b1321ccb8d6ddbeb4cf7d"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10e4ce0dca9ae7a66e6089bb29355d4432caed736acae36fef0fdd7879f0b0cb"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e507ff1e74373c4d3038195fdd2af30d297b4f0950eeda6f515ae3d84a1770f"}, - {file = "regex-2024.5.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1f059a4d795e646e1c37665b9d06062c62d0e8cc3c511fe01315973a6542e40"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0721931ad5fe0dda45d07f9820b90b2148ccdd8e45bb9e9b42a146cb4f695649"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:833616ddc75ad595dee848ad984d067f2f31be645d603e4d158bba656bbf516c"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:287eb7f54fc81546346207c533ad3c2c51a8d61075127d7f6d79aaf96cdee890"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:19dfb1c504781a136a80ecd1fff9f16dddf5bb43cec6871778c8a907a085bb3d"}, - {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:119af6e56dce35e8dfb5222573b50c89e5508d94d55713c75126b753f834de68"}, - {file = "regex-2024.5.15-cp311-cp311-win32.whl", hash = "sha256:1c1c174d6ec38d6c8a7504087358ce9213d4332f6293a94fbf5249992ba54efa"}, - {file = "regex-2024.5.15-cp311-cp311-win_amd64.whl", hash = "sha256:9e717956dcfd656f5055cc70996ee2cc82ac5149517fc8e1b60261b907740201"}, - {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:632b01153e5248c134007209b5c6348a544ce96c46005d8456de1d552455b014"}, - {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e64198f6b856d48192bf921421fdd8ad8eb35e179086e99e99f711957ffedd6e"}, - {file = "regex-2024.5.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68811ab14087b2f6e0fc0c2bae9ad689ea3584cad6917fc57be6a48bbd012c49"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ec0c2fea1e886a19c3bee0cd19d862b3aa75dcdfb42ebe8ed30708df64687a"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0c0c0003c10f54a591d220997dd27d953cd9ccc1a7294b40a4be5312be8797b"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2431b9e263af1953c55abbd3e2efca67ca80a3de8a0437cb58e2421f8184717a"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a605586358893b483976cffc1723fb0f83e526e8f14c6e6614e75919d9862cf"}, - {file = "regex-2024.5.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391d7f7f1e409d192dba8bcd42d3e4cf9e598f3979cdaed6ab11288da88cb9f2"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9ff11639a8d98969c863d4617595eb5425fd12f7c5ef6621a4b74b71ed8726d5"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4eee78a04e6c67e8391edd4dad3279828dd66ac4b79570ec998e2155d2e59fd5"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8fe45aa3f4aa57faabbc9cb46a93363edd6197cbc43523daea044e9ff2fea83e"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d0a3d8d6acf0c78a1fff0e210d224b821081330b8524e3e2bc5a68ef6ab5803d"}, - {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c486b4106066d502495b3025a0a7251bf37ea9540433940a23419461ab9f2a80"}, - {file = "regex-2024.5.15-cp312-cp312-win32.whl", hash = "sha256:c49e15eac7c149f3670b3e27f1f28a2c1ddeccd3a2812cba953e01be2ab9b5fe"}, - {file = "regex-2024.5.15-cp312-cp312-win_amd64.whl", hash = "sha256:673b5a6da4557b975c6c90198588181029c60793835ce02f497ea817ff647cb2"}, - {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:87e2a9c29e672fc65523fb47a90d429b70ef72b901b4e4b1bd42387caf0d6835"}, - {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3bea0ba8b73b71b37ac833a7f3fd53825924165da6a924aec78c13032f20850"}, - {file = "regex-2024.5.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfc4f82cabe54f1e7f206fd3d30fda143f84a63fe7d64a81558d6e5f2e5aaba9"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5bb9425fe881d578aeca0b2b4b3d314ec88738706f66f219c194d67179337cb"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64c65783e96e563103d641760664125e91bd85d8e49566ee560ded4da0d3e704"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf2430df4148b08fb4324b848672514b1385ae3807651f3567871f130a728cc3"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5397de3219a8b08ae9540c48f602996aa6b0b65d5a61683e233af8605c42b0f2"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:455705d34b4154a80ead722f4f185b04c4237e8e8e33f265cd0798d0e44825fa"}, - {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2b6f1b3bb6f640c1a92be3bbfbcb18657b125b99ecf141fb3310b5282c7d4ed"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3ad070b823ca5890cab606c940522d05d3d22395d432f4aaaf9d5b1653e47ced"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5b5467acbfc153847d5adb21e21e29847bcb5870e65c94c9206d20eb4e99a384"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e6662686aeb633ad65be2a42b4cb00178b3fbf7b91878f9446075c404ada552f"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2b4c884767504c0e2401babe8b5b7aea9148680d2e157fa28f01529d1f7fcf67"}, - {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3cd7874d57f13bf70078f1ff02b8b0aa48d5b9ed25fc48547516c6aba36f5741"}, - {file = "regex-2024.5.15-cp38-cp38-win32.whl", hash = "sha256:e4682f5ba31f475d58884045c1a97a860a007d44938c4c0895f41d64481edbc9"}, - {file = "regex-2024.5.15-cp38-cp38-win_amd64.whl", hash = "sha256:d99ceffa25ac45d150e30bd9ed14ec6039f2aad0ffa6bb87a5936f5782fc1569"}, - {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13cdaf31bed30a1e1c2453ef6015aa0983e1366fad2667657dbcac7b02f67133"}, - {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cac27dcaa821ca271855a32188aa61d12decb6fe45ffe3e722401fe61e323cd1"}, - {file = "regex-2024.5.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7dbe2467273b875ea2de38ded4eba86cbcbc9a1a6d0aa11dcf7bd2e67859c435"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f18a9a3513a99c4bef0e3efd4c4a5b11228b48aa80743be822b71e132ae4f5"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d347a741ea871c2e278fde6c48f85136c96b8659b632fb57a7d1ce1872547600"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1878b8301ed011704aea4c806a3cadbd76f84dece1ec09cc9e4dc934cfa5d4da"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4babf07ad476aaf7830d77000874d7611704a7fcf68c9c2ad151f5d94ae4bfc4"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cb514e137cb3488bce23352af3e12fb0dbedd1ee6e60da053c69fb1b29cc6c"}, - {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdd09d47c0b2efee9378679f8510ee6955d329424c659ab3c5e3a6edea696294"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:72d7a99cd6b8f958e85fc6ca5b37c4303294954eac1376535b03c2a43eb72629"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a094801d379ab20c2135529948cb84d417a2169b9bdceda2a36f5f10977ebc16"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c0c18345010870e58238790a6779a1219b4d97bd2e77e1140e8ee5d14df071aa"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:16093f563098448ff6b1fa68170e4acbef94e6b6a4e25e10eae8598bb1694b5d"}, - {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e38a7d4e8f633a33b4c7350fbd8bad3b70bf81439ac67ac38916c4a86b465456"}, - {file = "regex-2024.5.15-cp39-cp39-win32.whl", hash = "sha256:71a455a3c584a88f654b64feccc1e25876066c4f5ef26cd6dd711308aa538694"}, - {file = "regex-2024.5.15-cp39-cp39-win_amd64.whl", hash = "sha256:cab12877a9bdafde5500206d1020a584355a97884dfd388af3699e9137bf7388"}, - {file = "regex-2024.5.15.tar.gz", hash = "sha256:d3ee02d9e5f482cc8309134a91eeaacbdd2261ba111b0fef3748eeb4913e6a2c"}, -] - -[[package]] -name = "requests" -version = "2.32.1" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -files = [ - {file = "requests-2.32.1-py3-none-any.whl", hash = "sha256:21ac9465cdf8c1650fe1ecde8a71669a93d4e6f147550483a2967d08396a56a5"}, - {file = "requests-2.32.1.tar.gz", hash = "sha256:eb97e87e64c79e64e5b8ac75cee9dd1f97f49e289b083ee6be96268930725685"}, -] - -[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.7.1" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, -] - -[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.9\""} - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "ruff" -version = "0.5.0" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.5.0-py3-none-linux_armv6l.whl", hash = "sha256:ee770ea8ab38918f34e7560a597cc0a8c9a193aaa01bfbd879ef43cb06bd9c4c"}, - {file = "ruff-0.5.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38f3b8327b3cb43474559d435f5fa65dacf723351c159ed0dc567f7ab735d1b6"}, - {file = "ruff-0.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7594f8df5404a5c5c8f64b8311169879f6cf42142da644c7e0ba3c3f14130370"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:adc7012d6ec85032bc4e9065110df205752d64010bed5f958d25dbee9ce35de3"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d505fb93b0fabef974b168d9b27c3960714d2ecda24b6ffa6a87ac432905ea38"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dc5cfd3558f14513ed0d5b70ce531e28ea81a8a3b1b07f0f48421a3d9e7d80a"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:db3ca35265de239a1176d56a464b51557fce41095c37d6c406e658cf80bbb362"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1a321c4f68809fddd9b282fab6a8d8db796b270fff44722589a8b946925a2a8"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c4dfcd8d34b143916994b3876b63d53f56724c03f8c1a33a253b7b1e6bf2a7d"}, - {file = "ruff-0.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81e5facfc9f4a674c6a78c64d38becfbd5e4f739c31fcd9ce44c849f1fad9e4c"}, - {file = "ruff-0.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e589e27971c2a3efff3fadafb16e5aef7ff93250f0134ec4b52052b673cf988d"}, - {file = "ruff-0.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2ffbc3715a52b037bcb0f6ff524a9367f642cdc5817944f6af5479bbb2eb50e"}, - {file = "ruff-0.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cd096e23c6a4f9c819525a437fa0a99d1c67a1b6bb30948d46f33afbc53596cf"}, - {file = "ruff-0.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:46e193b36f2255729ad34a49c9a997d506e58f08555366b2108783b3064a0e1e"}, - {file = "ruff-0.5.0-py3-none-win32.whl", hash = "sha256:49141d267100f5ceff541b4e06552e98527870eafa1acc9dec9139c9ec5af64c"}, - {file = "ruff-0.5.0-py3-none-win_amd64.whl", hash = "sha256:e9118f60091047444c1b90952736ee7b1792910cab56e9b9a9ac20af94cd0440"}, - {file = "ruff-0.5.0-py3-none-win_arm64.whl", hash = "sha256:ed5c4df5c1fb4518abcb57725b576659542bdbe93366f4f329e8f398c4b71178"}, - {file = "ruff-0.5.0.tar.gz", hash = "sha256:eb641b5873492cf9bd45bc9c5ae5320648218e04386a5f0c264ad6ccce8226a1"}, -] - -[[package]] -name = "setuptools" -version = "70.0.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"}, - {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] - -[[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.*" -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 = "*" -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.4.0" -description = "ANSI color formatting for output in terminal" -optional = false -python-versions = ">=3.8" -files = [ - {file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"}, - {file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"}, -] - -[package.extras] -tests = ["pytest", "pytest-cov"] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "tomlkit" -version = "0.12.5" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, - {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, -] - -[[package]] -name = "traitlets" -version = "5.14.3" -description = "Traitlets Python configuration system" -optional = false -python-versions = ">=3.8" -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.9.20240311" -description = "Typing stubs for Deprecated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-Deprecated-1.2.9.20240311.tar.gz", hash = "sha256:0680e89989a8142707de8103f15d182445a533c1047fd9b7e8c5459101e9b90a"}, - {file = "types_Deprecated-1.2.9.20240311-py3-none-any.whl", hash = "sha256:d7793aaf32ff8f7e49a8ac781de4872248e0694c4b75a7a8a186c51167463f9d"}, -] - -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20240316" -description = "Typing stubs for python-dateutil" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, - {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, -] - -[[package]] -name = "types-pyyaml" -version = "6.0.12.20240311" -description = "Typing stubs for PyYAML" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-PyYAML-6.0.12.20240311.tar.gz", hash = "sha256:a9e0f0f88dc835739b0c1ca51ee90d04ca2a897a71af79de9aec5f38cb0a5342"}, - {file = "types_PyYAML-6.0.12.20240311-py3-none-any.whl", hash = "sha256:b845b06a1c7e54b8e5b4c683043de0d9caf205e7434b3edc678ff2411979b8f6"}, -] - -[[package]] -name = "types-termcolor" -version = "0.1.1" -description = "Typing stubs for termcolor" -optional = false -python-versions = "*" -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" -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"}, -] - -[[package]] -name = "urllib3" -version = "2.2.2" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.8" -files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, -] - -[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.26.2" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.7" -files = [ - {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, - {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, -] - -[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 = "4.0.0" -description = "Filesystem events monitoring" -optional = false -python-versions = ">=3.8" -files = [ - {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b"}, - {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b"}, - {file = "watchdog-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8"}, - {file = "watchdog-4.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b"}, - {file = "watchdog-4.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92"}, - {file = "watchdog-4.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269"}, - {file = "watchdog-4.0.0-py3-none-win32.whl", hash = "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c"}, - {file = "watchdog-4.0.0-py3-none-win_amd64.whl", hash = "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245"}, - {file = "watchdog-4.0.0-py3-none-win_ia64.whl", hash = "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7"}, - {file = "watchdog-4.0.0.tar.gz", hash = "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec"}, -] - -[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 = "*" -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.16.0" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = ">=3.6" -files = [ - {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, - {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, - {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, - {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, - {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, - {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, - {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, - {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, - {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, - {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, - {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, - {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, - {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, - {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, - {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, - {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, - {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, - {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, -] - -[[package]] -name = "zipp" -version = "3.18.2" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.18.2-py3-none-any.whl", hash = "sha256:dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e"}, - {file = "zipp-3.18.2.tar.gz", hash = "sha256:6278d9ddbcfb1f1089a88fde84481528b07b0e10474e09dcfe53dad4069fa059"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] - -[metadata] -lock-version = "2.0" -python-versions = ">=3.8" -content-hash = "dc743cceaf8c8d472ebd6f824feb5955c09b64b24f84d916fc910def5f7637b5" diff --git a/pyproject.toml b/pyproject.toml index 4558e42570..64f7d7c914 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,35 @@ -[tool.commitizen] -version = "3.27.0" -tag_format = "v$version" -version_files = [ - "pyproject.toml:version", - "commitizen/__version__.py", - ".pre-commit-config.yaml:rev:.+Commitizen", -] - -[tool.poetry] +[project] name = "commitizen" -version = "3.27.0" +version = "4.13.9" description = "Python commitizen client tool" -authors = ["Santiago Fraire <santiwilly@gmail.com>"] -license = "MIT" -keywords = ["commitizen", "conventional", "commits", "git"] +authors = [{ name = "Santiago Fraire", email = "santiwilly@gmail.com" }] +maintainers = [ + { name = "Wei Lee", email = "weilee.rx@gmail.com" }, + { name = "Axel H.", email = "noirbizarre@gmail.com" }, + { name = "Tim Hsiung", email = "bear890707@gmail.com" }, +] +license = { file = "LICENSE" } readme = "docs/README.md" -homepage = "https://github.com/commitizen-tools/commitizen" +requires-python = ">=3.10,<4.0" +dependencies = [ + "questionary (>=2.0,<3.0)", + # Exclude transitive dependency due to known issue in questionary: https://github.com/tmbo/questionary/issues/454 + "prompt_toolkit!=3.0.52", + "decli (>=0.6.0,<1.0)", + "colorama (>=0.4.1,<1.0)", + "termcolor (>=1.1.0,<4.0.0)", + "packaging>=19", + "tomlkit (>=0.8.0,<1.0.0)", + "jinja2>=2.10.3", + "pyyaml>=3.08", + "argcomplete >=1.12.1,<3.7", + "typing-extensions (>=4.0.1,<5.0.0) ; python_version < '3.11'", + "charset-normalizer (>=2.1.0,<4)", + "deprecated (>=1.2.13, <2)", + # Use the Python 3.11 and 3.12 compatible API: https://github.com/python/importlib_metadata#compatibility + "importlib-metadata >=8.0.0,<8.7.0 ; python_version < '3.10'", +] +keywords = ["commitizen", "conventional", "commits", "git"] # See also: https://pypi.org/classifiers/ classifiers = [ "Development Status :: 5 - Production/Stable", @@ -25,75 +39,38 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "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 :: 3.14", "Programming Language :: Python :: Implementation :: CPython", + "License :: OSI Approved :: MIT License", ] -packages = [ - {include = "commitizen"}, - {include = "commitizen/py.typed"}, -] - -[tool.poetry.dependencies] -python = ">=3.8" -questionary = "^2.0" -decli = "^0.6.0" -colorama = "^0.4.1" -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.5" -typing-extensions = { version = "^4.0.1", python = "<3.8" } -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 = { version = ">=8.0.0,<9", python = "<3.10"} - -[tool.poetry.group.dev.dependencies] -# dev tool -ipython = "^8.0" -# test -pytest = ">=7.2,<9.0" -pytest-cov = ">=4,<6" -pytest-mock = "^3.10" -pytest-regressions = "^2.4.0" -pytest-freezer = "^0.4.6" -pytest-xdist = "^3.1.0" -# linter -ruff = ">=0.5.0,<0.6.0" -pre-commit = ">=2.18,<4.0" -mypy = "^1.4" -types-PyYAML = ">=5.4.3,<7.0.0" -types-termcolor = "^0.1.1" -# documentation -mkdocs = "^1.4.2" -mkdocs-material = "^9.1.6" -deprecated = "^1.2.13" -types-deprecated = "^1.2.9.2" -types-python-dateutil = "^2.8.19.13" -rich = "^13.7.1" - - -[tool.poetry.scripts] + +[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" -[tool.poetry.plugins."commitizen.plugin"] +[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" -[tool.poetry.plugins."commitizen.changelog_format"] +[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" -[tool.poetry.plugins."commitizen.provider"] +[project.entry-points."commitizen.provider"] cargo = "commitizen.providers:CargoProvider" commitizen = "commitizen.providers:CommitizenProvider" composer = "commitizen.providers:ComposerProvider" @@ -101,53 +78,136 @@ npm = "commitizen.providers:NpmProvider" pep621 = "commitizen.providers:Pep621Provider" poetry = "commitizen.providers:PoetryProvider" scm = "commitizen.providers:ScmProvider" +uv = "commitizen.providers:UvProvider" -[tool.poetry.plugins."commitizen.scheme"] +[project.entry-points."commitizen.scheme"] pep440 = "commitizen.version_schemes:Pep440" semver = "commitizen.version_schemes:SemVer" semver2 = "commitizen.version_schemes:SemVer2" -[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/*', - ] +[dependency-groups] +dev = [ + { include-group = "base" }, + { include-group = "test" }, + { include-group = "linters" }, + { include-group = "documentation" }, + { include-group = "script" }, + "ipython>=8.0", + "tox>4", + "tox-uv", +] + +base = ["poethepoet>=0.34.0"] + +test = [ + "pytest>=9", + "pytest-cov>=4", + "pytest-mock>=3.10", + "pytest-regressions>=2.4.0", + "pytest-freezer>=0.4.6", + "pytest-xdist>=3.1.0", + "pytest-gitconfig>=0.9.0", + "pre-commit>=4.5.1", +] + +linters = [ + "ruff>=0.11.5", + "mypy>=1.16.0", + "types-deprecated>=1.2.9.2", + "types-python-dateutil>=2.8.19.13", + "types-PyYAML>=5.4.3", + "types-termcolor>=0.1.1", + "types-colorama>=0.4.15.20240311", + "prek>=0.2.28", +] + +documentation = [ + "mkdocs>=1.4.2", + "mkdocs-git-revision-date-localized-plugin>=1.5.0", + "mkdocs-material>=9.1.6", +] + +script = [ + # for scripts/gen_cli_help_screenshots.py + "rich>=13.7.1", +] [build-system] -requires = ["poetry_core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +requires = ["uv_build >= 0.9.17, <0.10.0"] +build-backend = "uv_build" + + +[tool.commitizen] +tag_format = "v$version" +version_files = [ + "commitizen/__version__.py", + ".pre-commit-config.yaml:rev:.+Commitizen", +] +version_provider = "uv" +version_scheme = "pep440" + -[tool.pytest.ini_options] -addopts = "--strict-markers" +[tool.uv.build-backend] +module-name = "commitizen" +module-root = "" +include = ["commitizen/py.typed"] + +[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] +addopts = ["--strict-markers"] +testpaths = ["tests/"] +filterwarnings = [ + # get_smart_tag_range is deprecated and will be removed in v5 + "ignore:Call to deprecated function \\(or staticmethod\\) get_smart_tag_range:DeprecationWarning", +] + +[tool.tox] +requires = ["tox>=4.22"] +env_list = ["3.10", "3.11", "3.12", "3.13", "3.14"] + +[tool.tox.env_run_base] +description = "Run tests suite against Python {base_python}" +dependency_groups = ["test"] +commands = [["pytest", { replace = "posargs", extend = true }]] [tool.ruff] +required-version = ">=0.11.5" line-length = 88 [tool.ruff.lint] select = [ + # flake8-annotations + "ANN001", + "ANN2", # pycodestyle "E", # Pyflakes @@ -156,13 +216,35 @@ select = [ "UP", # isort "I", + # pygrep-hooks + "PGH003", + "PGH004", + # unsorted-dunder-all + "RUF022", + # unused-noqa + "RUF100", + # flake8-pytest-style + "PT", + # Checks for uses of the assert keyword. + "S101", + # flake8-type-checking (TC) + "TC001", + "TC002", + "TC003", + "TC004", + "TC005", + "TC006", ] -ignore = [ - "E501", - "D1", - "D415" +ignore = ["E501", "D1", "D415"] +extend-safe-fixes = [ + "TC", # Move imports inside/outside TYPE_CHECKING blocks + "UP", # Update syntaxes for current Python version recommendations + "PT006", # Use tuple in pytest.mark.parametrize ] +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["ANN", "S101"] + [tool.ruff.lint.isort] known-first-party = ["commitizen", "tests"] @@ -170,7 +252,7 @@ known-first-party = ["commitizen", "tests"] convention = "google" [tool.mypy] -files = "commitizen" +files = ["commitizen", "tests", "scripts"] disallow_untyped_decorators = true disallow_subclassing_any = true warn_return_any = true @@ -179,7 +261,7 @@ warn_unused_ignores = true warn_unused_configs = true [[tool.mypy.overrides]] -module = "py.*" # Legacy pytest dependencies +module = "py.*" # Legacy pytest dependencies ignore_missing_imports = true [tool.codespell] @@ -187,3 +269,53 @@ ignore_missing_imports = true skip = '.git*,*.svg,*.lock' check-hidden = true ignore-words-list = 'asend' + +[tool.poe] +executor.type = "uv" + +[tool.poe.tasks] +format.help = "Format the code" +format.sequence = [{ cmd = "ruff check --fix" }, { cmd = "ruff format" }] + +lint.help = "Lint the code" +lint.sequence = [{ cmd = "ruff check" }, { cmd = "mypy" }] + +check-commit.help = "Check the commit messages" +check-commit.cmd = "cz --no-raise 3 check --rev-range origin/master.." + +test.help = "Run the test suite" +test.cmd = "pytest -n auto --dist=loadfile" + +"test:all".help = "Run the test suite on all supported Python versions" +"test:all".cmd = "tox --parallel" + +"test:regen".help = "Regenerate the test fixtures" +"test:regen".parallel = [ + { ref = "test -k 'not py_' --regen-all" }, + { ref = "test:all -- -k py_ --regen-all" }, +] + +cover.help = "Run the test suite with coverage" +cover.ref = "test --cov-report term-missing --cov-report=xml:coverage.xml --cov=commitizen --junitxml=junit.xml -o junit_family=legacy" + +all.help = "Run all tasks" +all.sequence = ["format", "lint", "check-commit", "cover"] + +"doc:screenshots".help = "Render documentation screenshots" +"doc:screenshots".parallel = [ + { script = "scripts.gen_cli_help_screenshots:gen_cli_help_screenshots" }, + { script = "scripts.gen_cli_interactive_gifs:gen_cli_interactive_gifs" }, +] + +"doc:build".help = "Build the documentation" +"doc:build".cmd = "mkdocs build" + +doc.help = "Live documentation server" +doc.cmd = "mkdocs serve --livereload" # mkdocs hot reload failure workaround. Ref: https://github.com/mkdocs/mkdocs/issues/4032#issuecomment-3591002290 + +ci.help = "Run all tasks in CI" +ci.sequence = ["check-commit", { cmd = "prek run --all-files" }, "cover"] +ci.env = { SKIP = "no-commit-to-branch" } + +setup-pre-commit.help = "Install pre-commit hooks" +setup-pre-commit.cmd = "prek install" diff --git a/scripts/format b/scripts/format deleted file mode 100755 index 0ffe29ba4f..0000000000 --- a/scripts/format +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env sh -set -e - -export PREFIX="poetry run python -m " - -set -x - -# This is needed for running import sorting -${PREFIX}ruff check --fix commitizen tests -${PREFIX}ruff format commitizen tests diff --git a/scripts/gen_cli_help_screenshots.py b/scripts/gen_cli_help_screenshots.py index 0706612391..fce95eb9cd 100644 --- a/scripts/gen_cli_help_screenshots.py +++ b/scripts/gen_cli_help_screenshots.py @@ -1,41 +1,40 @@ import os import subprocess +from itertools import chain 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: + images_root = Path(__file__).parent.parent / "docs" / "images" / "cli_help" + images_root.mkdir(parents=True, exist_ok=True) + + cz_commands = ( + command["name"] if isinstance(command["name"], str) else command["name"][0] + for command in data["subcommands"]["commands"] # type: ignore[index] + ) + for cmd in chain( + ["cz --help"], (f"cz {cz_command} --help" for cz_command in cz_commands) + ): file_name = f"{cmd.replace(' ', '_').replace('-', '_')}.svg" - _export_cmd_as_svg(cmd, f"{images_root}/{file_name}") + _export_cmd_as_svg(cmd, 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"] - ] +def _export_cmd_as_svg(cmd: str, file_path: Path) -> None: + console = Console(record=True, width=80, file=open(os.devnull, "w")) - return cmds + print("Processing command:", cmd) - -def _export_cmd_as_svg(cmd: str, file_name: str) -> None: + console.print(f"$ {cmd}") 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="") + console.print(stdout) + console.save_svg(file_path.as_posix(), title="") + + print("Saved to:", file_path.as_posix()) if __name__ == "__main__": diff --git a/scripts/gen_cli_interactive_gifs.py b/scripts/gen_cli_interactive_gifs.py new file mode 100644 index 0000000000..d60c476059 --- /dev/null +++ b/scripts/gen_cli_interactive_gifs.py @@ -0,0 +1,39 @@ +import subprocess +from pathlib import Path + + +def gen_cli_interactive_gifs() -> None: + """Generate GIF screenshots for interactive commands using VHS.""" + vhs_dir = Path(__file__).parent.parent / "docs" / "images" + output_dir = Path(__file__).parent.parent / "docs" / "images" / "cli_interactive" + output_dir.mkdir(parents=True, exist_ok=True) + + vhs_files = list(vhs_dir.glob("*.tape")) + + if not vhs_files: + print("No VHS tape files found in docs/images/, skipping") + return + + for vhs_file in vhs_files: + print(f"Processing: {vhs_file.name}") + try: + subprocess.run( + ["vhs", vhs_file.name], + check=True, + cwd=vhs_dir, + ) + gif_name = vhs_file.stem + ".gif" + print(f"✓ Generated {gif_name}") + except FileNotFoundError: + print( + "✗ VHS is not installed. Please install it from: " + "https://github.com/charmbracelet/vhs" + ) + raise + except subprocess.CalledProcessError as e: + print(f"✗ Error processing {vhs_file.name}: {e}") + raise + + +if __name__ == "__main__": + gen_cli_interactive_gifs() diff --git a/scripts/publish b/scripts/publish deleted file mode 100755 index 4d31f1188e..0000000000 --- a/scripts/publish +++ /dev/null @@ -1,2 +0,0 @@ -# Publish to pypi -poetry publish --build -u $PYPI_USERNAME -p $PYPI_PASSWORD diff --git a/scripts/test b/scripts/test deleted file mode 100755 index 894228b41f..0000000000 --- a/scripts/test +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env sh -set -e - -export PREFIX='poetry run python -m ' -export REGEX='^(?![.]|venv).*' - -${PREFIX}pytest -n 3 --dist=loadfile --cov-report term-missing --cov-report=xml:coverage.xml --cov=commitizen tests/ -${PREFIX}ruff check commitizen/ tests/ --fix -${PREFIX}mypy commitizen/ tests/ -${PREFIX}commitizen -nr 3 check --rev-range origin/master.. diff --git a/tests/commands/conftest.py b/tests/commands/conftest.py index 91931849b2..ad7e1bc766 100644 --- a/tests/commands/conftest.py +++ b/tests/commands/conftest.py @@ -1,20 +1,13 @@ -import os +from pathlib import Path import pytest +from pytest_mock import MockerFixture, MockType -from commitizen import defaults -from commitizen.config import BaseConfig, JsonConfig +from commitizen.config.json_config import JsonConfig -@pytest.fixture() -def config(): - _config = BaseConfig() - _config.settings.update({"name": defaults.DEFAULT_SETTINGS["name"]}) - return _config - - -@pytest.fixture() -def config_customize(): +@pytest.fixture +def config_customize() -> JsonConfig: json_string = r"""{ "commitizen": { "name": "cz_customize", @@ -39,15 +32,19 @@ def config_customize(): } } }""" - _config = JsonConfig(data=json_string, path="not_exist.json") - return _config + return JsonConfig(data=json_string, path=Path("not_exist.json")) + + +@pytest.fixture +def changelog_path() -> Path: + return Path("CHANGELOG.md") -@pytest.fixture() -def changelog_path() -> str: - return os.path.join(os.getcwd(), "CHANGELOG.md") +@pytest.fixture +def config_path() -> Path: + return Path("pyproject.toml") -@pytest.fixture() -def config_path() -> str: - return os.path.join(os.getcwd(), "pyproject.toml") +@pytest.fixture +def success_mock(mocker: MockerFixture) -> MockType: + return mocker.patch("commitizen.out.success") diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index a0271c322e..bf78e339c7 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -2,18 +2,15 @@ import inspect import re -import sys -from pathlib import Path from textwrap import dedent -from unittest.mock import MagicMock, call +from typing import TYPE_CHECKING +from unittest.mock import call 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 import cmd, defaults, git, hooks +from commitizen.config.base_config import BaseConfig from commitizen.exceptions import ( BumpTagFailedError, CommitizenException, @@ -29,114 +26,115 @@ NotAllowed, NoVersionSpecifiedError, ) -from tests.utils import create_file_and_commit, create_tag, skip_below_py_3_10 + +if TYPE_CHECKING: + from pathlib import Path + + from pytest_mock import MockFixture + from pytest_regressions.file_regression import FileRegressionFixture + + from commitizen.changelog_formats import ChangelogFormat + from commitizen.cz.base import BaseCommitizen + from tests.utils import UtilFixture @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", - ), + "perf: update to use multiprocess", + "perf(worker): update to use multiprocess", + ], ) @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 +def test_bump_patch_increment(commit_msg: str, util: UtilFixture): + util.create_file_and_commit(commit_msg) + util.run_cli("bump", "--yes") + assert git.tag_exist("0.1.1") is True -@pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file")) +@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 +def test_bump_minor_increment(commit_msg: str, util: UtilFixture): + util.create_file_and_commit(commit_msg) + util.run_cli("bump", "--yes") + assert git.tag_exist("0.2.0") is True + assert ( + "commit:refs/tags/0.2.0" + in cmd.run('git for-each-ref refs/tags --format "%(objecttype):%(refname)"').out + ) -@pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file")) +@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 +def test_bump_minor_increment_annotated(commit_msg: str, util: UtilFixture): + util.create_file_and_commit(commit_msg) + util.run_cli("bump", "--yes", "--annotated-tag") + assert git.tag_exist("0.2.0") is True + assert ( + "tag:refs/tags/0.2.0" + in cmd.run('git for-each-ref refs/tags --format "%(objecttype):%(refname)"').out + ) - _is_signed = git.is_signed_tag("0.2.0") - assert _is_signed is False + assert git.is_signed_tag("0.2.0") is False -@pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file")) +@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 +def test_bump_minor_increment_signed(commit_msg: str, util: UtilFixture): + util.create_file_and_commit(commit_msg) + util.run_cli("bump", "--yes", "--gpg-sign") + assert git.tag_exist("0.2.0") is True + assert ( + "tag:refs/tags/0.2.0" + in cmd.run('git for-each-ref refs/tags --format "%(objecttype):%(refname)"').out + ) - _is_signed = git.is_signed_tag("0.2.0") - assert _is_signed is True + assert git.is_signed_tag("0.2.0") is True -@pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file")) +@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 + commit_msg: str, util: UtilFixture, pyproject: Path ): - tmp_commitizen_cfg_file = tmp_commitizen_project.join("pyproject.toml") - tmp_commitizen_cfg_file.write( - f"{tmp_commitizen_cfg_file.read()}\n" f"annotated_tag = 1" + with pyproject.open("a", encoding="utf-8") as f: + f.write("\nannotated_tag = 1") + util.create_file_and_commit(commit_msg) + util.run_cli("bump", "--yes") + assert git.tag_exist("0.2.0") is True + assert ( + "tag:refs/tags/0.2.0" + in cmd.run('git for-each-ref refs/tags --format "%(objecttype):%(refname)"').out ) - 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 + assert git.is_signed_tag("0.2.0") is False -@pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file")) +@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 + commit_msg: str, util: UtilFixture, 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()}\n" f"gpg_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 + tmp_commitizen_cfg_file = tmp_commitizen_project_with_gpg / "pyproject.toml" + with tmp_commitizen_cfg_file.open("a", encoding="utf-8") as f: + f.write("\ngpg_sign = 1") + util.create_file_and_commit(commit_msg) + util.run_cli("bump", "--yes") + assert git.tag_exist("0.2.0") is True + assert ( + "tag:refs/tags/0.2.0" + in cmd.run('git for-each-ref refs/tags --format "%(objecttype):%(refname)"').out + ) - _is_signed = git.is_signed_tag("0.2.0") - assert _is_signed is True + assert git.is_signed_tag("0.2.0") 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", @@ -145,23 +143,18 @@ def test_bump_minor_increment_signed_config_file( "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 +def test_bump_major_increment(commit_msg: str, util: UtilFixture): + util.create_file_and_commit(commit_msg) + util.run_cli("bump", "--yes") + assert git.tag_exist("1.0.0") 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", @@ -170,22 +163,17 @@ def test_bump_major_increment(commit_msg, mocker: MockFixture): "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 +def test_bump_major_increment_major_version_zero(commit_msg: str, util: UtilFixture): + util.create_file_and_commit(commit_msg) + util.run_cli("bump", "--yes", "--major-version-zero") + assert git.tag_exist("0.2.0") is True @pytest.mark.usefixtures("tmp_commitizen_project") @pytest.mark.parametrize( - "commit_msg,increment,expected_tag", + ("commit_msg", "increment", "expected_tag"), [ ("feat: new file", "PATCH", "0.1.1"), ("fix: username exception", "major", "1.0.0"), @@ -194,425 +182,283 @@ def test_bump_major_increment_major_version_zero(commit_msg, mocker): ], ) def test_bump_command_increment_option( - commit_msg, increment, expected_tag, mocker: MockFixture + commit_msg: str, increment: str, expected_tag: str, util: UtilFixture ): - 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 + util.create_file_and_commit(commit_msg) + util.run_cli("bump", "--increment", increment, "--yes") + assert git.tag_exist(expected_tag) is True @pytest.mark.usefixtures("tmp_commitizen_project") -def test_bump_command_prelease(mocker: MockFixture): - create_file_and_commit("feat: location") +def test_bump_command_prerelease(util: UtilFixture): + util.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 + util.run_cli("bump", "--prerelease", "alpha", "--yes") + assert git.tag_exist("0.2.0a0") 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 + util.run_cli("bump", "--prerelease", "beta", "--yes") + assert git.tag_exist("0.2.0b0") 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 + util.run_cli("bump", "--prerelease", "alpha", "--yes") + assert git.tag_exist("0.2.0a1") is False + assert git.tag_exist("0.2.0b1") 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 + util.run_cli("bump", "--prerelease", "rc", "--yes") + assert git.tag_exist("0.2.0rc0") 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 + util.run_cli("bump", "--prerelease", "alpha", "--yes") + assert git.tag_exist("0.2.0a1") is False + assert git.tag_exist("0.2.0b2") is False + assert git.tag_exist("0.2.0rc1") 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 + util.run_cli("bump", "--prerelease", "beta", "--yes") + assert git.tag_exist("0.2.0a2") is False + assert git.tag_exist("0.2.0b2") is False + assert git.tag_exist("0.2.0rc2") 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 + util.run_cli("bump") + assert git.tag_exist("0.2.0") is True @pytest.mark.usefixtures("tmp_commitizen_project") -def test_bump_command_prelease_increment(mocker: MockFixture): +def test_bump_command_prerelease_increment(util: UtilFixture): # 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") + util.create_file_and_commit("fix: location") + util.run_cli("bump", "--yes") + assert git.tag_exist("0.1.1") is True # PRERELEASE - create_file_and_commit("fix: location") + util.create_file_and_commit("fix: location") + util.run_cli("bump", "--prerelease", "alpha", "--yes") + assert git.tag_exist("0.1.2a0") is True - testargs = ["cz", "bump", "--prerelease", "alpha", "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.create_file_and_commit("feat: location") + util.run_cli("bump", "--prerelease", "alpha", "--yes") + assert git.tag_exist("0.2.0a0") is True - 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") + util.create_file_and_commit("feat!: breaking") + util.run_cli("bump", "--prerelease", "alpha", "--yes") + assert git.tag_exist("1.0.0a0") is True @pytest.mark.usefixtures("tmp_commitizen_project") -def test_bump_command_prelease_exact_mode(mocker: MockFixture): +def test_bump_command_prerelease_exact_mode(util: UtilFixture): # 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 + util.create_file_and_commit("feat: location") + util.run_cli("bump", "--prerelease", "alpha", "--yes") + assert git.tag_exist("0.2.0a0") 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 + util.run_cli("bump", "--prerelease", "alpha", "--yes", "--increment-mode=exact") + assert git.tag_exist("0.2.0a1") 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 + util.create_file_and_commit("feat: location") + util.run_cli("bump", "--prerelease", "alpha", "--yes", "--increment-mode=exact") + assert git.tag_exist("0.3.0a0") is True # PRERELEASE + MAJOR BUMP # --increment-mode=exact allows the major version to bump, and restart the prerelease - testargs = [ - "cz", + util.run_cli( "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 + assert git.tag_exist("1.0.0a0") is True @pytest.mark.usefixtures("tmp_commitizen_project") -def test_bump_on_git_with_hooks_no_verify_disabled(mocker: MockFixture): +def test_bump_on_git_with_hooks_no_verify_disabled(util: UtilFixture): """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\n" 'echo "0.1.0"') + 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 + util.create_file_and_commit("feat: new file") + util.run_cli("bump", "--yes") + assert git.tag_exist("0.2.0") is True @pytest.mark.usefixtures("tmp_commitizen_project") -def test_bump_tag_exists_raises_exception(mocker: MockFixture): +def test_bump_tag_exists_raises_exception(util: UtilFixture): cmd.run("mkdir .git/hooks") with open(".git/hooks/post-commit", "w", encoding="utf-8") as f: - f.write("#!/usr/bin/env bash\n" "exit 9") + f.write("#!/usr/bin/env bash\nexit 9") cmd.run("chmod +x .git/hooks/post-commit") # MINOR - create_file_and_commit("feat: new file") + util.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() + util.run_cli("bump", "--yes") 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): +def test_bump_on_git_with_hooks_no_verify_enabled(util: UtilFixture): cmd.run("mkdir .git/hooks") with open(".git/hooks/pre-commit", "w", encoding="utf-8") as f: - f.write("#!/usr/bin/env bash\n" 'echo "0.1.0"') + 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 + util.create_file_and_commit("feat: new file") + util.run_cli("bump", "--yes", "--no-verify") + assert git.tag_exist("0.2.0") is True @pytest.mark.usefixtures("tmp_commitizen_project") -def test_bump_when_bumpping_is_not_support(mocker: MockFixture): - create_file_and_commit( +def test_bump_when_bumping_is_not_support(util: UtilFixture): + util.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() - + util.run_cli("-n", "cz_jira", "bump", "--yes") 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"]) - +def test_bump_when_version_is_not_specify(util: UtilFixture): with pytest.raises(NoVersionSpecifiedError) as excinfo: - cli.main() + util.run_cli("bump") assert NoVersionSpecifiedError.message in str(excinfo.value) @pytest.mark.usefixtures("tmp_commitizen_project") -def test_bump_when_no_new_commit(mocker: MockFixture): +def test_bump_when_no_new_commit(util: UtilFixture): """bump without any commits since the last bump.""" # We need this first commit otherwise the revision is invalid. - create_file_and_commit("feat: initial") + util.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() + util.run_cli("bump", "--yes") # bump without a new commit. with pytest.raises(NoCommitsFoundError) as excinfo: - cli.main() + util.run_cli("bump", "--yes") - expected_error_message = "[NO_COMMITS_FOUND]\n" "No new commits found." - assert expected_error_message in str(excinfo.value) + assert "[NO_COMMITS_FOUND]\nNo new commits found." in str(excinfo.value) def test_bump_when_version_inconsistent_in_version_files( - tmp_commitizen_project, mocker: MockFixture + tmp_commitizen_project, util: UtilFixture ): - 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 = tmp_commitizen_project / "__version__.py" + tmp_version_file.write_text("100.999.10000") + tmp_commitizen_cfg_file = tmp_commitizen_project / "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") + with tmp_commitizen_cfg_file.open("a", encoding="utf-8") as f: + f.write(f'\nversion_files = ["{tmp_version_file_string}"]') - testargs = ["cz", "bump", "--yes", "--check-consistency"] - mocker.patch.object(sys, "argv", testargs) + util.create_file_and_commit("feat: new file") with pytest.raises(CurrentVersionNotFoundError) as excinfo: - cli.main() + util.run_cli("bump", "--yes", "--check-consistency") - partial_expected_error_message = "Current version 0.1.0 is not found in" - assert partial_expected_error_message in str(excinfo.value) + assert "Current version 0.1.0 is not found in" 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") +def test_bump_major_version_zero_when_major_is_not_zero( + tmp_commitizen_project, util: UtilFixture +): + tmp_version_file = tmp_commitizen_project / "__version__.py" + tmp_version_file.write_text("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( + tmp_commitizen_cfg_file = tmp_commitizen_project / "pyproject.toml" + tmp_commitizen_cfg_file.write_text( 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") + tmp_changelog_file = tmp_commitizen_project / "CHANGELOG.md" + tmp_changelog_file.write_text("## v1.0.0") - testargs = ["cz", "bump", "--yes", "--major-version-zero"] - mocker.patch.object(sys, "argv", testargs) + util.create_file_and_commit("feat(user): new file") + util.create_tag("v1.0.0") + util.create_file_and_commit("feat(user)!: new file") with pytest.raises(NotAllowed) as excinfo: - cli.main() + util.run_cli("bump", "--yes", "--major-version-zero") - expected_error_message = ( - "--major-version-zero is meaningless for current version 1.0.0" + assert "--major-version-zero is meaningless for current version 1.0.0" in str( + excinfo.value ) - 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") +def test_bump_files_only(tmp_commitizen_project, util: UtilFixture): + tmp_version_file = tmp_commitizen_project / "__version__.py" + tmp_version_file.write_text("0.1.0") + tmp_commitizen_cfg_file = tmp_commitizen_project / "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}"]' - ) + with tmp_commitizen_cfg_file.open("a", encoding="utf-8") as f: + f.write(f'\nversion_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 + util.create_file_and_commit("feat: new user interface") + util.run_cli("bump", "--yes") + assert git.tag_exist("0.2.0") is True - create_file_and_commit("feat: another new feature") - testargs = ["cz", "bump", "--yes", "--files-only"] - mocker.patch.object(sys, "argv", testargs) + util.create_file_and_commit("feat: another new feature") with pytest.raises(ExpectedExit): - cli.main() + util.run_cli("bump", "--yes", "--version-files-only") - tag_exists = git.tag_exist("0.3.0") - assert tag_exists is False + assert git.tag_exist("0.3.0") is False - with open(tmp_version_file, encoding="utf-8") as f: - assert "0.3.0" in f.read() + assert "0.3.0" in tmp_version_file.read_text(encoding="utf-8") - with open(tmp_commitizen_cfg_file, encoding="utf-8") as f: - assert "0.3.0" in f.read() + assert "0.3.0" in tmp_commitizen_cfg_file.read_text(encoding="utf-8") -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") +def test_bump_local_version(tmp_commitizen_project, util: UtilFixture): + tmp_version_file = tmp_commitizen_project / "__version__.py" + tmp_version_file.write_text("4.5.1+0.1.0") + tmp_commitizen_cfg_file = tmp_commitizen_project / "pyproject.toml" tmp_version_file_string = str(tmp_version_file).replace("\\", "/") - tmp_commitizen_cfg_file.write( + tmp_commitizen_cfg_file.write_text( 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 + util.create_file_and_commit("feat: new user interface") + util.run_cli("bump", "--yes", "--local-version") + assert git.tag_exist("4.5.1+0.2.0") is True - with open(tmp_version_file, encoding="utf-8") as f: - assert "4.5.1+0.2.0" in f.read() + assert "4.5.1+0.2.0" in tmp_version_file.read_text(encoding="utf-8") @pytest.mark.usefixtures("tmp_commitizen_project") -def test_bump_dry_run(mocker: MockFixture, capsys): - create_file_and_commit("feat: new file") +def test_bump_dry_run(util: UtilFixture, capsys: pytest.CaptureFixture): + util.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() + util.run_cli("bump", "--yes", "--dry-run") out, _ = capsys.readouterr() assert "0.2.0" in out - - tag_exists = git.tag_exist("0.2.0") - assert tag_exists is False + assert git.tag_exist("0.2.0") 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_bump_in_non_git_project(tmp_path, monkeypatch, util: UtilFixture): + monkeypatch.chdir(tmp_path) + with pytest.raises(NotAGitProjectError): + util.run_cli("bump", "--yes") def test_none_increment_exit_should_be_a_class(): @@ -634,130 +480,103 @@ def test_none_increment_exit_is_exception(): @pytest.mark.usefixtures("tmp_commitizen_project") def test_none_increment_should_not_call_git_tag_and_error_code_is_not_zero( mocker: MockFixture, + util: UtilFixture, ): - 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) + util.create_file_and_commit("test(test_get_all_droplets): fix bad comparison test") + git_tag_mock = mocker.patch("commitizen.git.tag") - 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 + with pytest.raises(NoneIncrementExit) as e: + util.run_cli("bump", "--yes") - # restore pop stashed - git.tag = stashed_git_tag + git_tag_mock.assert_not_called() + assert e.value.exit_code == ExitCode.NO_INCREMENT @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() +def test_bump_with_changelog_arg(util: UtilFixture, changelog_path): + util.create_file_and_commit("feat(user): new file") + util.run_cli("bump", "--yes", "--changelog") + assert git.tag_exist("0.2.0") is True + + out = changelog_path.read_text(encoding="utf-8") 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: +def test_bump_with_changelog_config(util: UtilFixture, changelog_path, config_path): + util.create_file_and_commit("feat(user): new file") + with config_path.open("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 + util.run_cli("bump", "--yes") + assert git.tag_exist("0.2.0") is True - with open(changelog_path, encoding="utf-8") as f: - out = f.read() + out = changelog_path.read_text(encoding="utf-8") 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) +def test_prevent_prerelease_when_no_increment_detected( + util: UtilFixture, capsys: pytest.CaptureFixture +): + util.create_file_and_commit("feat: new file") - cli.main() + util.run_cli("bump", "--yes") 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) + util.create_file_and_commit("test: new file") with pytest.raises(NoCommitsFoundError) as excinfo: - cli.main() + util.run_cli("bump", "-pr", "beta") - expected_error_message = ( - "[NO_COMMITS_FOUND]\n" "No commits found to generate a pre-release." + assert "[NO_COMMITS_FOUND]\nNo commits found to generate a pre-release." in str( + excinfo.value ) - 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() +def test_bump_with_changelog_to_stdout_arg( + util: UtilFixture, capsys: pytest.CaptureFixture, changelog_path: Path +): + util.create_file_and_commit("feat(user): this should appear in stdout") + util.run_cli("bump", "--yes", "--changelog-to-stdout") out, _ = capsys.readouterr() assert "this should appear in stdout" in out - tag_exists = git.tag_exist("0.2.0") - assert tag_exists is True + assert git.tag_exist("0.2.0") is True - with open(changelog_path, encoding="utf-8") as f: - out = f.read() + out = changelog_path.read_text(encoding="utf-8") 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 + util: UtilFixture, capsys: pytest.CaptureFixture, changelog_path: Path ): - create_file_and_commit( + util.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() + util.run_cli("bump", "--yes", "--changelog-to-stdout", "--dry-run") out, _ = capsys.readouterr() - tag_exists = git.tag_exist("0.2.0") - assert tag_exists is False + assert git.tag_exist("0.2.0") 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() +def test_bump_without_git_to_stdout_arg( + util: UtilFixture, capsys: pytest.CaptureFixture +): + util.create_file_and_commit("feat(user): this should appear in stdout") + util.run_cli("bump", "--yes") out, _ = capsys.readouterr() assert ( @@ -767,11 +586,9 @@ def test_bump_without_git_to_stdout_arg(mocker: MockFixture, capsys, changelog_p @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() +def test_bump_with_git_to_stdout_arg(util: UtilFixture, capsys: pytest.CaptureFixture): + util.create_file_and_commit("feat(user): this should appear in stdout") + util.run_cli("bump", "--yes", "--git-output-to-stderr") out, _ = capsys.readouterr() assert ( @@ -781,7 +598,7 @@ def test_bump_with_git_to_stdout_arg(mocker: MockFixture, capsys, changelog_path @pytest.mark.parametrize( - "version_filepath, version_regex, version_file_content", + ("version_filepath", "version_regex", "version_file_content"), [ pytest.param( "pyproject.toml", @@ -827,9 +644,8 @@ def test_bump_with_git_to_stdout_arg(mocker: MockFixture, capsys, changelog_path @pytest.mark.parametrize( "cli_bump_changelog_args", [ - ("cz", "bump", "--changelog", "--yes"), + ("bump", "--changelog", "--yes"), ( - "cz", "bump", "--changelog", "--changelog-to-stdout", @@ -838,11 +654,13 @@ def test_bump_with_git_to_stdout_arg(mocker: MockFixture, capsys, changelog_path "--yes", ), ], - ids=lambda cmd_tuple: " ".join(cmd_tuple), + ids=lambda cmd_tuple: " ".join(["cz", *cmd_tuple]) + if isinstance(cmd_tuple, tuple) + else cmd_tuple, ) def test_bump_changelog_command_commits_untracked_changelog_and_version_files( tmp_commitizen_project, - mocker, + util: UtilFixture, cli_bump_changelog_args: tuple[str, ...], version_filepath: str, version_regex: str, @@ -857,21 +675,20 @@ def test_bump_changelog_command_commits_untracked_changelog_and_version_files( - Call commitizen main cli and assert that the `CHANGELOG.md` and the version file were committed. """ - with tmp_commitizen_project.join("pyproject.toml").open( + with (tmp_commitizen_project / "pyproject.toml").open( mode="a", encoding="utf-8", ) as commitizen_config: - commitizen_config.write(f"version_files = [\n" f"'{version_regex}'\n]") + commitizen_config.write(f"version_files = [\n'{version_regex}'\n]") - with tmp_commitizen_project.join(version_filepath).open( + with (tmp_commitizen_project / 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") + util.create_file_and_commit("fix: some test commit") - mocker.patch.object(sys, "argv", cli_bump_changelog_args) - cli.main() + util.run_cli(*cli_bump_changelog_args) commit_file_names = git.get_filenames_in_commit() assert "CHANGELOG.md" in commit_file_names @@ -881,21 +698,19 @@ def test_bump_changelog_command_commits_untracked_changelog_and_version_files( @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"], + ["--local-version", "1.2.3"], + ["--prerelease", "rc", "1.2.3"], + ["--devrelease", "0", "1.2.3"], + ["--devrelease", "1", "1.2.3"], + ["--increment", "PATCH", "1.2.3"], + ["--build-metadata=a.b.c", "1.2.3"], + ["--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) - +def test_bump_invalid_manual_args_raises_exception(util: UtilFixture, testargs): with pytest.raises(NotAllowed): - cli.main() + util.run_cli("bump", *testargs) @pytest.mark.usefixtures("tmp_commitizen_project") @@ -906,19 +721,18 @@ def test_bump_invalid_manual_args_raises_exception(mocker, testargs): "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) +def test_bump_invalid_manual_version_raises_exception( + util: UtilFixture, manual_version +): + util.create_file_and_commit("feat: new file") with pytest.raises(InvalidManualVersion) as excinfo: - cli.main() + util.run_cli("bump", "--yes", manual_version) - expected_error_message = ( - "[INVALID_MANUAL_VERSION]\n" f"Invalid manual version: '{manual_version}'" + assert ( + f"[INVALID_MANUAL_VERSION]\nInvalid manual version: '{manual_version}'" + in str(excinfo.value) ) - assert expected_error_message in str(excinfo.value) @pytest.mark.usefixtures("tmp_commitizen_project") @@ -933,59 +747,74 @@ def test_bump_invalid_manual_version_raises_exception(mocker, manual_version): "0.1.1", "0.2.0", "1.0.0", + "1.2", + "1", ], ) -def test_bump_manual_version(mocker, manual_version): - create_file_and_commit("feat: new file") +def test_bump_manual_version(util: UtilFixture, manual_version): + util.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 + util.run_cli("bump", "--yes", manual_version) + assert git.tag_exist(manual_version) 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) - +def test_bump_manual_version_disallows_major_version_zero(util: UtilFixture): + util.create_file_and_commit("feat: new file") with pytest.raises(NotAllowed) as excinfo: - cli.main() + util.run_cli("bump", "--yes", "--major-version-zero", "0.2.0") - expected_error_message = ( - "--major-version-zero cannot be combined with MANUAL_VERSION" + assert "--major-version-zero cannot be combined with MANUAL_VERSION" in str( + excinfo.value ) - assert expected_error_message in str(excinfo.value) -@pytest.mark.parametrize("commit_msg", ("feat: new file", "feat(user): new file")) +@pytest.mark.parametrize( + ("initial_version", "expected_version_after_bump"), + [ + ("1", "1.1.0"), + ("1.2", "1.3.0"), + ], +) +def test_bump_version_with_less_components_in_config( + tmp_commitizen_project_initial, + initial_version, + expected_version_after_bump, + util: UtilFixture, +): + tmp_commitizen_project = tmp_commitizen_project_initial(version=initial_version) + util.run_cli("bump", "--yes") + + assert git.tag_exist(expected_version_after_bump) is True + + for version_file in [ + tmp_commitizen_project / "__version__.py", + tmp_commitizen_project / "pyproject.toml", + ]: + assert expected_version_after_bump in version_file.read_text(encoding="utf-8") + + +@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 + commit_msg, mocker: MockFixture, tmp_commitizen_project, util: UtilFixture ): 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' - ) + tmp_commitizen_cfg_file = tmp_commitizen_project / "pyproject.toml" + with tmp_commitizen_cfg_file.open("a", encoding="utf-8") as f: + f.write( + f'\npre_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() + util.create_file_and_commit(commit_msg) + util.run_cli("bump", "--yes") - tag_exists = git.tag_exist("0.2.0") - assert tag_exists is True + assert git.tag_exist("0.2.0") is True run_mock.assert_has_calls( [ @@ -1017,218 +846,151 @@ def test_bump_with_pre_bump_hooks( ) -def test_bump_with_hooks_and_increment(mocker: MockFixture, tmp_commitizen_project): +def test_bump_with_hooks_and_increment( + mocker: MockFixture, tmp_commitizen_project, util: UtilFixture +): 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' - ) + tmp_commitizen_cfg_file = tmp_commitizen_project / "pyproject.toml" + with tmp_commitizen_cfg_file.open("a", encoding="utf-8") as f: + f.write( + f'\npre_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_commitizen_project") -def test_bump_manual_version_disallows_prerelease_offset(mocker): - create_file_and_commit("feat: new file") - - manual_version = "0.2.0" - testargs = ["cz", "bump", "--yes", "--prerelease-offset", "42", manual_version] - mocker.patch.object(sys, "argv", testargs) + util.create_file_and_commit("test: some test") + util.run_cli("bump", "--yes", "--increment", "MINOR") - with pytest.raises(NotAllowed) as excinfo: - cli.main() - - expected_error_message = ( - "--prerelease-offset cannot be combined with MANUAL_VERSION" - ) - assert expected_error_message in str(excinfo.value) + assert git.tag_exist("0.2.0") is True @pytest.mark.usefixtures("tmp_git_project") -def test_bump_use_version_provider(mocker: MockFixture): +def test_bump_use_version_provider(mocker: MockFixture, util: UtilFixture): 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() + util.create_file_and_commit("fix: fake commit") + util.run_cli("bump", "--yes", "--changelog") - assert git.tag_exist("0.0.1") + assert git.tag_exist("0.0.1") is True 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 +def test_bump_command_prerelease_scheme_via_cli( + tmp_commitizen_project_initial, util: UtilFixture ): 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") + tmp_version_file = tmp_commitizen_project / "__version__.py" + tmp_commitizen_cfg_file = tmp_commitizen_project / "pyproject.toml" - testargs = [ - "cz", + util.run_cli( "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 + ) + assert git.tag_exist("0.2.0-a0") 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() + assert "0.2.0-a0" in version_file.read_text(encoding="utf-8") # 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 + util.run_cli("bump", "--yes") + assert git.tag_exist("0.2.0") 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() + assert "0.2.0" in version_file.read_text(encoding="utf-8") -def test_bump_command_prelease_scheme_via_config( - tmp_commitizen_project_initial, mocker: MockFixture +def test_bump_command_prerelease_scheme_via_config( + tmp_commitizen_project_initial, util: UtilFixture ): 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() + tmp_version_file = tmp_commitizen_project / "__version__.py" + tmp_commitizen_cfg_file = tmp_commitizen_project / "pyproject.toml" - tag_exists = git.tag_exist("0.2.0-a0") - assert tag_exists is True + util.run_cli("bump", "--prerelease", "alpha", "--yes") + assert git.tag_exist("0.2.0-a0") 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() + assert "0.2.0-a0" in version_file.read_text(encoding="utf-8") - 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 + util.run_cli("bump", "--prerelease", "alpha", "--yes") + assert git.tag_exist("0.2.0-a1") 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() + assert "0.2.0-a1" in version_file.read_text(encoding="utf-8") # 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 + util.run_cli("bump", "--yes") + assert git.tag_exist("0.2.0") 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() + assert "0.2.0" in version_file.read_text(encoding="utf-8") -def test_bump_command_prelease_scheme_check_old_tags( - tmp_commitizen_project_initial, mocker: MockFixture +def test_bump_command_prerelease_scheme_check_old_tags( + tmp_commitizen_project_initial, util: UtilFixture ): 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") + tmp_version_file = tmp_commitizen_project / "__version__.py" + tmp_commitizen_cfg_file = tmp_commitizen_project / "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 + util.run_cli("bump", "--prerelease", "alpha", "--yes") + assert git.tag_exist("v0.2.0-a0") 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() + assert "0.2.0-a0" in version_file.read_text(encoding="utf-8") - tag_exists = git.tag_exist("v0.2.0-a1") - assert tag_exists is True + util.run_cli("bump", "--prerelease", "alpha") + assert git.tag_exist("v0.2.0-a1") 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() + assert "0.2.0-a1" in version_file.read_text(encoding="utf-8") # 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 + util.run_cli("bump") + assert git.tag_exist("v0.2.0") 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() + assert "0.2.0" in version_file.read_text(encoding="utf-8") @pytest.mark.usefixtures("tmp_commitizen_project") @pytest.mark.usefixtures("use_cz_semver") @pytest.mark.parametrize( - "message, expected_tag", + ("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 +def test_bump_with_plugin(util: UtilFixture, message: str, expected_tag: str): + util.create_file_and_commit(message) + util.run_cli("--name", "cz_semver", "bump", "--yes") + assert git.tag_exist(expected_tag) is True @pytest.mark.usefixtures("tmp_commitizen_project") @pytest.mark.usefixtures("use_cz_semver") @pytest.mark.parametrize( - "message, expected_tag", + ("message", "expected_tag"), [ ("minor: add users", "0.2.0"), ("patch: bug affecting users", "0.1.1"), @@ -1236,79 +998,68 @@ def test_bump_with_plugin(mocker: MockFixture, message: str, expected_tag: str): ], ) def test_bump_with_major_version_zero_with_plugin( - mocker: MockFixture, message: str, expected_tag: str + util: UtilFixture, 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 + util.create_file_and_commit(message) + util.run_cli("--name", "cz_semver", "bump", "--yes", "--major-version-zero") + assert git.tag_exist(expected_tag) 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") +def test_bump_command_version_type_deprecation(util: UtilFixture): + util.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() + with pytest.warns(DeprecationWarning, match=r".*--version-type.*deprecated"): + util.run_cli( + "bump", + "--prerelease", + "alpha", + "--yes", + "--version-type", + "semver", + ) - assert git.tag_exist("0.2.0-a0") + assert git.tag_exist("0.2.0-a0") is True @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") +def test_bump_command_version_scheme_priority_over_version_type(util: UtilFixture): + util.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() + with pytest.warns(DeprecationWarning, match=r".*--version-type.*deprecated"): + util.run_cli( + "bump", + "--prerelease", + "alpha", + "--yes", + "--version-type", + "semver", + "--version-scheme", + "pep440", + ) - assert git.tag_exist("0.2.0a0") + assert git.tag_exist("0.2.0a0") is True @pytest.mark.parametrize( - "arg, cfg, expected", - ( + ("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, +def test_bump_template_option_precedence( tmp_commitizen_project: Path, + util: UtilFixture, any_changelog_format: ChangelogFormat, arg: str, cfg: str, expected: str, ): - project_root = Path(tmp_commitizen_project) + project_root = tmp_commitizen_project cfg_template = project_root / "changelog.cfg" cmd_template = project_root / "changelog.cmd" default_template = project_root / any_changelog_format.template @@ -1318,7 +1069,7 @@ def test_bump_template_option_precedance( cmd_template.write_text("from cmd") default_template.write_text("default") - create_file_and_commit("feat: new file") + util.create_file_and_commit("feat: new file") if cfg: pyproject = project_root / "pyproject.toml" @@ -1332,23 +1083,22 @@ def test_bump_template_option_precedance( ) ) - testargs = ["cz", "bump", "--yes", "--changelog"] + args = ["bump", "--yes", "--changelog"] if arg: - testargs.append(arg) - mocker.patch.object(sys, "argv", testargs + ["0.1.1"]) - cli.main() + args.append(arg) + args.append("0.1.1") + util.run_cli(*args) - out = changelog.read_text() - assert out == expected + assert changelog.read_text() == expected -def test_bump_template_extras_precedance( - mocker: MockFixture, +def test_bump_template_extras_precedence( tmp_commitizen_project: Path, + util: UtilFixture, any_changelog_format: ChangelogFormat, mock_plugin: BaseCommitizen, ): - project_root = Path(tmp_commitizen_project) + project_root = tmp_commitizen_project changelog_tpl = project_root / any_changelog_format.template changelog_tpl.write_text("{{first}} - {{second}} - {{third}}") @@ -1369,37 +1119,33 @@ def test_bump_template_extras_precedance( ) ) - create_file_and_commit("feat: new file") + util.create_file_and_commit("feat: new file") - testargs = [ - "cz", + util.run_cli( "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, + util: UtilFixture, any_changelog_format: ChangelogFormat, ): - project_root = Path(tmp_commitizen_project) + project_root = 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") + util.create_file_and_commit("feat: new file") - testargs = [ - "cz", + util.run_cli( "bump", "--changelog", "--yes", @@ -1410,55 +1156,384 @@ def test_bump_template_extra_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): +def test_bump_changelog_contains_increment_only( + tmp_commitizen_project: Path, util: UtilFixture, capsys: pytest.CaptureFixture +): """Issue 1024""" # Initialize commitizen up to v1.0.0 - project_root = Path(tmp_commitizen_project) + project_root = tmp_commitizen_project tmp_commitizen_cfg_file = project_root / "pyproject.toml" tmp_commitizen_cfg_file.write_text( - "[tool.commitizen]\n" 'version="1.0.0"\n' "update_changelog_on_bump = true\n" + '[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") + util.create_file_and_commit("feat(user): new file") + util.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() + util.create_file_and_commit("feat(user)!: new file") + util.run_cli("bump", "--yes") _ = 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) + util.create_file_and_commit("feat(next)!: next version") with pytest.raises(ExpectedExit): - cli.main() + util.run_cli("bump", "--yes", "--version-files-only", "--changelog-to-stdout") out, _ = capsys.readouterr() assert "3.0.0" in out assert "2.0.0" not in out -@skip_below_py_3_10 -def test_bump_command_shows_description_when_use_help_option( - mocker: MockFixture, capsys, file_regression +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_get_next(util: UtilFixture, capsys: pytest.CaptureFixture): + util.create_file_and_commit("feat: new file") + + with pytest.raises(DryRunExit): + util.run_cli("bump", "--yes", "--get-next") + + out, _ = capsys.readouterr() + assert "0.2.0" in out + assert git.tag_exist("0.2.0") is False + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_get_next_update_changelog_on_bump( + util: UtilFixture, capsys: pytest.CaptureFixture, config_path: Path ): - testargs = ["cz", "bump", "--help"] - mocker.patch.object(sys, "argv", testargs) - with pytest.raises(SystemExit): - cli.main() + util.create_file_and_commit("feat: new file") + with config_path.open("a", encoding="utf-8") as fp: + fp.write("update_changelog_on_bump = true\n") + + with pytest.raises(DryRunExit): + util.run_cli("bump", "--yes", "--get-next") + + out, _ = capsys.readouterr() + assert "0.2.0" in out + assert git.tag_exist("0.2.0") is False + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_get_next__no_eligible_commits_raises(util: UtilFixture): + util.create_file_and_commit("chore: new commit") + + with pytest.raises(NoneIncrementExit): + util.run_cli("bump", "--yes", "--get-next") + + +def test_bump_allow_no_commit_with_no_commit( + tmp_commitizen_project, monkeypatch, util: UtilFixture, capsys +): + monkeypatch.chdir(tmp_commitizen_project) + # Create the first commit and bump to 1.0.0 + util.create_file_and_commit("feat(user)!: new file") + util.run_cli("bump", "--yes") + + # Verify NoCommitsFoundError should be raised + # when there's no new commit and "--allow-no-commit" is not set + with pytest.raises(NoCommitsFoundError): + util.run_cli("bump") + + # bump to 1.0.1 with new commit when "--allow-no-commit" is set + util.run_cli("bump", "--allow-no-commit") + out, _ = capsys.readouterr() + assert "bump: version 1.0.0 → 1.0.1" in out + + +def test_bump_allow_no_commit_with_no_eligible_commit( + tmp_commitizen_project, monkeypatch, util: UtilFixture, capsys +): + monkeypatch.chdir(tmp_commitizen_project) + # Create the first commit and bump to 1.0.0 + util.create_file_and_commit("feat(user)!: new file") + util.run_cli("bump", "--yes") + + # Create a commit that is ineligible to bump + util.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): + util.run_cli("bump", "--yes") + + # bump to 1.0.1 with ineligible commit when "--allow-no-commit" is set + util.run_cli("bump", "--allow-no-commit") + out, _ = capsys.readouterr() + assert "bump: version 1.0.0 → 1.0.1" in out + + +def test_bump_allow_no_commit_with_increment( + tmp_commitizen_project, monkeypatch, util: UtilFixture, capsys +): + monkeypatch.chdir(tmp_commitizen_project) + # # Create the first commit and bump to 1.0.0 + util.create_file_and_commit("feat(user)!: new file") + util.run_cli("bump", "--yes") + + # Verify NoCommitsFoundError should be raised + # when there's no new commit and "--allow-no-commit" is not set + with pytest.raises(NoCommitsFoundError): + util.run_cli("bump", "--yes") + + # bump to 1.1.0 with no new commit when "--allow-no-commit" is set + # and increment is specified + util.run_cli("bump", "--yes", "--allow-no-commit", "--increment", "MINOR") + out, _ = capsys.readouterr() + assert "bump: version 1.0.0 → 1.1.0" in out + +def test_bump_allow_no_commit_with_manual_version( + tmp_commitizen_project, monkeypatch, util: UtilFixture, capsys +): + monkeypatch.chdir(tmp_commitizen_project) + # # Create the first commit and bump to 1.0.0 + util.create_file_and_commit("feat(user)!: new file") + util.run_cli("bump", "--yes") + + # Verify NoCommitsFoundError should be raised + # when there's no new commit and "--allow-no-commit" is not set + with pytest.raises(NoCommitsFoundError): + util.run_cli("bump", "--yes") + + # bump to 1.1.0 with no new commit when "--allow-no-commit" is set + # and increment is specified + util.run_cli("bump", "--yes", "--allow-no-commit", "2.0.0") out, _ = capsys.readouterr() - file_regression.check(out, extension=".txt") + assert "bump: version 1.0.0 → 2.0.0" in out + + +def test_bump_detect_legacy_tags_from_scm( + tmp_commitizen_project: Path, util: UtilFixture +): + project_root = 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}"', + "]", + ] + ), + ) + util.create_file_and_commit("feat: new file") + util.create_tag("legacy-0.4.2") + util.create_file_and_commit("feat: new file") + + util.run_cli("bump", "--increment", "patch", "--changelog") + + assert git.tag_exist("v0.4.3") is True + + +def test_bump_warn_but_dont_fail_on_invalid_tags( + tmp_commitizen_project: Path, + util: UtilFixture, + capsys: pytest.CaptureFixture, +): + project_root = tmp_commitizen_project + tmp_commitizen_cfg_file = project_root / "pyproject.toml" + tmp_commitizen_cfg_file.write_text( + "\n".join( + [ + "[tool.commitizen]", + 'version_provider = "scm"', + 'version_scheme = "pep440"', + ] + ), + ) + util.create_file_and_commit("feat: new file") + util.create_tag("0.4.2") + util.create_file_and_commit("feat: new file") + util.create_tag("0.4.3.deadbeaf") + util.create_file_and_commit("feat: new file") + + util.run_cli("bump", "--increment", "patch", "--changelog") + + _, err = capsys.readouterr() + + assert err.count("Invalid version tag: '0.4.3.deadbeaf'") == 1 + assert git.tag_exist("0.4.3") is True + + +def test_is_initial_tag(mocker: MockFixture, tmp_commitizen_project, util: UtilFixture): + """Test the _is_initial_tag method behavior.""" + # Create a commit but no tags + util.create_file_and_commit("feat: initial commit") + + # Initialize Bump with minimal config + config = BaseConfig() + config.settings.update( + { + "name": defaults.DEFAULT_SETTINGS["name"], + "encoding": "utf-8", + "pre_bump_hooks": [], + "post_bump_hooks": [], + } + ) + + # Initialize with required arguments + arguments = { + "changelog": False, + "changelog_to_stdout": False, + "git_output_to_stderr": False, + "no_verify": False, + "check_consistency": False, + "retry": False, + "version_scheme": None, + "file_name": None, + "template": None, + "extras": None, + } + + bump_cmd = bump.Bump(config, arguments) # type: ignore[arg-type] + + # Test case 1: No current tag, not yes mode + mocker.patch("questionary.confirm", return_value=mocker.Mock(ask=lambda: True)) + assert bump_cmd._is_initial_tag(None, is_yes=False) is True + + # Test case 2: No current tag, yes mode + assert bump_cmd._is_initial_tag(None, is_yes=True) is True + + # Test case 3: Has current tag + mock_tag = mocker.Mock() + assert bump_cmd._is_initial_tag(mock_tag, is_yes=False) is False + + # Test case 4: No current tag, user denies + mocker.patch("questionary.confirm", return_value=mocker.Mock(ask=lambda: False)) + assert bump_cmd._is_initial_tag(None, is_yes=False) is False + + +@pytest.mark.parametrize("test_input", ["rc", "alpha", "beta"]) +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2025-01-01") +def test_changelog_config_flag_merge_prerelease( + mocker: MockFixture, + util: UtilFixture, + changelog_path: Path, + config_path: Path, + file_regression: FileRegressionFixture, + test_input: str, +): + with config_path.open("a") as f: + f.write("changelog_merge_prerelease = true\n") + f.write("update_changelog_on_bump = true\n") + f.write("annotated_tag = true\n") + + util.create_file_and_commit("irrelevant commit") + mocker.patch("commitizen.git.GitTag.date", "1970-01-01") + git.tag("0.1.0") + + util.create_file_and_commit("feat: add new output") + util.create_file_and_commit("fix: output glitch") + util.run_cli("bump", "--prerelease", test_input, "--yes") + + util.run_cli("bump", "--changelog") + + out = changelog_path.read_text() + + file_regression.check(out, extension=".md") + + +@pytest.mark.parametrize("test_input", ["rc", "alpha", "beta"]) +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2025-01-01") +def test_changelog_config_flag_merge_prerelease_only_prerelease_present( + util: UtilFixture, + changelog_path: Path, + config_path: Path, + file_regression: FileRegressionFixture, + test_input: str, +): + with config_path.open("a") as f: + f.write("changelog_merge_prerelease = true\n") + f.write("update_changelog_on_bump = true\n") + f.write("annotated_tag = true\n") + + util.create_file_and_commit("feat: more relevant commit") + util.run_cli("bump", "--prerelease", test_input, "--yes") + + util.create_file_and_commit("feat: add new output") + util.create_file_and_commit("fix: output glitch") + util.run_cli("bump", "--prerelease", test_input, "--yes") + + util.run_cli("bump", "--changelog") + + out = changelog_path.read_text() + + file_regression.check(out, extension=".md") + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_bump_deprecate_files_only(util: UtilFixture): + util.create_file_and_commit("feat: new file") + with ( + pytest.warns(DeprecationWarning, match=r".*--files-only.*deprecated"), + pytest.raises(ExpectedExit), + ): + util.run_cli("bump", "--yes", "--files-only") + + +@pytest.mark.parametrize( + ("prerelease", "merge"), + [ + pytest.param(True, "true", id="with_prerelease_merge"), + pytest.param(True, "false", id="with_prerelease_no_merge"), + pytest.param(False, "true", id="without_prerelease"), + ], +) +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2025-01-01") +def test_changelog_merge_preserves_header( + mocker: MockFixture, + util: UtilFixture, + changelog_path: Path, + config_path: Path, + file_regression: FileRegressionFixture, + prerelease: bool, + merge: str, +): + """Test that merge_prerelease preserves existing changelog header.""" + with config_path.open("a") as f: + f.write(f"changelog_merge_prerelease = {merge}\n") + f.write("update_changelog_on_bump = true\n") + f.write("annotated_tag = true\n") + + # Create initial version with changelog that has a header + util.create_file_and_commit("irrelevant commit") + mocker.patch("commitizen.git.GitTag.date", "1970-01-01") + git.tag("0.1.0") + + # Create a changelog with a header manually + changelog_path.write_text( + dedent("""\ + # Changelog + + All notable changes to this project will be documented here. + + ## 0.1.0 (1970-01-01) + """) + ) + + util.create_file_and_commit("feat: add new output") + util.create_file_and_commit("fix: output glitch") + + if prerelease: + util.run_cli("bump", "--prerelease", "alpha", "--yes") + + util.create_file_and_commit("feat: new feature right before the bump") + util.run_cli("bump", "--changelog") + + out = changelog_path.read_text() + + file_regression.check(out, extension=".md") 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 deleted file mode 100644 index de76f2efcf..0000000000 --- a/tests/commands/test_bump_command/test_bump_command_shows_description_when_use_help_option.txt +++ /dev/null @@ -1,79 +0,0 @@ -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] - [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 {alpha,beta,rc}, -pr {alpha,beta,rc} - choose type of prerelease - --devrelease 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 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 TEMPLATE, -t TEMPLATE - changelog template file name (relative to the current - working directory) - --extra 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 diff --git a/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_alpha_.md b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_alpha_.md new file mode 100644 index 0000000000..6061938125 --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_alpha_.md @@ -0,0 +1,11 @@ +## 0.2.0 (2025-01-01) + +### Feat + +- add new output + +### Fix + +- output glitch + +## 0.1.0 (1970-01-01) diff --git a/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_beta_.md b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_beta_.md new file mode 100644 index 0000000000..6061938125 --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_beta_.md @@ -0,0 +1,11 @@ +## 0.2.0 (2025-01-01) + +### Feat + +- add new output + +### Fix + +- output glitch + +## 0.1.0 (1970-01-01) diff --git a/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_only_prerelease_present_alpha_.md b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_only_prerelease_present_alpha_.md new file mode 100644 index 0000000000..1f04057964 --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_only_prerelease_present_alpha_.md @@ -0,0 +1,10 @@ +## 0.2.0 (2025-01-01) + +### Feat + +- add new output +- more relevant commit + +### Fix + +- output glitch diff --git a/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_only_prerelease_present_beta_.md b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_only_prerelease_present_beta_.md new file mode 100644 index 0000000000..1f04057964 --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_only_prerelease_present_beta_.md @@ -0,0 +1,10 @@ +## 0.2.0 (2025-01-01) + +### Feat + +- add new output +- more relevant commit + +### Fix + +- output glitch diff --git a/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_only_prerelease_present_rc_.md b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_only_prerelease_present_rc_.md new file mode 100644 index 0000000000..1f04057964 --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_only_prerelease_present_rc_.md @@ -0,0 +1,10 @@ +## 0.2.0 (2025-01-01) + +### Feat + +- add new output +- more relevant commit + +### Fix + +- output glitch diff --git a/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_rc_.md b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_rc_.md new file mode 100644 index 0000000000..6061938125 --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_config_flag_merge_prerelease_rc_.md @@ -0,0 +1,11 @@ +## 0.2.0 (2025-01-01) + +### Feat + +- add new output + +### Fix + +- output glitch + +## 0.1.0 (1970-01-01) diff --git a/tests/commands/test_bump_command/test_changelog_merge_preserves_header_with_prerelease_merge_.md b/tests/commands/test_bump_command/test_changelog_merge_preserves_header_with_prerelease_merge_.md new file mode 100644 index 0000000000..c0ac9c5c9c --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_merge_preserves_header_with_prerelease_merge_.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to this project will be documented here. + +## 0.2.0 (2025-01-01) + +### Feat + +- new feature right before the bump +- add new output + +### Fix + +- output glitch + +## 0.1.0 (1970-01-01) diff --git a/tests/commands/test_bump_command/test_changelog_merge_preserves_header_with_prerelease_no_merge_.md b/tests/commands/test_bump_command/test_changelog_merge_preserves_header_with_prerelease_no_merge_.md new file mode 100644 index 0000000000..6058182503 --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_merge_preserves_header_with_prerelease_no_merge_.md @@ -0,0 +1,21 @@ +# Changelog + +All notable changes to this project will be documented here. + +## 0.2.0 (2025-01-01) + +### Feat + +- new feature right before the bump + +## 0.2.0a0 (2025-01-01) + +### Feat + +- add new output + +### Fix + +- output glitch + +## 0.1.0 (1970-01-01) diff --git a/tests/commands/test_bump_command/test_changelog_merge_preserves_header_without_prerelease_.md b/tests/commands/test_bump_command/test_changelog_merge_preserves_header_without_prerelease_.md new file mode 100644 index 0000000000..c0ac9c5c9c --- /dev/null +++ b/tests/commands/test_bump_command/test_changelog_merge_preserves_header_without_prerelease_.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to this project will be documented here. + +## 0.2.0 (2025-01-01) + +### Feat + +- new feature right before the bump +- add new output + +### Fix + +- output glitch + +## 0.1.0 (1970-01-01) diff --git a/tests/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py index 4694d33305..b2d024ac7f 100644 --- a/tests/commands/test_changelog_command.py +++ b/tests/commands/test_changelog_command.py @@ -1,20 +1,15 @@ +from __future__ import annotations + import itertools -import sys -from datetime import datetime from pathlib import Path from textwrap import dedent +from typing import TYPE_CHECKING import pytest -from dateutil import relativedelta from jinja2 import FileSystemLoader -from pytest_mock import MockFixture -from commitizen import __file__ as commitizen_init -from commitizen import cli, git -from commitizen.changelog_formats import ChangelogFormat +from commitizen import git 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, @@ -23,181 +18,164 @@ NotAGitProjectError, NotAllowed, ) -from tests.utils import ( - create_branch, - create_file_and_commit, - create_tag, - get_current_branch, - merge_branch, - skip_below_py_3_10, - switch_branch, - wait_for_tag, -) + +if TYPE_CHECKING: + from pytest_mock import MockFixture + from pytest_regressions.file_regression import FileRegressionFixture + + from commitizen.changelog_formats import ChangelogFormat + from commitizen.config.base_config import BaseConfig + from commitizen.config.json_config import JsonConfig + from commitizen.cz.base import BaseCommitizen + from tests.utils import UtilFixture + + +@pytest.fixture +def changelog_jinja_file(tmp_commitizen_project: Path) -> Path: + return tmp_commitizen_project / "changelog.jinja" + + +@pytest.fixture +def changelog_tpl( + tmp_commitizen_project: Path, any_changelog_format: ChangelogFormat +) -> Path: + return tmp_commitizen_project / any_changelog_format.template + + +@pytest.fixture +def changelog_file( + tmp_commitizen_project: Path, any_changelog_format: ChangelogFormat +) -> Path: + return tmp_commitizen_project / any_changelog_format.default_changelog_file @pytest.mark.usefixtures("tmp_commitizen_project") def test_changelog_from_version_zero_point_two( - mocker: MockFixture, capsys, file_regression + capsys: pytest.CaptureFixture, + file_regression: FileRegressionFixture, + util: UtilFixture, ): - create_file_and_commit("feat: new file") - create_file_and_commit("refactor: not in changelog") + util.create_file_and_commit("feat: new file") + util.create_file_and_commit("refactor: not in changelog") # create tag - testargs = ["cz", "bump", "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.run_cli("bump", "--yes") capsys.readouterr() - create_file_and_commit("feat: after 0.2.0") - create_file_and_commit("feat: after 0.2") + util.create_file_and_commit("feat: after 0.2.0") + util.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() + util.run_cli("changelog", "--dry-run", "--start-rev", "0.2.0") 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) +def test_changelog_with_different_cz( + capsys: pytest.CaptureFixture, + file_regression: FileRegressionFixture, + util: UtilFixture, +): + util.create_file_and_commit("JRA-34 #comment corrected indent issue") + util.create_file_and_commit("JRA-35 #time 1w 2d 4h 30m Total work logged") with pytest.raises(DryRunExit): - cli.main() + util.run_cli("-n", "cz_jira", "changelog", "--dry-run") 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 + changelog_format: ChangelogFormat, + file_regression: FileRegressionFixture, + util: UtilFixture, ): - create_file_and_commit("feat: new file") - create_file_and_commit("refactor: is in changelog") - create_file_and_commit("Merge into master") + util.create_file_and_commit("feat: new file") + util.create_file_and_commit("refactor: is in changelog") + util.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() + util.run_cli("changelog", "--file-name", changelog_file, "--template", template) - with open(changelog_file, encoding="utf-8") as f: - out = f.read() + out = Path(changelog_file).read_text(encoding="utf-8") file_regression.check(out, extension=changelog_format.ext) @pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-08-14") def test_changelog_replacing_unreleased_using_incremental( - mocker: MockFixture, capsys, changelog_format: ChangelogFormat, file_regression + changelog_format: ChangelogFormat, + file_regression: FileRegressionFixture, + util: UtilFixture, ): - create_file_and_commit("feat: add new output") - create_file_and_commit("fix: output glitch") - create_file_and_commit("Merge into master") + util.create_file_and_commit("feat: add new output") + util.create_file_and_commit("fix: output glitch") + util.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() + util.run_cli("changelog", "--file-name", changelog_file, "--template", template) - create_file_and_commit("fix: mama gotta work") - create_file_and_commit("feat: add more stuff") - create_file_and_commit("Merge into master") + util.run_cli("bump", "--yes", "--file-name", changelog_file, "--template", template) - testargs = [ - "cz", + util.create_file_and_commit("fix: mama gotta work") + util.create_file_and_commit("feat: add more stuff") + util.create_file_and_commit("Merge into master") + + util.run_cli( "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" - ) + out = Path(changelog_file).read_text(encoding="utf-8") file_regression.check(out, extension=changelog_format.ext) @pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-08-14") def test_changelog_is_persisted_using_incremental( - mocker: MockFixture, capsys, changelog_path, file_regression + changelog_path: Path, + file_regression: FileRegressionFixture, + util: UtilFixture, ): - create_file_and_commit("feat: add new output") - create_file_and_commit("fix: output glitch") - create_file_and_commit("Merge into master") + util.create_file_and_commit("feat: add new output") + util.create_file_and_commit("fix: output glitch") + util.create_file_and_commit("Merge into master") - testargs = ["cz", "bump", "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.run_cli("bump", "--yes") - testargs = ["cz", "changelog"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.run_cli("changelog") - with open(changelog_path, "a", encoding="utf-8") as f: + with changelog_path.open("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") + util.create_file_and_commit("fix: mama gotta work") + util.create_file_and_commit("feat: add more stuff") + util.create_file_and_commit("Merge into master") - testargs = ["cz", "changelog", "--incremental"] + util.run_cli("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" - ) + out = changelog_path.read_text(encoding="utf-8") 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 + changelog_path: Path, + file_regression: FileRegressionFixture, + util: UtilFixture, ): - with open(changelog_path, "w", encoding="utf-8") as f: + with changelog_path.open("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" @@ -205,22 +183,18 @@ def test_changelog_incremental_angular_sample( "\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") + util.create_file_and_commit("irrelevant commit") + util.create_tag("10.0.0-rc.3") - testargs = ["cz", "changelog", "--incremental"] + util.create_file_and_commit("feat: add new output") + util.create_file_and_commit("fix: output glitch") + util.create_file_and_commit("fix: mama gotta work") + util.create_file_and_commit("feat: add more stuff") + util.create_file_and_commit("Merge into master") - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.run_cli("changelog", "--incremental") - with open(changelog_path, encoding="utf-8") as f: - out = f.read() + out = changelog_path.read_text(encoding="utf-8") file_regression.check(out, extension=".md") @@ -252,49 +226,50 @@ def test_changelog_incremental_angular_sample( @pytest.mark.usefixtures("tmp_commitizen_project") def test_changelog_incremental_keep_a_changelog_sample( - mocker: MockFixture, capsys, changelog_path, file_regression + changelog_path: Path, + file_regression: FileRegressionFixture, + util: UtilFixture, ): - with open(changelog_path, "w", encoding="utf-8") as f: + with changelog_path.open("w", encoding="utf-8") as f: f.write(KEEP_A_CHANGELOG) - create_file_and_commit("irrelevant commit") - git.tag("1.0.0") + util.create_file_and_commit("irrelevant commit") + util.create_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") + util.create_file_and_commit("feat: add new output") + util.create_file_and_commit("fix: output glitch") + util.create_file_and_commit("fix: mama gotta work") + util.create_file_and_commit("feat: add more stuff") + util.create_file_and_commit("Merge into master") - testargs = ["cz", "changelog", "--incremental"] + util.run_cli("changelog", "--incremental") - mocker.patch.object(sys, "argv", testargs) - cli.main() - - with open(changelog_path, encoding="utf-8") as f: - out = f.read() + out = changelog_path.read_text(encoding="utf-8") 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): +def test_changelog_hook( + mocker: MockFixture, config: BaseConfig, dry_run: bool, util: UtilFixture +): 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") + util.create_file_and_commit("feat: new file") + util.create_file_and_commit("refactor: is in changelog") + util.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: + if dry_run: + with pytest.raises(DryRunExit): + changelog() + else: changelog() - except DryRunExit: - pass full_changelog = ( "## Unreleased\n\n### Refactor\n\n- is in changelog\n\n### Feat\n\n- new file\n" @@ -307,13 +282,15 @@ def test_changelog_hook(mocker: MockFixture, config: BaseConfig, dry_run: bool): @pytest.mark.usefixtures("tmp_commitizen_project") -def test_changelog_hook_customize(mocker: MockFixture, config_customize): +def test_changelog_hook_customize( + mocker: MockFixture, config_customize: JsonConfig, util: UtilFixture +): 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") + util.create_file_and_commit("feat: new file") + util.create_file_and_commit("refactor: is in changelog") + util.create_file_and_commit("Merge into master") changelog = Changelog( config_customize, @@ -329,15 +306,17 @@ def test_changelog_hook_customize(mocker: MockFixture, config_customize): @pytest.mark.usefixtures("tmp_commitizen_project") -def test_changelog_release_hook(mocker: MockFixture, config): +def test_changelog_release_hook( + mocker: MockFixture, config: BaseConfig, util: UtilFixture +): 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") + util.create_file_and_commit("feat: new file") + util.create_file_and_commit("refactor: is in changelog") + util.create_file_and_commit("Merge into master") + util.create_tag(f"0.{i + 1}.0") # changelog = Changelog(config, {}) changelog = Changelog( @@ -352,7 +331,7 @@ def changelog_release_hook(release: dict, tag: git.GitTag) -> dict: @pytest.mark.usefixtures("tmp_commitizen_project") def test_changelog_with_non_linear_merges_commit_order( - mocker: MockFixture, config_customize + mocker: MockFixture, config_customize: JsonConfig, util: UtilFixture ): """Test that commits merged non-linearly are correctly ordered in the changelog @@ -384,23 +363,23 @@ def test_changelog_with_non_linear_merges_commit_order( changelog_hook_mock = mocker.Mock() changelog_hook_mock.return_value = "cool changelog hook" - create_file_and_commit("feat: initial commit") + util.create_file_and_commit("feat: initial commit") - main_branch = get_current_branch() + main_branch = util.get_current_branch() - create_branch("branchA") - create_branch("branchB") + util.create_branch("branchA") + util.create_branch("branchB") - switch_branch("branchA") - create_file_and_commit("feat: I will be merged second") + util.switch_branch("branchA") + util.create_file_and_commit("feat: I will be merged second") - switch_branch("branchB") - create_file_and_commit("feat: I will be merged first") + util.switch_branch("branchB") + util.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") + util.switch_branch(main_branch) + util.merge_branch("branchB") + util.merge_branch("branchA") changelog = Changelog( config_customize, @@ -421,66 +400,44 @@ def test_changelog_with_non_linear_merges_commit_order( @pytest.mark.usefixtures("tmp_commitizen_project") def test_changelog_multiple_incremental_do_not_add_new_lines( - mocker: MockFixture, capsys, changelog_path, file_regression + changelog_path: Path, + file_regression: FileRegressionFixture, + util: UtilFixture, ): """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() + for commit_message in [ + "feat: add new output", + "fix: output glitch", + "fix: no more explosions", + "feat: add more stuff", + ]: + util.create_file_and_commit(commit_message) + util.run_cli("changelog", "--incremental") - 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() + out = changelog_path.read_text(encoding="utf-8") 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 + changelog_path: Path, + util: UtilFixture, + file_regression: FileRegressionFixture, ): """Test for https://github.com/commitizen-tools/commitizen/issues/509""" - with open(changelog_path, "w", encoding="utf-8") as f: + with changelog_path.open("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() + util.create_file_and_commit("feat: add more cat videos") + util.run_cli("changelog", "--incremental") + out = changelog_path.read_text(encoding="utf-8") - 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" - ) + file_regression.check(out, extension=".md") -def test_changelog_without_revision(mocker: MockFixture, tmp_commitizen_project): - changelog_file = tmp_commitizen_project.join("CHANGELOG.md") - changelog_file.write( +def test_changelog_without_revision(tmp_commitizen_project, util: UtilFixture): + (tmp_commitizen_project / "CHANGELOG.md").write_text( """ # Unreleased @@ -488,91 +445,61 @@ def test_changelog_without_revision(mocker: MockFixture, tmp_commitizen_project) """ ) - # create_file_and_commit("feat: new file") - testargs = ["cz", "changelog", "--incremental"] - mocker.patch.object(sys, "argv", testargs) + # No revision + with pytest.raises(NoRevisionError): + util.run_cli("changelog", "--incremental") + + util.create_file_and_commit("feat: new file") + util.create_tag("2.0.0") + # With different tag name and changelog content with pytest.raises(NoRevisionError): - cli.main() + util.run_cli("changelog", "--incremental") @pytest.mark.usefixtures("tmp_commitizen_project") -def test_changelog_incremental_with_revision(mocker): +def test_changelog_incremental_with_revision(util: UtilFixture): """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() + util.run_cli("changelog", "--incremental", "0.2.0") -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 +@pytest.mark.usefixtures("chdir") +def test_changelog_in_non_git_project(util: UtilFixture): + with pytest.raises(NotAGitProjectError): + util.run_cli("changelog", "--incremental") - ## 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 = ( +@pytest.mark.parametrize( + "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 = ( + "footer content", "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) + "BREAKING CHANGE: migrate by renaming user to users", + ], +) +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_breaking_change_content_v1_beta( + capsys: pytest.CaptureFixture, + file_regression: FileRegressionFixture, + util: UtilFixture, + commit_message: str, +): + util.create_file_and_commit(commit_message) with pytest.raises(DryRunExit): - cli.main() + util.run_cli("changelog", "--dry-run") 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 + capsys: pytest.CaptureFixture, + file_regression: FileRegressionFixture, + util: UtilFixture, ): commit_message = ( "feat(users): email pattern corrected\n\n" @@ -581,25 +508,23 @@ def test_breaking_change_content_v1_multiline( "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) + util.create_file_and_commit(commit_message) with pytest.raises(DryRunExit): - cli.main() + util.run_cli("changelog", "--dry-run") 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 + capsys: pytest.CaptureFixture, + file_regression: FileRegressionFixture, + util: UtilFixture, ): commit_message = "chore!: drop support for py36" - create_file_and_commit(commit_message) - testargs = ["cz", "changelog", "--dry-run"] - mocker.patch.object(sys, "argv", testargs) + util.create_file_and_commit(commit_message) with pytest.raises(DryRunExit): - cli.main() + util.run_cli("changelog", "--dry-run") out, _ = capsys.readouterr() file_regression.check(out, extension=".md") @@ -607,14 +532,14 @@ def test_breaking_change_content_v1_with_exclamation_mark( @pytest.mark.usefixtures("tmp_commitizen_project") def test_breaking_change_content_v1_with_exclamation_mark_feat( - mocker: MockFixture, capsys, file_regression + capsys: pytest.CaptureFixture, + file_regression: FileRegressionFixture, + util: UtilFixture, ): 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) + util.create_file_and_commit(commit_message) with pytest.raises(DryRunExit): - cli.main() + util.run_cli("changelog", "--dry-run") out, _ = capsys.readouterr() file_regression.check(out, extension=".md") @@ -622,21 +547,21 @@ def test_breaking_change_content_v1_with_exclamation_mark_feat( @pytest.mark.usefixtures("tmp_commitizen_project") def test_changelog_config_flag_increment( - mocker: MockFixture, changelog_path, config_path, file_regression + changelog_path: Path, + config_path: Path, + file_regression: FileRegressionFixture, + util: UtilFixture, ): - with open(config_path, "a", encoding="utf-8") as f: + with config_path.open("a", encoding="utf-8") as f: f.write("changelog_incremental = true\n") - with open(changelog_path, "a", encoding="utf-8") as f: + with changelog_path.open("a", encoding="utf-8") as f: f.write("\nnote: this should be persisted using increment\n") - create_file_and_commit("feat: add new output") + util.create_file_and_commit("feat: add new output") - testargs = ["cz", "changelog"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.run_cli("changelog") - with open(changelog_path, encoding="utf-8") as f: - out = f.read() + out = changelog_path.read_text(encoding="utf-8") assert "this should be persisted using increment" in out file_regression.check(out, extension=".md") @@ -644,59 +569,56 @@ def test_changelog_config_flag_increment( @pytest.mark.parametrize("test_input", ["rc", "alpha", "beta"]) @pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2025-12-29") def test_changelog_config_flag_merge_prerelease( - mocker: MockFixture, changelog_path, config_path, file_regression, test_input + changelog_path: Path, + config_path: Path, + file_regression: FileRegressionFixture, + test_input: str, + util: UtilFixture, ): - with open(config_path, "a") as f: + with config_path.open("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") + util.create_file_and_commit("irrelevant commit") + util.create_tag("1.0.0") - testargs = ["cz", "bump", "--prerelease", test_input, "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.create_file_and_commit("feat: add new output") + util.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") + util.run_cli("bump", "--prerelease", test_input, "--yes") - testargs = ["cz", "changelog"] + util.create_file_and_commit("fix: mama gotta work") + util.create_file_and_commit("feat: add more stuff") + util.create_file_and_commit("Merge into master") - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.run_cli("changelog") - with open(changelog_path) as f: - out = f.read() + out = changelog_path.read_text() 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 + capsys: pytest.CaptureFixture, + config_path: Path, + file_regression: FileRegressionFixture, + util: UtilFixture, ): # create commit and tag - create_file_and_commit("feat: new file") - testargs = ["cz", "bump", "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.create_file_and_commit("feat: new file") + util.run_cli("bump", "--yes") capsys.readouterr() - create_file_and_commit("feat: after 0.2.0") - create_file_and_commit("feat: after 0.2") + util.create_file_and_commit("feat: after 0.2.0") + util.create_file_and_commit("feat: after 0.2") - with open(config_path, "a", encoding="utf-8") as f: + with config_path.open("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() + util.run_cli("changelog", "--dry-run") out, _ = capsys.readouterr() file_regression.check(out, extension=".md") @@ -704,27 +626,25 @@ def test_changelog_config_start_rev_option( @pytest.mark.usefixtures("tmp_commitizen_project") def test_changelog_incremental_keep_a_changelog_sample_with_annotated_tag( - mocker: MockFixture, capsys, changelog_path, file_regression + changelog_path: Path, + file_regression: FileRegressionFixture, + util: UtilFixture, ): """Fix #378""" - with open(changelog_path, "w", encoding="utf-8") as f: + with changelog_path.open("w", encoding="utf-8") as f: f.write(KEEP_A_CHANGELOG) - create_file_and_commit("irrelevant commit") - git.tag("1.0.0", annotated=True) + util.create_file_and_commit("irrelevant commit") + util.create_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") + util.create_file_and_commit("feat: add new output") + util.create_file_and_commit("fix: output glitch") + util.create_file_and_commit("fix: mama gotta work") + util.create_file_and_commit("feat: add more stuff") + util.create_file_and_commit("Merge into master") - testargs = ["cz", "changelog", "--incremental"] + util.run_cli("changelog", "--incremental") - mocker.patch.object(sys, "argv", testargs) - cli.main() - - with open(changelog_path, encoding="utf-8") as f: - out = f.read() + out = changelog_path.read_text(encoding="utf-8") file_regression.check(out, extension=".md") @@ -733,96 +653,89 @@ def test_changelog_incremental_keep_a_changelog_sample_with_annotated_tag( @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 + changelog_path: Path, + file_regression: FileRegressionFixture, + test_input: str, + util: UtilFixture, ): """Fix #357""" - with open(changelog_path, "w", encoding="utf-8") as f: + with changelog_path.open("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") + util.create_file_and_commit("irrelevant commit") + util.create_tag("1.0.0", annotated=True) - testargs = ["cz", "bump", "--changelog", "--prerelease", test_input, "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.create_file_and_commit("feat: add new output") + util.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") + util.run_cli("bump", "--changelog", "--prerelease", test_input, "--yes") - testargs = ["cz", "changelog", "--incremental"] + util.create_file_and_commit("fix: mama gotta work") + util.create_file_and_commit("feat: add more stuff") + util.create_file_and_commit("Merge into master") - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.run_cli("changelog", "--incremental") - with open(changelog_path, encoding="utf-8") as f: - out = f.read() + out = changelog_path.read_text(encoding="utf-8") file_regression.check(out, extension=".md") @pytest.mark.parametrize( - "from_pre,to_pre", itertools.product(["alpha", "beta", "rc"], repeat=2) + ("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 + changelog_path: Path, + file_regression: FileRegressionFixture, + from_pre: str, + to_pre: str, + util: UtilFixture, ): - with open(changelog_path, "w") as f: + with changelog_path.open("w") as f: f.write(KEEP_A_CHANGELOG) - create_file_and_commit("irrelevant commit") - git.tag("1.0.0", annotated=True) + util.create_file_and_commit("irrelevant commit") + util.create_tag("1.0.0", annotated=True) - create_file_and_commit("feat: add new output") - create_file_and_commit("fix: output glitch") + util.create_file_and_commit("feat: add new output") + util.create_file_and_commit("fix: output glitch") - testargs = ["cz", "bump", "--changelog", "--prerelease", from_pre, "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.run_cli("bump", "--changelog", "--prerelease", from_pre, "--yes") - testargs = ["cz", "bump", "--changelog", "--prerelease", to_pre, "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.run_cli("bump", "--changelog", "--prerelease", to_pre, "--yes") - with open(changelog_path) as f: - out = f.read() + out = changelog_path.read_text() file_regression.check(out, extension=".md") @pytest.mark.parametrize("test_input", ["rc", "alpha", "beta"]) @pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2025-12-29") def test_changelog_release_candidate_version_with_merge_prerelease( - mocker: MockFixture, changelog_path, file_regression, test_input + changelog_path: Path, + file_regression: FileRegressionFixture, + test_input: str, + util: UtilFixture, ): """Fix #357""" - with open(changelog_path, "w") as f: + with changelog_path.open("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") + util.create_file_and_commit("irrelevant commit") + util.create_tag("1.0.0") - create_file_and_commit("feat: add new output") - create_file_and_commit("fix: output glitch") + util.create_file_and_commit("feat: add new output") + util.create_file_and_commit("fix: output glitch") - testargs = ["cz", "bump", "--prerelease", test_input, "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.run_cli("bump", "--prerelease", test_input, "--yes") - create_file_and_commit("fix: mama gotta work") - create_file_and_commit("feat: add more stuff") - create_file_and_commit("Merge into master") + util.create_file_and_commit("fix: mama gotta work") + util.create_file_and_commit("feat: add more stuff") + util.create_file_and_commit("Merge into master") - testargs = ["cz", "changelog", "--merge-prerelease"] + util.run_cli("changelog", "--merge-prerelease") - mocker.patch.object(sys, "argv", testargs) - cli.main() - - with open(changelog_path) as f: - out = f.read() + out = changelog_path.read_text() file_regression.check(out, extension=".md") @@ -831,87 +744,69 @@ def test_changelog_release_candidate_version_with_merge_prerelease( @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 + changelog_path: Path, + file_regression: FileRegressionFixture, + test_input: str, + util: UtilFixture, ): """Fix #357""" - with open(changelog_path, "w") as f: + with changelog_path.open("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") + util.create_file_and_commit("irrelevant commit") + util.create_tag("1.0.0") - testargs = ["cz", "bump", "--prerelease", test_input, "--yes", "--changelog"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.create_file_and_commit("feat: add new output") - create_file_and_commit("fix: output glitch") + util.run_cli("bump", "--prerelease", test_input, "--yes", "--changelog") - testargs = ["cz", "bump", "--prerelease", test_input, "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.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") + util.run_cli("bump", "--prerelease", test_input, "--yes") - testargs = ["cz", "changelog", "--merge-prerelease", "--incremental"] + util.create_file_and_commit("fix: mama gotta work") + util.create_file_and_commit("feat: add more stuff") + util.create_file_and_commit("Merge into master") - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.run_cli("changelog", "--merge-prerelease", "--incremental") - with open(changelog_path) as f: - out = f.read() + out = changelog_path.read_text() 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: +def test_changelog_with_filename_as_empty_string(config_path: Path, util: UtilFixture): + with config_path.open("a", encoding="utf-8") as f: f.write("changelog_file = true\n") - create_file_and_commit("feat: add new output") + util.create_file_and_commit("feat: add new output") - testargs = ["cz", "changelog"] - mocker.patch.object(sys, "argv", testargs) with pytest.raises(NotAllowed): - cli.main() + util.run_cli("changelog") @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 + config_path: Path, + changelog_path: Path, + file_regression: FileRegressionFixture, + util: UtilFixture, ): - mocker.patch("commitizen.git.GitTag.date", "2022-02-13") - - with open(config_path, "a", encoding="utf-8") as f: + with config_path.open("a", encoding="utf-8") as f: f.write('tag_format = "$version"\n') # create commit and tag - create_file_and_commit("feat: new file") + util.create_file_and_commit("feat: new file") + util.run_cli("bump", "--yes") - testargs = ["cz", "bump", "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() - wait_for_tag() + util.create_file_and_commit("feat: after 0.2.0") + util.create_file_and_commit("feat: another feature") - create_file_and_commit("feat: after 0.2.0") - create_file_and_commit("feat: another feature") + util.run_cli("bump", "--yes") - 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() + util.run_cli("changelog", "0.2.0") + out = changelog_path.read_text(encoding="utf-8") file_regression.check(out, extension=".md") @@ -919,259 +814,226 @@ def test_changelog_from_rev_first_version_from_arg( @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 + config_path: Path, + changelog_path: Path, + file_regression: FileRegressionFixture, + util: UtilFixture, ): - mocker.patch("commitizen.git.GitTag.date", "2022-02-13") - - with open(config_path, "a", encoding="utf-8") as f: + with config_path.open("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") + util.create_file_and_commit("feat: new file") + util.run_cli("bump", "--yes") - testargs = ["cz", "bump", "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.create_file_and_commit("feat: after 0.2.0") + util.create_file_and_commit("feat: another feature") - wait_for_tag() + util.run_cli("bump", "--yes") - testargs = ["cz", "changelog", "0.3.0"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.run_cli("changelog", "0.3.0") - with open(changelog_path) as f: - out = f.read() + out = changelog_path.read_text() file_regression.check(out, extension=".md") @pytest.mark.usefixtures("tmp_commitizen_project") -@pytest.mark.freeze_time("2022-02-13") -def test_changelog_from_rev_single_version_not_found( - mocker: MockFixture, config_path, changelog_path +@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( + config_path: Path, rev_range: str, tag: str, util: UtilFixture ): """Provides an invalid revision ID to changelog command""" - with open(config_path, "a", encoding="utf-8") as f: + with config_path.open("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() + util.create_file_and_commit("feat: new file") + util.create_tag(tag) + util.create_file_and_commit("feat: new file") + util.create_tag("1.0.0") - wait_for_tag() + with pytest.raises(NoCommitsFoundError) as excinfo: + util.run_cli("changelog", rev_range) # it shouldn't exist - create_file_and_commit("feat: after 0.2.0") - create_file_and_commit("feat: another feature") + assert "Could not find a valid revision" in str(excinfo) - testargs = ["cz", "bump", "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() - wait_for_tag() - testargs = ["cz", "changelog", "0.8.0"] # it shouldn't exist - mocker.patch.object(sys, "argv", testargs) - with pytest.raises(NoCommitsFoundError) as excinfo: - cli.main() +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_multiple_matching_tags( + config_path: Path, changelog_path: Path, util: UtilFixture +): + with config_path.open("a", encoding="utf-8") as f: + f.write('tag_format = "new-$version"\nlegacy_tag_formats = ["legacy-$version"]') - assert "Could not find a valid revision" in str(excinfo) + util.create_file_and_commit("feat: new file") + util.create_tag("legacy-1.0.0") + util.create_file_and_commit("feat: new file") + util.create_tag("legacy-2.0.0") + util.create_tag("new-2.0.0") + + with pytest.warns() as warnings: + util.run_cli("changelog", "1.0.0..2.0.0") # it shouldn't exist + + assert len(warnings) == 1 + warning = warnings[0] + assert "Multiple tags found for version 2.0.0" in str(warning.message) + + out = changelog_path.read_text() + + # 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 + changelog_path: Path, util: UtilFixture ): """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() + util.create_file_and_commit("feat: new file") + util.run_cli("bump", "--yes") - create_file_and_commit("feat: after 0.2.0") - create_file_and_commit("feat: another feature") + util.create_file_and_commit("feat: after 0.2.0") + util.create_file_and_commit("feat: another feature") - testargs = ["cz", "bump", "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() - wait_for_tag() + util.run_cli("bump", "--yes") - testargs = ["cz", "changelog", "0.3.0"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.run_cli("changelog", "0.3.0") - with open(changelog_path) as f: - out = f.read() + out = changelog_path.read_text() assert "new file" not in out @pytest.mark.usefixtures("tmp_commitizen_project") @pytest.mark.freeze_time("2022-02-13") -def test_changelog_from_rev_range_version_not_found(mocker: MockFixture, config_path): - """Provides an invalid end revision ID to changelog command""" - with open(config_path, "a", encoding="utf-8") as f: +def test_changelog_from_rev_version_range_including_first_tag( + config_path: Path, + changelog_path: Path, + file_regression: FileRegressionFixture, + util: UtilFixture, +): + with config_path.open("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() + util.create_file_and_commit("feat: new file") + util.run_cli("bump", "--yes") - create_file_and_commit("feat: after 0.2.0") - create_file_and_commit("feat: another feature") + util.create_file_and_commit("feat: after 0.2.0") + util.create_file_and_commit("feat: another feature") - testargs = ["cz", "bump", "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.run_cli("bump", "--yes") - testargs = ["cz", "changelog", "0.5.0..0.8.0"] # it shouldn't exist - mocker.patch.object(sys, "argv", testargs) - with pytest.raises(NoCommitsFoundError) as excinfo: - cli.main() + util.run_cli("changelog", "0.2.0..0.3.0") + out = changelog_path.read_text(encoding="utf-8") - assert "Could not find a valid revision" in str(excinfo) + 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_including_first_tag( - mocker: MockFixture, config_path, changelog_path, file_regression +def test_changelog_from_rev_version_range_from_arg( + config_path: Path, + changelog_path: Path, + file_regression: FileRegressionFixture, + util: UtilFixture, ): - mocker.patch("commitizen.git.GitTag.date", "2022-02-13") - - with open(config_path, "a", encoding="utf-8") as f: + with config_path.open("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() + util.create_file_and_commit("feat: new file") + util.run_cli("bump", "--yes") + util.create_file_and_commit("feat: after 0.2.0") + util.create_file_and_commit("feat: another feature") - create_file_and_commit("feat: after 0.2.0") - create_file_and_commit("feat: another feature") + util.run_cli("bump", "--yes") - testargs = ["cz", "bump", "--yes"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.create_file_and_commit("feat: getting ready for this") - 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() + util.run_cli("bump", "--yes") + + util.run_cli("changelog", "0.3.0..0.4.0") + out = changelog_path.read_text() 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 +def test_changelog_from_rev_version_range_with_legacy_tags( + config_path: Path, + changelog_path: Path, + file_regression: FileRegressionFixture, + util: UtilFixture, ): - mocker.patch("commitizen.git.GitTag.date", "2022-02-13") + Path(config_path).write_text( + "\n".join( + [ + "[tool.commitizen]", + 'version_provider = "scm"', + 'tag_format = "v$version"', + "legacy_tag_formats = [", + ' "legacy-${version}",', + ' "old-${version}",', + "]", + ] + ), + ) - with open(config_path, "a", encoding="utf-8") as f: - f.write('tag_format = "$version"\n') + util.create_file_and_commit("feat: new file") + util.create_tag("old-0.2.0") + util.create_file_and_commit("feat: new file") + util.create_tag("legacy-0.3.0") + util.create_file_and_commit("feat: new file") + util.create_tag("legacy-0.4.0") - # 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") + util.run_cli("changelog", "0.2.0..0.4.0") + file_regression.check(Path(changelog_path).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 + config_path, changelog_path, file_regression, util: UtilFixture ): - mocker.patch("commitizen.git.GitTag.date", "2022-02-13") - - with open(config_path, "a", encoding="utf-8") as f: + with config_path.open("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() + util.create_file_and_commit("feat: new file") + + util.run_cli("bump", "--yes") + + util.create_file_and_commit("feat: after 0.2.0") + util.create_file_and_commit("feat: another feature") + + util.run_cli("bump", "--yes") # 0.3.0 + util.create_file_and_commit("feat: getting ready for this") + + util.run_cli("bump", "--yes") # 0.4.0 + util.create_file_and_commit("fix: small error") + + util.run_cli("bump", "--yes") # 0.4.1 + util.create_file_and_commit("feat: new shinny feature") + + util.run_cli("bump", "--yes") # 0.5.0 + util.create_file_and_commit("feat: amazing different shinny feature") + + util.run_cli("bump", "--yes") # 0.6.0 + + util.run_cli("changelog", "0.3.0..0.5.0") + out = changelog_path.read_text() file_regression.check(out, extension=".md") @@ -1179,33 +1041,26 @@ def test_changelog_from_rev_version_with_big_range_from_arg( @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 + capsys: pytest.CaptureFixture, + config_path: Path, + file_regression: FileRegressionFixture, + util: UtilFixture, ): - mocker.patch("commitizen.git.GitTag.date", "2022-02-13") - - with open(config_path, "a") as f: + with config_path.open("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() + util.create_file_and_commit("feat: new file") + util.run_cli("bump", "--yes") + + util.create_file_and_commit("feat: after 0.2.0") + util.create_file_and_commit("feat: another feature") + + util.run_cli("bump", "--yes") 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() + util.run_cli("changelog", "0.3.0", "--dry-run") out, _ = capsys.readouterr() @@ -1213,148 +1068,123 @@ def test_changelog_from_rev_latest_version_dry_run( @pytest.mark.usefixtures("tmp_commitizen_project") -def test_invalid_subject_is_skipped(mocker: MockFixture, capsys): +def test_invalid_subject_is_skipped( + capsys: pytest.CaptureFixture, + file_regression: FileRegressionFixture, + util: UtilFixture, +): """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) + util.create_file_and_commit(non_conformant_commit_title) + util.create_file_and_commit("feat: a new world") with pytest.raises(DryRunExit): - cli.main() + util.run_cli("changelog", "--dry-run") out, _ = capsys.readouterr() - assert out == ("## Unreleased\n\n### Feat\n\n- a new world\n\n") + file_regression.check(out, extension=".md") -@pytest.mark.freeze_time("2022-02-13") @pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") def test_changelog_with_customized_change_type_order( - mocker, config_path, changelog_path, file_regression + config_path: Path, + changelog_path: Path, + file_regression: FileRegressionFixture, + util: UtilFixture, ): - mocker.patch("commitizen.git.GitTag.date", "2022-02-13") - - with open(config_path, "a") as f: + with config_path.open("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() + util.create_file_and_commit("feat: new file") + util.run_cli("bump", "--yes") + util.create_file_and_commit("feat: after 0.2.0") + util.create_file_and_commit("feat: another feature") + util.create_file_and_commit("fix: fix bug") + + util.run_cli("bump", "--yes") + + util.create_file_and_commit("feat: getting ready for this") + util.create_file_and_commit("perf: perf improvement") + + util.run_cli("bump", "--yes") + + util.run_cli("changelog", "0.3.0..0.4.0") + out = changelog_path.read_text() 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") +def test_empty_commit_list(mocker: MockFixture, util: UtilFixture): + util.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() + util.run_cli("changelog") @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 + capsys: pytest.CaptureFixture, + config_path: Path, + file_regression: FileRegressionFixture, + util: UtilFixture, ): - mocker.patch("commitizen.git.GitTag.date", "2022-02-13") - - with open(config_path, "a") as f: - f.write('tag_format = "$version"\n' 'version_scheme = "semver"') + with config_path.open("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() + util.create_file_and_commit("feat: new file") + util.run_cli("bump", "--yes") + + util.create_file_and_commit("feat: after 0.2.0") + util.create_file_and_commit("feat: another feature") + + util.run_cli("bump", "--yes", "--prerelease", "alpha") capsys.readouterr() - wait_for_tag() - tag_exists = git.tag_exist("0.3.0-a0") - assert tag_exists is True + assert git.tag_exist("0.3.0-a0") - testargs = ["cz", "changelog", "0.3.0-a0", "--dry-run"] - mocker.patch.object(sys, "argv", testargs) with pytest.raises(DryRunExit): - cli.main() + util.run_cli("changelog", "0.3.0-a0", "--dry-run") out, _ = capsys.readouterr() - file_regression.check(out, extension=".md") - testargs = ["cz", "bump", "--yes", "--prerelease", "alpha"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.run_cli("bump", "--yes", "--prerelease", "alpha") capsys.readouterr() - wait_for_tag() - tag_exists = git.tag_exist("0.3.0-a1") - assert tag_exists is True + assert git.tag_exist("0.3.0-a1") - testargs = ["cz", "changelog", "0.3.0-a1", "--dry-run"] - mocker.patch.object(sys, "argv", testargs) with pytest.raises(DryRunExit): - cli.main() + util.run_cli("changelog", "0.3.0-a1", "--dry-run") 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): +def test_changelog_uses_version_tags_for_header( + mocker: MockFixture, config: BaseConfig, util: UtilFixture +): """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") + util.create_file_and_commit("feat: commit in 1.0.0") + util.create_tag("0-not-a-version") + util.create_tag("1.0.0") + util.create_tag("also-not-a-version") write_patch = mocker.patch("commitizen.commands.changelog.out.write") @@ -1373,8 +1203,9 @@ def test_changelog_uses_version_tags_for_header(mocker: MockFixture, config): @pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2022-02-13") def test_changelog_from_current_version_tag_with_nonversion_tag( - mocker: MockFixture, config + mocker: MockFixture, config: BaseConfig, util: UtilFixture ): """Tests that changelog generation for a single version works even if there is a non-version tag in the list of tags @@ -1383,86 +1214,67 @@ def test_changelog_from_current_version_tag_with_nonversion_tag( 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") + util.create_file_and_commit("feat: initial commit") + util.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") + util.create_file_and_commit("feat: commit 1") + util.create_tag("1-not-a-version") - create_file_and_commit( - "feat: commit 2", - committer_date=( - datetime.now() - relativedelta.relativedelta(seconds=1) - ).isoformat(), - ) + util.create_file_and_commit("feat: commit 2") - create_file_and_commit("bump: version 1.0.0 → 2.0.0") - create_tag("2.0.0") + util.create_file_and_commit("bump: version 1.0.0 → 2.0.0") + util.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 = "\ + Changelog( + config, + { + "dry_run": True, + "incremental": False, + "unreleased_version": None, + "rev_range": "2.0.0", + }, + )() + + write_patch.assert_called_with( + "\ ## 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", - ( + ("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, +def test_changelog_template_option_precedence( tmp_commitizen_project: Path, - any_changelog_format: ChangelogFormat, arg: str, cfg: str, expected: str, + util: UtilFixture, + changelog_file: Path, + changelog_tpl: Path, ): - project_root = Path(tmp_commitizen_project) + project_root = 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") + changelog_tpl.write_text("default") - create_file_and_commit("feat: new file") + util.create_file_and_commit("feat: new file") if cfg: pyproject = project_root / "pyproject.toml" @@ -1476,31 +1288,27 @@ def test_changelog_template_option_precedance( ) ) - testargs = ["cz", "changelog"] + testargs = ["changelog"] if arg: testargs.append(arg) - mocker.patch.object(sys, "argv", testargs) - cli.main() + util.run_cli(*testargs) - out = changelog.read_text() - assert out == expected + assert changelog_file.read_text() == expected -def test_changelog_template_extras_precedance( - mocker: MockFixture, - tmp_commitizen_project: Path, +def test_changelog_template_extras_precedence( + changelog_tpl: Path, mock_plugin: BaseCommitizen, - any_changelog_format: ChangelogFormat, + pyproject: Path, + changelog_file: Path, + util: UtilFixture, ): - 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( """\ @@ -1513,29 +1321,231 @@ def test_changelog_template_extras_precedance( ) ) - create_file_and_commit("feat: new file") + util.create_file_and_commit("feat: new file") + util.run_cli("changelog", "--extra", "first=from-command") - testargs = ["cz", "changelog", "--extra", "first=from-command"] - mocker.patch.object(sys, "argv", testargs) - cli.main() + assert changelog_file.read_text() == "from-command - from-config - from-plugin" - 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( + changelog_path: Path, + config_path: Path, + util: UtilFixture, +): + with config_path.open("a", encoding="utf-8") as f: + f.write('\ntag_format = "custom${version}"\n') + util.create_file_and_commit("feat: new file") + util.create_tag("v0.2.0") + util.create_file_and_commit("feat: another new file") + util.create_tag("0.2.0") + util.create_tag("random0.2.0") + util.run_cli("bump", "--changelog", "--yes") + util.create_file_and_commit("feat: another new file") + util.run_cli("bump", "--changelog", "--yes") + out = changelog_path.read_text() + 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 -def test_changelog_template_extra_quotes( - mocker: MockFixture, - tmp_commitizen_project: Path, - any_changelog_format: ChangelogFormat, + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_changelog_only_tag_matching_tag_format_included_prefix_sep( + changelog_path: Path, + config_path: Path, + util: UtilFixture, ): - project_root = Path(tmp_commitizen_project) - changelog_tpl = project_root / any_changelog_format.template - changelog_tpl.write_text("{{first}} - {{second}} - {{third}}") + with config_path.open("a", encoding="utf-8") as f: + f.write('\ntag_format = "custom-${version}"\n') + util.create_file_and_commit("feat: new file") + util.create_tag("v0.2.0") + util.create_file_and_commit("feat: another new file") + util.create_tag("0.2.0") + util.create_tag("random0.2.0") + util.run_cli("bump", "--changelog", "--yes") + util.create_file_and_commit("feat: new version another new file") + util.create_file_and_commit("feat: new version some new file") + util.run_cli("bump", "--changelog") + out = changelog_path.read_text() + 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( + changelog_path: Path, + config_path: Path, + util: UtilFixture, +): + with config_path.open("a", encoding="utf-8") as f: + f.write('\ntag_format = "${version}custom"\n') + util.create_file_and_commit("feat: new file") + util.create_tag("v0.2.0") + util.create_file_and_commit("feat: another new file") + util.create_tag("0.2.0") + util.create_tag("random0.2.0") + # bump to 0.2.0custom + util.run_cli("bump", "--changelog", "--yes") + + util.create_file_and_commit("feat: another new file") + # bump to 0.3.0custom + util.run_cli("bump", "--changelog", "--yes") + out = changelog_path.read_text() + 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( + changelog_path: Path, + config_path: Path, + util: UtilFixture, +): + with config_path.open("a", encoding="utf-8") as f: + f.write('\ntag_format = "${version}-custom"\n') + util.create_file_and_commit("feat: new file") + util.create_tag("v0.2.0") + util.create_file_and_commit("feat: another new file") + util.create_tag("0.2.0") + util.create_tag("random0.2.0") + util.run_cli("bump", "--changelog", "--yes") + util.create_file_and_commit("feat: another new file") + util.run_cli("bump", "--changelog", "--yes") + out = changelog_path.read_text() + 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( + changelog_path: Path, + config_path: Path, + util: UtilFixture, +): + with config_path.open("a", encoding="utf-8") as f: + f.writelines( + [ + 'tag_format = "v${version}"\n', + "legacy_tag_formats = [\n", + ' "older-${version}",\n', + ' "oldest-${version}",\n', + "]\n", + ] + ) + util.create_file_and_commit("feat: new file") + util.create_tag("oldest-0.1.0") + util.create_file_and_commit("feat: new file") + util.create_tag("older-0.2.0") + util.create_file_and_commit("feat: another new file") + util.create_tag("v0.3.0") + util.create_file_and_commit("feat: another new file") + util.create_tag("not-0.3.1") + util.run_cli("bump", "--changelog", "--yes") + out = changelog_path.read_text() + 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 - create_file_and_commit("feat: new file") - testargs = [ - "cz", +@pytest.mark.usefixtures("tmp_commitizen_project") +@pytest.mark.freeze_time("2024-11-18") +def test_changelog_incremental_change_tag_format( + changelog_path: Path, + config_path: Path, + file_regression: FileRegressionFixture, + util: UtilFixture, +): + util.freezer.move_to("2024-11-18") + config = Path(config_path) + base_config = config.read_text() + config.write_text( + "\n".join( + ( + base_config, + 'tag_format = "older-${version}"', + ) + ) + ) + util.create_file_and_commit("feat: new file") + util.create_tag("older-0.1.0") + util.create_file_and_commit("feat: new file") + util.create_tag("older-0.2.0") + util.run_cli("changelog") + + config.write_text( + "\n".join( + ( + base_config, + 'tag_format = "v${version}"', + 'legacy_tag_formats = ["older-${version}"]', + ) + ) + ) + util.create_file_and_commit("feat: another new file") + util.create_tag("v0.3.0") + util.run_cli("changelog", "--incremental") + out = changelog_path.read_text() + 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( + changelog_path: Path, + config_path: Path, + capsys: pytest.CaptureFixture, + util: UtilFixture, +): + with config_path.open("a", encoding="utf-8") as f: + f.writelines( + [ + 'tag_format = "v${version}"\n', + "ignored_tag_formats = [\n", + ' "ignored",\n', + ' "ignore-${version}",\n', + "]\n", + ] + ) + util.create_file_and_commit("feat: new file") + util.create_tag("ignore-0.1.0") + util.create_file_and_commit("feat: new file") + util.create_tag("ignored") + util.create_file_and_commit("feat: another new file") + util.create_tag("v0.3.0") + util.create_file_and_commit("feat: another new file") + util.create_tag("not-ignored") + util.run_cli("bump", "--changelog", "--yes") + out = changelog_path.read_text() + assert "## ignore-0.1.0" not in out + assert "## ignored" not in out + assert "## not-ignored" not in out + assert "## v0.3.0" in out + + _, err = capsys.readouterr() + assert "Invalid version tag: 'ignore-0.1.0'" not in err + assert "Invalid version tag: 'ignored'" not in err + assert "Invalid version tag: 'not-ignored'" in err + assert "Invalid version tag: 'v0.3.0'" not in err + + +def test_changelog_template_extra_quotes( + changelog_tpl: Path, + changelog_file: Path, + util: UtilFixture, +): + changelog_tpl.write_text("{{first}} - {{second}} - {{third}}") + util.create_file_and_commit("feat: new file") + util.run_cli( "changelog", "-e", "first=no-quote", @@ -1543,111 +1553,145 @@ def test_changelog_template_extra_quotes( "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" + assert changelog_file.read_text() == "no-quote - single quotes - double quotes" @pytest.mark.parametrize( - "extra, expected", - ( + ("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="2-consecutive-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, + changelog_tpl: Path, + changelog_file: Path, extra: str, - expected, + expected: str, + util: UtilFixture, ): - project_root = Path(tmp_commitizen_project) - changelog_tpl = project_root / any_changelog_format.template changelog_tpl.write_text("{{key}}") + util.create_file_and_commit("feat: new file") + util.run_cli("changelog", "-e", extra) - 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 + assert changelog_file.read_text() == expected -@pytest.mark.parametrize("extra", ("no-equal", "", "=no-key")) +@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, + changelog_tpl: Path, extra: str, + util: UtilFixture, ): - 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) + util.create_file_and_commit("feat: new file") with pytest.raises(InvalidCommandArgumentError): - cli.main() + util.run_cli("changelog", "-e", extra) def test_export_changelog_template_from_default( - mocker: MockFixture, - tmp_commitizen_project: Path, any_changelog_format: ChangelogFormat, + util: UtilFixture, + changelog_jinja_file: Path, + repo_root: Path, ): - project_root = Path(tmp_commitizen_project) - target = project_root / "changelog.jinja" - src = Path(commitizen_init).parent / "templates" / any_changelog_format.template + src = repo_root / "commitizen" / "templates" / any_changelog_format.template - args = ["cz", "changelog", "--export-template", str(target)] + util.run_cli("changelog", "--export-template", str(changelog_jinja_file)) - mocker.patch.object(sys, "argv", args) - cli.main() - - assert target.exists() - assert target.read_text() == src.read_text() + assert changelog_jinja_file.exists() + assert changelog_jinja_file.read_text() == src.read_text() def test_export_changelog_template_from_plugin( - mocker: MockFixture, - tmp_commitizen_project: Path, + changelog_jinja_file: Path, mock_plugin: BaseCommitizen, changelog_format: ChangelogFormat, tmp_path: Path, + util: UtilFixture, ): - 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)] + util.run_cli("changelog", "--export-template", str(changelog_jinja_file)) - mocker.patch.object(sys, "argv", args) - cli.main() + assert changelog_jinja_file.exists() + assert changelog_jinja_file.read_text() == tpl - assert target.exists() - assert target.read_text() == tpl +def test_export_changelog_template_fails_when_template_has_no_filename( + mocker: MockFixture, + changelog_jinja_file: Path, + util: UtilFixture, +): + # Mock a template object with no filename + class FakeTemplate: + filename = None + + # Patch get_changelog_template to return a template without a filename + mocker.patch( + "commitizen.changelog.get_changelog_template", return_value=FakeTemplate() + ) + + with pytest.raises(NotAllowed) as exc_info: + util.run_cli("changelog", "--export-template", str(changelog_jinja_file)) -@skip_below_py_3_10 -def test_changelog_command_shows_description_when_use_help_option( - mocker: MockFixture, capsys, file_regression + assert not changelog_jinja_file.exists() + assert "Template filename is not set" in str(exc_info.value) + + +def test_changelog_template_incremental_variable( + tmp_commitizen_project: Path, + any_changelog_format: ChangelogFormat, + util: UtilFixture, + file_regression: FileRegressionFixture, ): - testargs = ["cz", "changelog", "--help"] - mocker.patch.object(sys, "argv", testargs) - with pytest.raises(SystemExit): - cli.main() + """ + Test that the changelog template is not rendered when the incremental flag is not set. + Reference: https://github.com/commitizen-tools/commitizen/discussions/1620 + """ + project_root = tmp_commitizen_project + changelog_tpl = project_root / any_changelog_format.template + changelog_tpl.write_text( + dedent(""" + {% if not incremental %} + # CHANGELOG + {% endif %} + + {% 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 %} + """) + ) + target = "CHANGELOG.md" - out, _ = capsys.readouterr() - file_regression.check(out, extension=".txt") + util.create_file_and_commit("feat(foo): new file") + util.run_cli("changelog", "--file-name", target) + out = Path(target).read_text(encoding="utf-8") + file_regression.check(out, extension=".md") + + util.create_file_and_commit("refactor(bar): another new file") + util.run_cli("changelog", "--file-name", target, "--incremental") + out = Path(target).read_text(encoding="utf-8") + file_regression.check(out, extension=".incremental.md") 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_feat_users___email_pattern_corrected_n_nBREAKING_CHANGE__migrate_by_renaming_user_to_users_n_nfooter_content_.md similarity index 100% rename from tests/commands/test_changelog_command/test_breaking_change_content_v1_beta.md rename to tests/commands/test_changelog_command/test_breaking_change_content_v1_beta_feat_users___email_pattern_corrected_n_nBREAKING_CHANGE__migrate_by_renaming_user_to_users_n_nfooter_content_.md diff --git a/tests/commands/test_changelog_command/test_breaking_change_content_v1_beta_feat_users___email_pattern_corrected_n_nbody_content_n_nBREAKING_CHANGE__migrate_by_renaming_user_to_users_.md b/tests/commands/test_changelog_command/test_breaking_change_content_v1_beta_feat_users___email_pattern_corrected_n_nbody_content_n_nBREAKING_CHANGE__migrate_by_renaming_user_to_users_.md new file mode 100644 index 0000000000..c4809739a9 --- /dev/null +++ b/tests/commands/test_changelog_command/test_breaking_change_content_v1_beta_feat_users___email_pattern_corrected_n_nbody_content_n_nBREAKING_CHANGE__migrate_by_renaming_user_to_users_.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_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 deleted file mode 100644 index dae438ca24..0000000000 --- a/tests/commands/test_changelog_command/test_changelog_command_shows_description_when_use_help_option.txt +++ /dev/null @@ -1,40 +0,0 @@ -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 TEMPLATE, -t TEMPLATE - changelog template file name (relative to the current - working directory) - --extra EXTRA, -e EXTRA - a changelog extra variable (in the form 'key=value') 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 index dca7824480..f20281df71 100644 --- 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 @@ -10,4 +10,4 @@ - mama gotta work - output glitch -## 1.0.0 (1970-01-01) +## 1.0.0 (2025-12-29) 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 index dca7824480..f20281df71 100644 --- 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 @@ -10,4 +10,4 @@ - mama gotta work - output glitch -## 1.0.0 (1970-01-01) +## 1.0.0 (2025-12-29) 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 index dca7824480..f20281df71 100644 --- 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 @@ -10,4 +10,4 @@ - mama gotta work - output glitch -## 1.0.0 (1970-01-01) +## 1.0.0 (2025-12-29) 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 0000000000..5d37333aa5 --- /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_incremental_change_tag_format.md b/tests/commands/test_changelog_command/test_changelog_incremental_change_tag_format.md new file mode 100644 index 0000000000..2f0cc2909e --- /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_newline_separates_new_content_from_old.md b/tests/commands/test_changelog_command/test_changelog_incremental_newline_separates_new_content_from_old.md new file mode 100644 index 0000000000..788105a3f6 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_incremental_newline_separates_new_content_from_old.md @@ -0,0 +1,7 @@ +Pre-existing content that should be kept + +## Unreleased + +### Feat + +- add more cat videos 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 index dca7824480..f20281df71 100644 --- 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 @@ -10,4 +10,4 @@ - mama gotta work - output glitch -## 1.0.0 (1970-01-01) +## 1.0.0 (2025-12-29) 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 index dca7824480..f20281df71 100644 --- 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 @@ -10,4 +10,4 @@ - mama gotta work - output glitch -## 1.0.0 (1970-01-01) +## 1.0.0 (2025-12-29) 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 index dca7824480..f20281df71 100644 --- 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 @@ -10,4 +10,4 @@ - mama gotta work - output glitch -## 1.0.0 (1970-01-01) +## 1.0.0 (2025-12-29) diff --git a/tests/commands/test_changelog_command/test_changelog_template_incremental_variable.incremental.md b/tests/commands/test_changelog_command/test_changelog_template_incremental_variable.incremental.md new file mode 100644 index 0000000000..4a851c6627 --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_template_incremental_variable.incremental.md @@ -0,0 +1,12 @@ +# CHANGELOG + + +## Unreleased + +### Feat + +- **foo**: new file + +### Refactor + +- **bar**: another new file diff --git a/tests/commands/test_changelog_command/test_changelog_template_incremental_variable.md b/tests/commands/test_changelog_command/test_changelog_template_incremental_variable.md new file mode 100644 index 0000000000..7c9034d27a --- /dev/null +++ b/tests/commands/test_changelog_command/test_changelog_template_incremental_variable.md @@ -0,0 +1,8 @@ +# CHANGELOG + + +## Unreleased + +### Feat + +- **foo**: new file diff --git a/tests/commands/test_changelog_command/test_invalid_subject_is_skipped.md b/tests/commands/test_changelog_command/test_invalid_subject_is_skipped.md new file mode 100644 index 0000000000..d24ea7ba7c --- /dev/null +++ b/tests/commands/test_changelog_command/test_invalid_subject_is_skipped.md @@ -0,0 +1,6 @@ +## Unreleased + +### Feat + +- a new world + diff --git a/tests/commands/test_check_command.py b/tests/commands/test_check_command.py index 47bdafd651..f3f860313d 100644 --- a/tests/commands/test_check_command.py +++ b/tests/commands/test_check_command.py @@ -1,22 +1,33 @@ from __future__ import annotations -import sys from io import StringIO +from typing import TYPE_CHECKING, Any import pytest -from pytest_mock import MockFixture -from commitizen import cli, commands, git +from commitizen import commands, git +from commitizen.cz import registry +from commitizen.cz.base import BaseCommitizen from commitizen.exceptions import ( + CommitMessageLengthExceededError, InvalidCommandArgumentError, InvalidCommitMessageError, NoCommitsFoundError, ) -from tests.utils import create_file_and_commit, skip_below_py_3_10 + +if TYPE_CHECKING: + from collections.abc import Mapping + + from pytest_mock import MockFixture, MockType + + from commitizen.config.base_config import BaseConfig + from commitizen.question import CzQuestion + from tests.utils import UtilFixture + 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", + r"refactor(cz/conventional_commit): use \S to check scope", "refactor(git): remove unnecessary dot between git range", "bump: version 1.16.3 → 1.16.4", ( @@ -57,81 +68,48 @@ 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"), +def test_check_jira_fails(mocker: MockFixture, util: UtilFixture): + mock_path = mocker.patch("commitizen.commands.check.Path") + mock_path.return_value.read_text.return_value = ( + "random message for J-2 #fake_command blah" ) with pytest.raises(InvalidCommitMessageError) as excinfo: - cli.main() + util.run_cli("-n", "cz_jira", "check", "--commit-msg-file", "some_file") 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() +@pytest.mark.parametrize( + "commit_msg", + [ + "JR-23 #command some arguments etc", + "JR-2 #command some arguments etc", + "JR-234 some text #command some arguments etc", + "JRA-23 some text #command1 args #command2 args", + ], +) +def test_check_jira_command_after_issue( + mocker: MockFixture, capsys, util: UtilFixture, commit_msg: str +): + mock_path = mocker.patch("commitizen.commands.check.Path") + mock_path.return_value.read_text.return_value = commit_msg + util.run_cli("-n", "cz_jira", "check", "--commit-msg-file", "some_file") 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() +def test_check_conventional_commit_succeeds( + mocker: MockFixture, capsys, util: UtilFixture +): + mock_path = mocker.patch("commitizen.commands.check.Path") + mock_path.return_value.read_text.return_value = "fix(scope): some commit message" + util.run_cli("check", "--commit-msg-file", "some_file") out, _ = capsys.readouterr() assert "Commit validation: successful!" in out @pytest.mark.parametrize( "commit_msg", - ( + [ "feat!(lang): removed polish language", "no conventional commit", ( @@ -139,40 +117,31 @@ def test_check_conventional_commit_succeeds(mocker: MockFixture, capsys): "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") +def test_check_no_conventional_commit(commit_msg, config, tmp_path): + tempfile = tmp_path / "temp_commit_file" + tempfile.write_text(commit_msg) - 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() + with pytest.raises(InvalidCommitMessageError): + commands.Check(config=config, arguments={"commit_msg_file": tempfile})() @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() +def test_check_conventional_commit( + commit_msg, config, success_mock: MockType, tmp_path +): + tempfile = tmp_path / "temp_commit_file" + tempfile.write_text(commit_msg) + commands.Check(config=config, arguments={"commit_msg_file": tempfile})() success_mock.assert_called_once() @@ -181,33 +150,26 @@ def test_check_command_when_commit_file_not_found(config): 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") +def test_check_a_range_of_git_commits( + config, success_mock: MockType, mocker: MockFixture +): 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() + commands.Check(config=config, arguments={"rev_range": "HEAD~10..master"})() 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() + with pytest.raises(InvalidCommitMessageError) as excinfo: + commands.Check(config=config, arguments={"rev_range": "HEAD~10..master"})() + assert "This commit does not follow rule" in str(excinfo.value) def test_check_command_with_invalid_argument(config): @@ -223,170 +185,126 @@ def test_check_command_with_invalid_argument(config): @pytest.mark.usefixtures("tmp_commitizen_project") -def test_check_command_with_empty_range(config, mocker: MockFixture): +def test_check_command_with_empty_range(config: BaseConfig, util: UtilFixture): # must initialize git with a commit - create_file_and_commit("feat: initial") - - check_cmd = commands.Check(config=config, arguments={"rev_range": "master..master"}) + util.create_file_and_commit("feat: initial") with pytest.raises(NoCommitsFoundError) as excinfo: - check_cmd() - + commands.Check(config=config, arguments={"rev_range": "master..master"})() 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 = [ + ill_formatted_commits_msgs = [ "First commit does not follow rule", "Second commit does not follow rule", - ("Third commit does not follow rule\n" "Ill-formatted commit with body"), + ("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"} + return_value=_build_fake_git_commits(ill_formatted_commits_msgs), ) with pytest.raises(InvalidCommitMessageError) as excinfo: - check_cmd() - assert all([msg in str(excinfo.value) for msg in ill_formated_commits_msgs]) + commands.Check(config=config, arguments={"rev_range": "HEAD~10..master"})() + assert all([msg in str(excinfo.value) for msg in ill_formatted_commits_msgs]) -def test_check_command_with_valid_message(config, mocker: MockFixture): - success_mock = mocker.patch("commitizen.out.success") - check_cmd = commands.Check( +def test_check_command_with_valid_message(config, success_mock: MockType): + 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"}) - +@pytest.mark.parametrize("message", ["bad commit", ""]) +def test_check_command_with_invalid_message(config, message): with pytest.raises(InvalidCommitMessageError): - check_cmd() - error_mock.assert_called_once() - + commands.Check(config=config, arguments={"message": message})() -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() +def test_check_command_with_allow_abort_arg(config, success_mock): + commands.Check(config=config, arguments={"message": "", "allow_abort": True})() success_mock.assert_called_once() -def test_check_command_with_allow_abort_config(config, mocker: MockFixture): - success_mock = mocker.patch("commitizen.out.success") +def test_check_command_with_allow_abort_config(config, success_mock): config.settings["allow_abort"] = True - check_cmd = commands.Check(config=config, arguments={"message": ""}) - - check_cmd() + commands.Check(config=config, arguments={"message": ""})() success_mock.assert_called_once() -def test_check_command_override_allow_abort_config(config, mocker: MockFixture): - error_mock = mocker.patch("commitizen.out.error") +def test_check_command_override_allow_abort_config(config): 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() + commands.Check(config=config, arguments={"message": "", "allow_abort": False})() -def test_check_command_with_allowed_prefixes_arg(config, mocker: MockFixture): - success_mock = mocker.patch("commitizen.out.success") - check_cmd = commands.Check( +def test_check_command_with_allowed_prefixes_arg(config, success_mock): + 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") +def test_check_command_with_allowed_prefixes_config(config, success_mock): config.settings["allowed_prefixes"] = ["custom!"] - check_cmd = commands.Check(config=config, arguments={"message": "custom! test"}) - - check_cmd() + commands.Check(config=config, arguments={"message": "custom! test"})() success_mock.assert_called_once() -def test_check_command_override_allowed_prefixes_config(config, mocker: MockFixture): - error_mock = mocker.patch("commitizen.out.error") +def test_check_command_override_allowed_prefixes_config(config): 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() + commands.Check( + config=config, + arguments={"message": "fixup! test", "allowed_prefixes": ["custom!"]}, + )() -def test_check_command_with_pipe_message(mocker: MockFixture, capsys): - testargs = ["cz", "check"] - mocker.patch.object(sys, "argv", testargs) +def test_check_command_with_pipe_message( + mocker: MockFixture, capsys, util: UtilFixture +): mocker.patch("sys.stdin", StringIO("fix(scope): some commit message")) - cli.main() + util.run_cli("check") 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) +def test_check_command_with_pipe_message_and_failed( + mocker: MockFixture, util: UtilFixture +): mocker.patch("sys.stdin", StringIO("bad commit message")) with pytest.raises(InvalidCommitMessageError) as excinfo: - cli.main() + util.run_cli("check") 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." - ), +def test_check_command_with_comment_in_message_file( + mocker: MockFixture, capsys, util: UtilFixture +): + mock_path = mocker.patch("commitizen.commands.check.Path") + mock_path.return_value.read_text.return_value = ( + "# <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() + util.run_cli("check", "--commit-msg-file", "some_file") out, _ = capsys.readouterr() assert "Commit validation: successful!" in out -def test_check_conventional_commit_succeed_with_git_diff(mocker, capsys): - commit_msg = ( +def test_check_conventional_commit_succeed_with_git_diff( + mocker, capsys, util: UtilFixture +): + mock_path = mocker.patch("commitizen.commands.check.Path") + mock_path.return_value.read_text.return_value = ( "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" @@ -405,25 +323,128 @@ def test_check_conventional_commit_succeed_with_git_diff(mocker, capsys): "@@ -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() + util.run_cli("check", "--commit-msg-file", "some_file") out, _ = capsys.readouterr() assert "Commit validation: successful!" in out -@skip_below_py_3_10 -def test_check_command_shows_description_when_use_help_option( - mocker: MockFixture, capsys, file_regression +def test_check_command_with_message_length_limit(config, success_mock): + message = "fix(scope): some commit message" + commands.Check( + config=config, + arguments={"message": message, "message_length_limit": len(message) + 1}, + )() + success_mock.assert_called_once() + + +def test_check_command_with_message_length_limit_exceeded(config): + message = "fix(scope): some commit message" + with pytest.raises(CommitMessageLengthExceededError): + commands.Check( + config=config, + arguments={"message": message, "message_length_limit": len(message) - 1}, + )() + + +def test_check_command_with_amend_prefix_default(config, success_mock): + commands.Check(config=config, arguments={"message": "amend! test"})() + success_mock.assert_called_once() + + +def test_check_command_with_config_message_length_limit(config, success_mock): + message = "fix(scope): some commit message" + config.settings["message_length_limit"] = len(message) + 1 + commands.Check( + config=config, + arguments={"message": message}, + )() + success_mock.assert_called_once() + + +def test_check_command_with_config_message_length_limit_exceeded(config): + message = "fix(scope): some commit message" + config.settings["message_length_limit"] = len(message) - 1 + with pytest.raises(CommitMessageLengthExceededError): + commands.Check( + config=config, + arguments={"message": message}, + )() + + +def test_check_command_cli_overrides_config_message_length_limit( + config, success_mock: MockType ): - testargs = ["cz", "check", "--help"] - mocker.patch.object(sys, "argv", testargs) - with pytest.raises(SystemExit): - cli.main() + message = "fix(scope): some commit message" + config.settings["message_length_limit"] = len(message) - 1 + for message_length_limit in [len(message) + 1, 0]: + success_mock.reset_mock() + commands.Check( + config=config, + arguments={ + "message": message, + "message_length_limit": message_length_limit, + }, + )() + success_mock.assert_called_once() + + +class ValidationCz(BaseCommitizen): + def questions(self) -> list[CzQuestion]: + return [ + {"type": "input", "name": "commit", "message": "Initial commit:\n"}, + {"type": "input", "name": "issue_nb", "message": "ABC-123"}, + ] + + def message(self, answers: Mapping[str, Any]) -> str: + return f"{answers['issue_nb']}: {answers['commit']}" + + def schema(self) -> str: + return "<issue_nb>: <commit>" + + def schema_pattern(self) -> str: + return r"^(?P<issue_nb>[A-Z]{3}-\d+): (?P<commit>.*)$" + + def example(self) -> str: + return "ABC-123: fixed a bug" + def info(self) -> str: + return "Commit message must start with an issue number like ABC-123" + + +@pytest.fixture +def use_cz_custom_validator(mocker): + new_cz = {**registry, "cz_custom_validator": ValidationCz} + mocker.patch.dict("commitizen.cz.registry", new_cz) + + +@pytest.mark.usefixtures("use_cz_custom_validator") +def test_check_command_with_custom_validator_succeed( + mocker: MockFixture, capsys, util: UtilFixture +): + mock_path = mocker.patch("commitizen.commands.check.Path") + mock_path.return_value.read_text.return_value = ( + "ABC-123: add commitizen pre-commit hook" + ) + util.run_cli( + "--name", "cz_custom_validator", "check", "--commit-msg-file", "some_file" + ) out, _ = capsys.readouterr() - file_regression.check(out, extension=".txt") + assert "Commit validation: successful!" in out + + +@pytest.mark.usefixtures("use_cz_custom_validator") +def test_check_command_with_custom_validator_failed( + mocker: MockFixture, util: UtilFixture +): + mock_path = mocker.patch("commitizen.commands.check.Path") + mock_path.return_value.read_text.return_value = ( + "123-ABC issue id has wrong format and misses colon" + ) + with pytest.raises(InvalidCommitMessageError) as excinfo: + util.run_cli( + "--name", "cz_custom_validator", "check", "--commit-msg-file", "some_file" + ) + assert "commit validation: failed!" in str(excinfo.value), ( + "Pattern validation unexpectedly passed" + ) + assert "pattern: " in str(excinfo.value), "Pattern not found in error message" 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 deleted file mode 100644 index 56e42388dc..0000000000 --- a/tests/commands/test_check_command/test_check_command_shows_description_when_use_help_option.txt +++ /dev/null @@ -1,22 +0,0 @@ -usage: cz check [-h] - [--commit-msg-file COMMIT_MSG_FILE | --rev-range REV_RANGE | -m MESSAGE] - [--allow-abort] [--allowed-prefixes [ALLOWED_PREFIXES ...]] - -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 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 diff --git a/tests/commands/test_commit_command.py b/tests/commands/test_commit_command.py index 8ae7568a9d..2322cb3cb6 100644 --- a/tests/commands/test_commit_command.py +++ b/tests/commands/test_commit_command.py @@ -1,11 +1,10 @@ -import os -import sys +from pathlib import Path from unittest.mock import ANY import pytest -from pytest_mock import MockFixture +from pytest_mock import MockFixture, MockType -from commitizen import cli, cmd, commands +from commitizen import cmd, commands from commitizen.cz.exceptions import CzException from commitizen.cz.utils import get_backup_file_path from commitizen.exceptions import ( @@ -19,73 +18,71 @@ NotAllowed, NothingToCommitError, ) -from tests.utils import skip_below_py_3_10 + + +@pytest.fixture +def commit_mock(mocker: MockFixture): + return mocker.patch( + "commitizen.git.commit", return_value=cmd.Command("success", "", b"", b"", 0) + ) + + +@pytest.fixture +def prompt_mock_feat(mocker: MockFixture): + return mocker.patch( + "questionary.prompt", + return_value={ + "prefix": "feat", + "subject": "user created", + "scope": "", + "is_breaking_change": False, + "body": "closes #21", + "footer": "", + }, + ) @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 + mocker.patch("commitizen.git.is_staging_clean", 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") +def backup_file(tmp_git_project, monkeypatch): + """Write backup message so Commit finds it when run from tmp_git_project.""" + monkeypatch.chdir(tmp_git_project) + path = get_backup_file_path() + path.write_text("backup commit", encoding="utf-8") -@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") - +@pytest.mark.usefixtures("staging_is_clean", "commit_mock", "prompt_mock_feat") +def test_commit(config, success_mock: MockType): 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) +def test_commit_backup_on_failure( + config, mocker: MockFixture, prompt_mock_feat: MockType +): + mocker.patch( + "commitizen.git.commit", return_value=cmd.Command("", "error", b"", b"", 9) + ) error_mock = mocker.patch("commitizen.out.error") + commit_cmd = commands.Commit(config, {}) + temp_file = commit_cmd.backup_file_path with pytest.raises(CommitError): - commit_cmd = commands.Commit(config, {}) - temp_file = commit_cmd.temp_file commit_cmd() - prompt_mock.assert_called_once() + prompt_mock_feat.assert_called_once() error_mock.assert_called_once() - assert os.path.isfile(temp_file) - + assert Path(temp_file).exists() -@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) +@pytest.mark.usefixtures("staging_is_clean", "commit_mock") +def test_commit_retry_fails_no_backup(config): with pytest.raises(NoCommitBackupError) as excinfo: commands.Commit(config, {"retry": True})() @@ -93,315 +90,259 @@ def test_commit_retry_fails_no_backup(config, mocker: MockFixture): @pytest.mark.usefixtures("staging_is_clean", "backup_file") -def test_commit_retry_works(config, mocker: MockFixture): +def test_commit_retry_works( + config, success_mock: MockType, mocker: MockFixture, commit_mock: MockType +): 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 + temp_file = commit_cmd.backup_file_path 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) + assert not Path(temp_file).exists() @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") - +def test_commit_retry_after_failure_no_backup( + config, success_mock: MockType, commit_mock: MockType, prompt_mock_feat: MockType +): 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() + prompt_mock_feat.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): +def test_commit_retry_after_failure_works( + config, success_mock: MockType, mocker: MockFixture, commit_mock: MockType +): 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 + temp_file = commit_cmd.backup_file_path 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) + assert not Path(temp_file).exists() @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") - +def test_commit_retry_after_failure_with_no_retry_works( + config, success_mock: MockType, commit_mock: MockType, prompt_mock_feat: MockType +): config.settings["retry_after_failure"] = True commit_cmd = commands.Commit(config, {"no_retry": True}) - temp_file = commit_cmd.temp_file + temp_file = commit_cmd.backup_file_path commit_cmd() commit_mock.assert_called_with("feat: user created\n\ncloses #21", args="") - prompt_mock.assert_called_once() + prompt_mock_feat.assert_called_once() success_mock.assert_called_once() - assert not os.path.isfile(temp_file) - + assert not Path(temp_file).exists() -@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": "", - } +@pytest.mark.usefixtures("staging_is_clean", "prompt_mock_feat") +def test_commit_command_with_dry_run_option(config): with pytest.raises(DryRunExit): - commit_cmd = commands.Commit(config, {"dry_run": True}) - commit_cmd() + commands.Commit(config, {"dry_run": True})() -@pytest.mark.usefixtures("staging_is_clean") +@pytest.mark.usefixtures("staging_is_clean", "commit_mock", "prompt_mock_feat") def test_commit_command_with_write_message_to_file_option( - config, tmp_path, mocker: MockFixture + config, tmp_path, success_mock: MockType ): 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" + assert "feat: user created" in tmp_file.read_text() -@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": "", - } - +@pytest.mark.usefixtures("staging_is_clean", "prompt_mock_feat") +def test_commit_command_with_invalid_write_message_to_file_option(config, tmp_path): with pytest.raises(NotAllowed): - commit_cmd = commands.Commit(config, {"write_message_to_file": tmp_path}) - commit_cmd() + commands.Commit(config, {"write_message_to_file": tmp_path})() -@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") - +@pytest.mark.usefixtures("staging_is_clean", "prompt_mock_feat") +def test_commit_command_with_signoff_option( + config, success_mock: MockType, commit_mock: MockType +): commands.Commit(config, {"signoff": True})() - commit_mock.assert_called_once_with(ANY, args="-- -s") + 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") - +@pytest.mark.usefixtures("staging_is_clean", "prompt_mock_feat") +def test_commit_command_with_always_signoff_enabled( + config, success_mock: MockType, commit_mock: MockType +): config.settings["always_signoff"] = True commands.Commit(config, {})() - commit_mock.assert_called_once_with(ANY, args="-- -s") + commit_mock.assert_called_once_with(ANY, args="-s") + success_mock.assert_called_once() + + +@pytest.mark.usefixtures("staging_is_clean", "prompt_mock_feat") +def test_commit_command_with_gpgsign_and_always_signoff_enabled( + config, success_mock: MockType, commit_mock: MockType +): + 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 + mocker.patch("commitizen.git.is_staging_clean", return_value=True) with pytest.raises(NothingToCommitError) as excinfo: - commit_cmd = commands.Commit(config, {}) - commit_cmd() + commands.Commit(config, {})() assert "No files added to staging!" in str(excinfo.value) +@pytest.mark.usefixtures("staging_is_clean", "prompt_mock_feat") +def test_commit_with_allow_empty(config, success_mock: MockType, commit_mock: MockType): + 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", "prompt_mock_feat") +def test_commit_with_signoff_and_allow_empty( + config, success_mock: MockType, commit_mock: MockType +): + 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): +def test_commit_when_customized_expected_raised(config, mocker: MockFixture): _err = ValueError() _err.__context__ = CzException("This is the root custom err") - prompt_mock = mocker.patch("questionary.prompt") - prompt_mock.side_effect = _err - + mocker.patch("questionary.prompt", side_effect=_err) with pytest.raises(CustomError) as excinfo: - commit_cmd = commands.Commit(config, {}) - commit_cmd() + commands.Commit(config, {})() # 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() +def test_commit_when_non_customized_expected_raised(config, mocker: MockFixture): + mocker.patch("questionary.prompt", side_effect=ValueError("error message")) + with pytest.raises(ValueError, match="error message"): + commands.Commit(config, {})() @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 - +def test_commit_when_no_user_answer(config, mocker: MockFixture): + mocker.patch("questionary.prompt", return_value=None) with pytest.raises(NoAnswersError): - commit_cmd = commands.Commit(config, {}) - commit_cmd() + commands.Commit(config, {})() -def test_commit_in_non_git_project(tmpdir, config): - with tmpdir.as_cwd(): - with pytest.raises(NotAGitProjectError): - commands.Commit(config, {}) +def test_commit_in_non_git_project(tmp_path, monkeypatch, config): + monkeypatch.chdir(tmp_path) + 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") +@pytest.mark.usefixtures("staging_is_clean", "commit_mock", "prompt_mock_feat") +def test_commit_command_with_all_option( + config, success_mock: MockType, mocker: MockFixture +): 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") +@pytest.mark.usefixtures("staging_is_clean", "prompt_mock_feat") +def test_commit_command_with_extra_args( + config, success_mock: MockType, commit_mock: MockType +): 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") +@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() + + +@pytest.mark.usefixtures("staging_is_clean", "prompt_mock_feat") +@pytest.mark.parametrize( + "out", ["no changes added to commit", "nothing added to commit"] +) +def test_commit_when_nothing_added_to_commit(config, mocker: MockFixture, out): + commit_mock = mocker.patch( + "commitizen.git.commit", + return_value=cmd.Command( + out=out, + err="", + stdout=out.encode(), + stderr=b"", + return_code=0, + ), + ) + error_mock = mocker.patch("commitizen.out.error") + + commands.Commit(config, {})() + + commit_mock.assert_called_once() + error_mock.assert_called_once_with(out) + + +@pytest.mark.usefixtures("staging_is_clean", "commit_mock") +def test_commit_command_with_config_message_length_limit( + config, success_mock: MockType, prompt_mock_feat: MockType +): + prefix = prompt_mock_feat.return_value["prefix"] + subject = prompt_mock_feat.return_value["subject"] + message_length = len(f"{prefix}: {subject}") commands.Commit(config, {"message_length_limit": message_length})() success_mock.assert_called_once() @@ -409,15 +350,20 @@ def test_commit_command_with_message_length_limit(config, mocker: MockFixture): with pytest.raises(CommitMessageLengthExceededError): commands.Commit(config, {"message_length_limit": message_length - 1})() + config.settings["message_length_limit"] = message_length + success_mock.reset_mock() + commands.Commit(config, {})() + success_mock.assert_called_once() + + config.settings["message_length_limit"] = message_length - 1 + with pytest.raises(CommitMessageLengthExceededError): + commands.Commit(config, {})() -@skip_below_py_3_10 -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() + # Test config message length limit is overridden by CLI argument + success_mock.reset_mock() + commands.Commit(config, {"message_length_limit": message_length})() + success_mock.assert_called_once() - out, _ = capsys.readouterr() - file_regression.check(out, extension=".txt") + success_mock.reset_mock() + commands.Commit(config, {"message_length_limit": 0})() + success_mock.assert_called_once() 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 deleted file mode 100644 index ebdb68446e..0000000000 --- a/tests/commands/test_commit_command/test_commit_command_shows_description_when_use_help_option.txt +++ /dev/null @@ -1,20 +0,0 @@ -usage: cz commit [-h] [--retry] [--no-retry] [--dry-run] - [--write-message-to-file FILE_PATH] [-s] [-a] - [-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. - -l MESSAGE_LENGTH_LIMIT, --message-length-limit MESSAGE_LENGTH_LIMIT - length limit of the commit message; 0 for no limit diff --git a/tests/commands/test_common_command.py b/tests/commands/test_common_command.py new file mode 100644 index 0000000000..c70bc96e94 --- /dev/null +++ b/tests/commands/test_common_command.py @@ -0,0 +1,54 @@ +import pytest +from pytest_mock import MockFixture + +from commitizen.commands import Example, Info, ListCz, Schema +from tests.utils import UtilFixture + + +@pytest.mark.parametrize( + "command", + [ + "bump", + "changelog", + "check", + "commit", + "example", + "info", + "init", + "ls", + "schema", + "version", + ], +) +@pytest.mark.usefixtures("python_version", "consistent_terminal_output") +def test_command_shows_description_when_use_help_option( + capsys, + file_regression, + command: str, + util: UtilFixture, +): + """Test that the command shows the description when the help option is used. + + Note: If the command description changes, please run `poe test:regen` to regenerate the test files. + """ + + with pytest.raises(SystemExit): + util.run_cli(command, "--help") + + out, _ = capsys.readouterr() + file_regression.check(out, extension=".txt") + + +@pytest.mark.parametrize( + "command", + [ + Example, + Info, + ListCz, + Schema, + ], +) +def test_simple_command_call_once(config, mocker: MockFixture, command): + write_mock = mocker.patch("commitizen.out.write") + command(config)() + write_mock.assert_called_once() diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_bump_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_bump_.txt new file mode 100644 index 0000000000..fa696d063d --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_bump_.txt @@ -0,0 +1,87 @@ +usage: cz bump [-h] [--dry-run] [--files-only] [--version-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 Perform a dry run, without committing or modifying + files. + --files-only Bump version in the `version_files` specified in the + configuration file only(deprecated; use --version- + files-only instead). + --version-files-only Bump version in the files from the config + --local-version Bump version only the local version portion (ignoring + the public version). + --changelog, -ch Generate the changelog for the latest version. + --no-verify Bypass the pre-commit and commit-msg hooks. + --yes Accept automatically answered questions. + --tag-format TAG_FORMAT + The format used to tag the commit and read it. Use it + in existing projects, and wrap around simple quotes. + --bump-message BUMP_MESSAGE + Template used to create the release commit, useful + when working with CI. + --prerelease {alpha,beta,rc}, -pr {alpha,beta,rc} + Type of prerelease. + --devrelease DEVRELEASE, -d DEVRELEASE + Specify non-negative integer for dev release. + --increment {MAJOR,MINOR,PATCH} + Specify the desired increment. + --increment-mode {linear,exact} + Set the method by which the new version is chosen. + 'linear' (default) resolves the next version based on + typical linear version progression, where 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, ensuring the increment + and pre-release are always honored. + --check-consistency, -cc + Check consistency among versions defined in Commitizen + configuration file and `version_files`. + --annotated-tag, -at Create annotated tag instead of lightweight one. + --annotated-tag-message 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 stdout. + --git-output-to-stderr + Redirect git output to stderr. + --retry Retry commit if it fails for the first time. + --major-version-zero Keep major version at zero, even for breaking changes. + --template TEMPLATE, -t TEMPLATE + Changelog template file name (relative to the current + working directory). + --extra EXTRA, -e EXTRA + Changelog extra variables (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` instead. + --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_common_command/test_command_shows_description_when_use_help_option_py_3_10_changelog_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_changelog_.txt new file mode 100644 index 0000000000..2d1135af74 --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_changelog_.txt @@ -0,0 +1,42 @@ +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] [--tag-format TAG_FORMAT] + [rev_range] + +Generate changelog (note that it will overwrite existing files) + +positional arguments: + rev_range Generate 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 versions. + --incremental Generate changelog from the 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 beginning. + --merge-prerelease Collect all changes from prereleases into the 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 TEMPLATE, -t TEMPLATE + Changelog template file name (relative to the current + working directory). + --extra EXTRA, -e EXTRA + Changelog extra variables (in the form 'key=value'). + --tag-format TAG_FORMAT + The format of the tag, wrap around simple quotes. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_check_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_check_.txt new file mode 100644 index 0000000000..144e91b61a --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_check_.txt @@ -0,0 +1,29 @@ +usage: cz check [-h] + [--commit-msg-file COMMIT_MSG_FILE | --rev-range REV_RANGE | -d | -m MESSAGE] + [--allow-abort] [--allowed-prefixes [ALLOWED_PREFIXES ...]] + [-l MESSAGE_LENGTH_LIMIT] + +Validate 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 temporary file that contains + the commit message. Use it in a git hook script: + MSG_FILE=$1. + --rev-range REV_RANGE + Validate the commits in the given range of git rev, + e.g., master..HEAD. + -d, --use-default-range + Validate the commits from the default branch to HEAD, + e.g., refs/remotes/origin/master..HEAD. + -m MESSAGE, --message MESSAGE + Validate the given commit message. + --allow-abort Allow empty commit messages, which typically abort a + commit. + --allowed-prefixes [ALLOWED_PREFIXES ...] + Skip validation for commit messages that start with + the specified prefixes. + -l MESSAGE_LENGTH_LIMIT, --message-length-limit MESSAGE_LENGTH_LIMIT + Restrict the length of the **first line** of the + commit message; 0 for no limit. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_commit_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_commit_.txt new file mode 100644 index 0000000000..bd256ccf8c --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_commit_.txt @@ -0,0 +1,25 @@ +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 the last commit. + --no-retry Skip retry if --retry or `retry_after_failure` is set + to true. + --dry-run Perform a dry run, without committing or modifying + files. + --write-message-to-file FILE_PATH + Write message to FILE_PATH before committing (can be + used with --dry-run). + -s, --signoff Deprecated, use `cz commit -- -s` instead. + -a, --all 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 MESSAGE_LENGTH_LIMIT + Set the length limit of the commit message; 0 for no + limit. + -- Positional arguments separator (recommended). diff --git a/tests/commands/test_example_command/test_example_command_shows_description_when_use_help_option.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_example_.txt similarity index 80% rename from tests/commands/test_example_command/test_example_command_shows_description_when_use_help_option.txt rename to tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_example_.txt index b9bf7f84fc..8a0f1c9d94 100644 --- a/tests/commands/test_example_command/test_example_command_shows_description_when_use_help_option.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_example_.txt @@ -1,6 +1,6 @@ usage: cz example [-h] -show commit example +Show commit example options: -h, --help show this help message and exit diff --git a/tests/commands/test_info_command/test_info_command_shows_description_when_use_help_option.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_info_.txt similarity index 71% rename from tests/commands/test_info_command/test_info_command_shows_description_when_use_help_option.txt rename to tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_info_.txt index 99b1ba8a4a..ed5ae2522e 100644 --- a/tests/commands/test_info_command/test_info_command_shows_description_when_use_help_option.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_info_.txt @@ -1,6 +1,6 @@ usage: cz info [-h] -show information about the cz +Show information about the cz options: -h, --help show this help message and exit diff --git a/tests/commands/test_init_command/test_init_command_shows_description_when_use_help_option.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_init_.txt similarity index 68% rename from tests/commands/test_init_command/test_init_command_shows_description_when_use_help_option.txt rename to tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_init_.txt index 0f72042f88..546ab51cb3 100644 --- a/tests/commands/test_init_command/test_init_command_shows_description_when_use_help_option.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_init_.txt @@ -1,6 +1,6 @@ usage: cz init [-h] -init commitizen configuration +Initialize commitizen configuration options: -h, --help show this help message and exit diff --git a/tests/commands/test_ls_command/test_ls_command_shows_description_when_use_help_option.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_ls_.txt similarity index 73% rename from tests/commands/test_ls_command/test_ls_command_shows_description_when_use_help_option.txt rename to tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_ls_.txt index 5fa8fe1f79..253da1722c 100644 --- a/tests/commands/test_ls_command/test_ls_command_shows_description_when_use_help_option.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_ls_.txt @@ -1,6 +1,6 @@ usage: cz ls [-h] -show available commitizens +Show available Commitizens options: -h, --help show this help message and exit diff --git a/tests/commands/test_schema_command/test_schema_command_shows_description_when_use_help_option.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_schema_.txt similarity index 80% rename from tests/commands/test_schema_command/test_schema_command_shows_description_when_use_help_option.txt rename to tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_schema_.txt index 6666db4d41..dd05ead81b 100644 --- a/tests/commands/test_schema_command/test_schema_command_shows_description_when_use_help_option.txt +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_schema_.txt @@ -1,6 +1,6 @@ usage: cz schema [-h] -show commit schema +Show commit schema options: -h, --help show this help message and exit diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_version_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_version_.txt new file mode 100644 index 0000000000..5085d0fd3c --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_10_version_.txt @@ -0,0 +1,18 @@ +usage: cz version [-h] [-r | -p | -c | -v] [--major | --minor | --tag] + +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 Output the system information for reporting bugs. + -p, --project Output the version of the current project. + -c, --commitizen Output the version of the installed commitizen. + -v, --verbose Output the version of both the installed commitizen and + the current project. + --major Output just the major version. Must be used with --project + or --verbose. + --minor Output just the minor version. Must be used with --project + or --verbose. + --tag get the version with tag prefix. Need to be used with + --project or --verbose. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_bump_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_bump_.txt new file mode 100644 index 0000000000..fa696d063d --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_bump_.txt @@ -0,0 +1,87 @@ +usage: cz bump [-h] [--dry-run] [--files-only] [--version-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 Perform a dry run, without committing or modifying + files. + --files-only Bump version in the `version_files` specified in the + configuration file only(deprecated; use --version- + files-only instead). + --version-files-only Bump version in the files from the config + --local-version Bump version only the local version portion (ignoring + the public version). + --changelog, -ch Generate the changelog for the latest version. + --no-verify Bypass the pre-commit and commit-msg hooks. + --yes Accept automatically answered questions. + --tag-format TAG_FORMAT + The format used to tag the commit and read it. Use it + in existing projects, and wrap around simple quotes. + --bump-message BUMP_MESSAGE + Template used to create the release commit, useful + when working with CI. + --prerelease {alpha,beta,rc}, -pr {alpha,beta,rc} + Type of prerelease. + --devrelease DEVRELEASE, -d DEVRELEASE + Specify non-negative integer for dev release. + --increment {MAJOR,MINOR,PATCH} + Specify the desired increment. + --increment-mode {linear,exact} + Set the method by which the new version is chosen. + 'linear' (default) resolves the next version based on + typical linear version progression, where 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, ensuring the increment + and pre-release are always honored. + --check-consistency, -cc + Check consistency among versions defined in Commitizen + configuration file and `version_files`. + --annotated-tag, -at Create annotated tag instead of lightweight one. + --annotated-tag-message 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 stdout. + --git-output-to-stderr + Redirect git output to stderr. + --retry Retry commit if it fails for the first time. + --major-version-zero Keep major version at zero, even for breaking changes. + --template TEMPLATE, -t TEMPLATE + Changelog template file name (relative to the current + working directory). + --extra EXTRA, -e EXTRA + Changelog extra variables (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` instead. + --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_common_command/test_command_shows_description_when_use_help_option_py_3_11_changelog_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_changelog_.txt new file mode 100644 index 0000000000..2d1135af74 --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_changelog_.txt @@ -0,0 +1,42 @@ +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] [--tag-format TAG_FORMAT] + [rev_range] + +Generate changelog (note that it will overwrite existing files) + +positional arguments: + rev_range Generate 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 versions. + --incremental Generate changelog from the 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 beginning. + --merge-prerelease Collect all changes from prereleases into the 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 TEMPLATE, -t TEMPLATE + Changelog template file name (relative to the current + working directory). + --extra EXTRA, -e EXTRA + Changelog extra variables (in the form 'key=value'). + --tag-format TAG_FORMAT + The format of the tag, wrap around simple quotes. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_check_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_check_.txt new file mode 100644 index 0000000000..144e91b61a --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_check_.txt @@ -0,0 +1,29 @@ +usage: cz check [-h] + [--commit-msg-file COMMIT_MSG_FILE | --rev-range REV_RANGE | -d | -m MESSAGE] + [--allow-abort] [--allowed-prefixes [ALLOWED_PREFIXES ...]] + [-l MESSAGE_LENGTH_LIMIT] + +Validate 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 temporary file that contains + the commit message. Use it in a git hook script: + MSG_FILE=$1. + --rev-range REV_RANGE + Validate the commits in the given range of git rev, + e.g., master..HEAD. + -d, --use-default-range + Validate the commits from the default branch to HEAD, + e.g., refs/remotes/origin/master..HEAD. + -m MESSAGE, --message MESSAGE + Validate the given commit message. + --allow-abort Allow empty commit messages, which typically abort a + commit. + --allowed-prefixes [ALLOWED_PREFIXES ...] + Skip validation for commit messages that start with + the specified prefixes. + -l MESSAGE_LENGTH_LIMIT, --message-length-limit MESSAGE_LENGTH_LIMIT + Restrict the length of the **first line** of the + commit message; 0 for no limit. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_commit_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_commit_.txt new file mode 100644 index 0000000000..bd256ccf8c --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_commit_.txt @@ -0,0 +1,25 @@ +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 the last commit. + --no-retry Skip retry if --retry or `retry_after_failure` is set + to true. + --dry-run Perform a dry run, without committing or modifying + files. + --write-message-to-file FILE_PATH + Write message to FILE_PATH before committing (can be + used with --dry-run). + -s, --signoff Deprecated, use `cz commit -- -s` instead. + -a, --all 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 MESSAGE_LENGTH_LIMIT + Set the length limit of the commit message; 0 for no + limit. + -- Positional arguments separator (recommended). diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_example_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_example_.txt new file mode 100644 index 0000000000..8a0f1c9d94 --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_example_.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_common_command/test_command_shows_description_when_use_help_option_py_3_11_info_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_info_.txt new file mode 100644 index 0000000000..ed5ae2522e --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_info_.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_common_command/test_command_shows_description_when_use_help_option_py_3_11_init_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_init_.txt new file mode 100644 index 0000000000..546ab51cb3 --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_init_.txt @@ -0,0 +1,6 @@ +usage: cz init [-h] + +Initialize commitizen configuration + +options: + -h, --help show this help message and exit diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_ls_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_ls_.txt new file mode 100644 index 0000000000..253da1722c --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_ls_.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_common_command/test_command_shows_description_when_use_help_option_py_3_11_schema_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_schema_.txt new file mode 100644 index 0000000000..dd05ead81b --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_schema_.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_common_command/test_command_shows_description_when_use_help_option_py_3_11_version_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_version_.txt new file mode 100644 index 0000000000..5085d0fd3c --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_11_version_.txt @@ -0,0 +1,18 @@ +usage: cz version [-h] [-r | -p | -c | -v] [--major | --minor | --tag] + +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 Output the system information for reporting bugs. + -p, --project Output the version of the current project. + -c, --commitizen Output the version of the installed commitizen. + -v, --verbose Output the version of both the installed commitizen and + the current project. + --major Output just the major version. Must be used with --project + or --verbose. + --minor Output just the minor version. Must be used with --project + or --verbose. + --tag get the version with tag prefix. Need to be used with + --project or --verbose. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_bump_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_bump_.txt new file mode 100644 index 0000000000..fa696d063d --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_bump_.txt @@ -0,0 +1,87 @@ +usage: cz bump [-h] [--dry-run] [--files-only] [--version-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 Perform a dry run, without committing or modifying + files. + --files-only Bump version in the `version_files` specified in the + configuration file only(deprecated; use --version- + files-only instead). + --version-files-only Bump version in the files from the config + --local-version Bump version only the local version portion (ignoring + the public version). + --changelog, -ch Generate the changelog for the latest version. + --no-verify Bypass the pre-commit and commit-msg hooks. + --yes Accept automatically answered questions. + --tag-format TAG_FORMAT + The format used to tag the commit and read it. Use it + in existing projects, and wrap around simple quotes. + --bump-message BUMP_MESSAGE + Template used to create the release commit, useful + when working with CI. + --prerelease {alpha,beta,rc}, -pr {alpha,beta,rc} + Type of prerelease. + --devrelease DEVRELEASE, -d DEVRELEASE + Specify non-negative integer for dev release. + --increment {MAJOR,MINOR,PATCH} + Specify the desired increment. + --increment-mode {linear,exact} + Set the method by which the new version is chosen. + 'linear' (default) resolves the next version based on + typical linear version progression, where 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, ensuring the increment + and pre-release are always honored. + --check-consistency, -cc + Check consistency among versions defined in Commitizen + configuration file and `version_files`. + --annotated-tag, -at Create annotated tag instead of lightweight one. + --annotated-tag-message 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 stdout. + --git-output-to-stderr + Redirect git output to stderr. + --retry Retry commit if it fails for the first time. + --major-version-zero Keep major version at zero, even for breaking changes. + --template TEMPLATE, -t TEMPLATE + Changelog template file name (relative to the current + working directory). + --extra EXTRA, -e EXTRA + Changelog extra variables (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` instead. + --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_common_command/test_command_shows_description_when_use_help_option_py_3_12_changelog_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_changelog_.txt new file mode 100644 index 0000000000..2d1135af74 --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_changelog_.txt @@ -0,0 +1,42 @@ +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] [--tag-format TAG_FORMAT] + [rev_range] + +Generate changelog (note that it will overwrite existing files) + +positional arguments: + rev_range Generate 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 versions. + --incremental Generate changelog from the 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 beginning. + --merge-prerelease Collect all changes from prereleases into the 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 TEMPLATE, -t TEMPLATE + Changelog template file name (relative to the current + working directory). + --extra EXTRA, -e EXTRA + Changelog extra variables (in the form 'key=value'). + --tag-format TAG_FORMAT + The format of the tag, wrap around simple quotes. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_check_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_check_.txt new file mode 100644 index 0000000000..144e91b61a --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_check_.txt @@ -0,0 +1,29 @@ +usage: cz check [-h] + [--commit-msg-file COMMIT_MSG_FILE | --rev-range REV_RANGE | -d | -m MESSAGE] + [--allow-abort] [--allowed-prefixes [ALLOWED_PREFIXES ...]] + [-l MESSAGE_LENGTH_LIMIT] + +Validate 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 temporary file that contains + the commit message. Use it in a git hook script: + MSG_FILE=$1. + --rev-range REV_RANGE + Validate the commits in the given range of git rev, + e.g., master..HEAD. + -d, --use-default-range + Validate the commits from the default branch to HEAD, + e.g., refs/remotes/origin/master..HEAD. + -m MESSAGE, --message MESSAGE + Validate the given commit message. + --allow-abort Allow empty commit messages, which typically abort a + commit. + --allowed-prefixes [ALLOWED_PREFIXES ...] + Skip validation for commit messages that start with + the specified prefixes. + -l MESSAGE_LENGTH_LIMIT, --message-length-limit MESSAGE_LENGTH_LIMIT + Restrict the length of the **first line** of the + commit message; 0 for no limit. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_commit_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_commit_.txt new file mode 100644 index 0000000000..bd256ccf8c --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_commit_.txt @@ -0,0 +1,25 @@ +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 the last commit. + --no-retry Skip retry if --retry or `retry_after_failure` is set + to true. + --dry-run Perform a dry run, without committing or modifying + files. + --write-message-to-file FILE_PATH + Write message to FILE_PATH before committing (can be + used with --dry-run). + -s, --signoff Deprecated, use `cz commit -- -s` instead. + -a, --all 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 MESSAGE_LENGTH_LIMIT + Set the length limit of the commit message; 0 for no + limit. + -- Positional arguments separator (recommended). diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_example_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_example_.txt new file mode 100644 index 0000000000..8a0f1c9d94 --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_example_.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_common_command/test_command_shows_description_when_use_help_option_py_3_12_info_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_info_.txt new file mode 100644 index 0000000000..ed5ae2522e --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_info_.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_common_command/test_command_shows_description_when_use_help_option_py_3_12_init_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_init_.txt new file mode 100644 index 0000000000..546ab51cb3 --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_init_.txt @@ -0,0 +1,6 @@ +usage: cz init [-h] + +Initialize commitizen configuration + +options: + -h, --help show this help message and exit diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_ls_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_ls_.txt new file mode 100644 index 0000000000..253da1722c --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_ls_.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_common_command/test_command_shows_description_when_use_help_option_py_3_12_schema_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_schema_.txt new file mode 100644 index 0000000000..dd05ead81b --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_schema_.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_common_command/test_command_shows_description_when_use_help_option_py_3_12_version_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_version_.txt new file mode 100644 index 0000000000..5085d0fd3c --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_12_version_.txt @@ -0,0 +1,18 @@ +usage: cz version [-h] [-r | -p | -c | -v] [--major | --minor | --tag] + +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 Output the system information for reporting bugs. + -p, --project Output the version of the current project. + -c, --commitizen Output the version of the installed commitizen. + -v, --verbose Output the version of both the installed commitizen and + the current project. + --major Output just the major version. Must be used with --project + or --verbose. + --minor Output just the minor version. Must be used with --project + or --verbose. + --tag get the version with tag prefix. Need to be used with + --project or --verbose. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_bump_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_bump_.txt new file mode 100644 index 0000000000..8e4c863585 --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_bump_.txt @@ -0,0 +1,86 @@ +usage: cz bump [-h] [--dry-run] [--files-only] [--version-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 Perform a dry run, without committing or modifying + files. + --files-only Bump version in the `version_files` specified in the + configuration file only(deprecated; use --version- + files-only instead). + --version-files-only Bump version in the files from the config + --local-version Bump version only the local version portion (ignoring + the public version). + --changelog, -ch Generate the changelog for the latest version. + --no-verify Bypass the pre-commit and commit-msg hooks. + --yes Accept automatically answered questions. + --tag-format TAG_FORMAT + The format used to tag the commit and read it. Use it + in existing projects, and 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} + Type of prerelease. + --devrelease, -d DEVRELEASE + Specify non-negative integer for dev release. + --increment {MAJOR,MINOR,PATCH} + Specify the desired increment. + --increment-mode {linear,exact} + Set the method by which the new version is chosen. + 'linear' (default) resolves the next version based on + typical linear version progression, where 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, ensuring the increment + and pre-release are always honored. + --check-consistency, -cc + Check consistency among versions defined in Commitizen + configuration file 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 stdout. + --git-output-to-stderr + Redirect git output to stderr. + --retry Retry commit if it fails for the first 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 Changelog extra variables (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` instead. + --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_common_command/test_command_shows_description_when_use_help_option_py_3_13_changelog_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_changelog_.txt new file mode 100644 index 0000000000..50ab468d64 --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_changelog_.txt @@ -0,0 +1,41 @@ +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] [--tag-format TAG_FORMAT] + [rev_range] + +Generate changelog (note that it will overwrite existing files) + +positional arguments: + rev_range Generate 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 versions. + --incremental Generate changelog from the 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 beginning. + --merge-prerelease Collect all changes from prereleases into the 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 Changelog extra variables (in the form 'key=value'). + --tag-format TAG_FORMAT + The format of the tag, wrap around simple quotes. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_check_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_check_.txt new file mode 100644 index 0000000000..6f8297e1ee --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_check_.txt @@ -0,0 +1,29 @@ +usage: cz check [-h] [--commit-msg-file COMMIT_MSG_FILE | + --rev-range REV_RANGE | -d | -m MESSAGE] [--allow-abort] + [--allowed-prefixes [ALLOWED_PREFIXES ...]] + [-l MESSAGE_LENGTH_LIMIT] + +Validate 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 temporary file that contains + the commit message. Use it in a git hook script: + MSG_FILE=$1. + --rev-range REV_RANGE + Validate the commits in the given range of git rev, + e.g., master..HEAD. + -d, --use-default-range + Validate the commits from the default branch to HEAD, + e.g., refs/remotes/origin/master..HEAD. + -m, --message MESSAGE + Validate the given commit message. + --allow-abort Allow empty commit messages, which typically abort a + commit. + --allowed-prefixes [ALLOWED_PREFIXES ...] + Skip validation for commit messages that start with + the specified prefixes. + -l, --message-length-limit MESSAGE_LENGTH_LIMIT + Restrict the length of the **first line** of the + commit message; 0 for no limit. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_commit_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_commit_.txt new file mode 100644 index 0000000000..cbd5780f6d --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_commit_.txt @@ -0,0 +1,25 @@ +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 the last commit. + --no-retry Skip retry if --retry or `retry_after_failure` is set + to true. + --dry-run Perform a dry run, without committing or modifying + files. + --write-message-to-file FILE_PATH + Write message to FILE_PATH before committing (can be + used with --dry-run). + -s, --signoff Deprecated, use `cz commit -- -s` instead. + -a, --all 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 + Set the length limit of the commit message; 0 for no + limit. + -- Positional arguments separator (recommended). diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_example_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_example_.txt new file mode 100644 index 0000000000..8a0f1c9d94 --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_example_.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_common_command/test_command_shows_description_when_use_help_option_py_3_13_info_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_info_.txt new file mode 100644 index 0000000000..ed5ae2522e --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_info_.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_common_command/test_command_shows_description_when_use_help_option_py_3_13_init_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_init_.txt new file mode 100644 index 0000000000..546ab51cb3 --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_init_.txt @@ -0,0 +1,6 @@ +usage: cz init [-h] + +Initialize commitizen configuration + +options: + -h, --help show this help message and exit diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_ls_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_ls_.txt new file mode 100644 index 0000000000..253da1722c --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_ls_.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_common_command/test_command_shows_description_when_use_help_option_py_3_13_schema_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_schema_.txt new file mode 100644 index 0000000000..dd05ead81b --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_schema_.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_common_command/test_command_shows_description_when_use_help_option_py_3_13_version_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_version_.txt new file mode 100644 index 0000000000..5085d0fd3c --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_13_version_.txt @@ -0,0 +1,18 @@ +usage: cz version [-h] [-r | -p | -c | -v] [--major | --minor | --tag] + +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 Output the system information for reporting bugs. + -p, --project Output the version of the current project. + -c, --commitizen Output the version of the installed commitizen. + -v, --verbose Output the version of both the installed commitizen and + the current project. + --major Output just the major version. Must be used with --project + or --verbose. + --minor Output just the minor version. Must be used with --project + or --verbose. + --tag get the version with tag prefix. Need to be used with + --project or --verbose. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_bump_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_bump_.txt new file mode 100644 index 0000000000..8e4c863585 --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_bump_.txt @@ -0,0 +1,86 @@ +usage: cz bump [-h] [--dry-run] [--files-only] [--version-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 Perform a dry run, without committing or modifying + files. + --files-only Bump version in the `version_files` specified in the + configuration file only(deprecated; use --version- + files-only instead). + --version-files-only Bump version in the files from the config + --local-version Bump version only the local version portion (ignoring + the public version). + --changelog, -ch Generate the changelog for the latest version. + --no-verify Bypass the pre-commit and commit-msg hooks. + --yes Accept automatically answered questions. + --tag-format TAG_FORMAT + The format used to tag the commit and read it. Use it + in existing projects, and 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} + Type of prerelease. + --devrelease, -d DEVRELEASE + Specify non-negative integer for dev release. + --increment {MAJOR,MINOR,PATCH} + Specify the desired increment. + --increment-mode {linear,exact} + Set the method by which the new version is chosen. + 'linear' (default) resolves the next version based on + typical linear version progression, where 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, ensuring the increment + and pre-release are always honored. + --check-consistency, -cc + Check consistency among versions defined in Commitizen + configuration file 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 stdout. + --git-output-to-stderr + Redirect git output to stderr. + --retry Retry commit if it fails for the first 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 Changelog extra variables (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` instead. + --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_common_command/test_command_shows_description_when_use_help_option_py_3_14_changelog_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_changelog_.txt new file mode 100644 index 0000000000..50ab468d64 --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_changelog_.txt @@ -0,0 +1,41 @@ +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] [--tag-format TAG_FORMAT] + [rev_range] + +Generate changelog (note that it will overwrite existing files) + +positional arguments: + rev_range Generate 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 versions. + --incremental Generate changelog from the 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 beginning. + --merge-prerelease Collect all changes from prereleases into the 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 Changelog extra variables (in the form 'key=value'). + --tag-format TAG_FORMAT + The format of the tag, wrap around simple quotes. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_check_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_check_.txt new file mode 100644 index 0000000000..6f8297e1ee --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_check_.txt @@ -0,0 +1,29 @@ +usage: cz check [-h] [--commit-msg-file COMMIT_MSG_FILE | + --rev-range REV_RANGE | -d | -m MESSAGE] [--allow-abort] + [--allowed-prefixes [ALLOWED_PREFIXES ...]] + [-l MESSAGE_LENGTH_LIMIT] + +Validate 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 temporary file that contains + the commit message. Use it in a git hook script: + MSG_FILE=$1. + --rev-range REV_RANGE + Validate the commits in the given range of git rev, + e.g., master..HEAD. + -d, --use-default-range + Validate the commits from the default branch to HEAD, + e.g., refs/remotes/origin/master..HEAD. + -m, --message MESSAGE + Validate the given commit message. + --allow-abort Allow empty commit messages, which typically abort a + commit. + --allowed-prefixes [ALLOWED_PREFIXES ...] + Skip validation for commit messages that start with + the specified prefixes. + -l, --message-length-limit MESSAGE_LENGTH_LIMIT + Restrict the length of the **first line** of the + commit message; 0 for no limit. diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_commit_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_commit_.txt new file mode 100644 index 0000000000..cbd5780f6d --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_commit_.txt @@ -0,0 +1,25 @@ +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 the last commit. + --no-retry Skip retry if --retry or `retry_after_failure` is set + to true. + --dry-run Perform a dry run, without committing or modifying + files. + --write-message-to-file FILE_PATH + Write message to FILE_PATH before committing (can be + used with --dry-run). + -s, --signoff Deprecated, use `cz commit -- -s` instead. + -a, --all 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 + Set the length limit of the commit message; 0 for no + limit. + -- Positional arguments separator (recommended). diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_example_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_example_.txt new file mode 100644 index 0000000000..8a0f1c9d94 --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_example_.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_common_command/test_command_shows_description_when_use_help_option_py_3_14_info_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_info_.txt new file mode 100644 index 0000000000..ed5ae2522e --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_info_.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_common_command/test_command_shows_description_when_use_help_option_py_3_14_init_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_init_.txt new file mode 100644 index 0000000000..546ab51cb3 --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_init_.txt @@ -0,0 +1,6 @@ +usage: cz init [-h] + +Initialize commitizen configuration + +options: + -h, --help show this help message and exit diff --git a/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_ls_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_ls_.txt new file mode 100644 index 0000000000..253da1722c --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_ls_.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_common_command/test_command_shows_description_when_use_help_option_py_3_14_schema_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_schema_.txt new file mode 100644 index 0000000000..dd05ead81b --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_schema_.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_common_command/test_command_shows_description_when_use_help_option_py_3_14_version_.txt b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_version_.txt new file mode 100644 index 0000000000..5085d0fd3c --- /dev/null +++ b/tests/commands/test_common_command/test_command_shows_description_when_use_help_option_py_3_14_version_.txt @@ -0,0 +1,18 @@ +usage: cz version [-h] [-r | -p | -c | -v] [--major | --minor | --tag] + +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 Output the system information for reporting bugs. + -p, --project Output the version of the current project. + -c, --commitizen Output the version of the installed commitizen. + -v, --verbose Output the version of both the installed commitizen and + the current project. + --major Output just the major version. Must be used with --project + or --verbose. + --minor Output just the minor version. Must be used with --project + or --verbose. + --tag get the version with tag prefix. Need to be used with + --project or --verbose. diff --git a/tests/commands/test_example_command.py b/tests/commands/test_example_command.py deleted file mode 100644 index 0521679f1c..0000000000 --- a/tests/commands/test_example_command.py +++ /dev/null @@ -1,26 +0,0 @@ -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_info_command.py b/tests/commands/test_info_command.py deleted file mode 100644 index 2bd1553679..0000000000 --- a/tests/commands/test_info_command.py +++ /dev/null @@ -1,26 +0,0 @@ -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_init_command.py b/tests/commands/test_init_command.py index ea18e89a2b..db47fd064a 100644 --- a/tests/commands/test_init_command.py +++ b/tests/commands/test_init_command.py @@ -1,18 +1,20 @@ from __future__ import annotations import json -import os -import sys -from typing import Any +from pathlib import Path +from typing import TYPE_CHECKING, Any import pytest import yaml -from pytest_mock import MockFixture -from commitizen import cli, commands +from commitizen import cmd, commands from commitizen.__version__ import __version__ from commitizen.exceptions import InitFailedError, NoAnswersError -from tests.utils import skip_below_py_3_10 + +if TYPE_CHECKING: + from pytest_mock import MockFixture + + from commitizen.config.base_config import BaseConfig class FakeQuestion: @@ -32,7 +34,7 @@ def unsafe_ask(self): "rev": f"v{__version__}", "hooks": [ {"id": "commitizen"}, - {"id": "commitizen-branch", "stages": ["push"]}, + {"id": "commitizen-branch", "stages": ["pre-push"]}, ], } @@ -58,7 +60,9 @@ def unsafe_ask(self): } -def test_init_without_setup_pre_commit_hook(tmpdir, mocker: MockFixture, config): +def test_init_without_setup_pre_commit_hook( + tmp_path, monkeypatch, mocker: MockFixture, config: BaseConfig +): mocker.patch( "questionary.select", side_effect=[ @@ -72,28 +76,28 @@ def test_init_without_setup_pre_commit_hook(tmpdir, mocker: MockFixture, config) mocker.patch("questionary.text", return_value=FakeQuestion("$version")) # Return None to skip hook installation mocker.patch("questionary.checkbox", return_value=FakeQuestion(None)) + monkeypatch.chdir(tmp_path) + commands.Init(config)() - 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 + config_data = Path("pyproject.toml").read_text(encoding="utf-8") + assert config_data == expected_config - assert not os.path.isfile(pre_commit_config_filename) + assert not Path(pre_commit_config_filename).exists() -def test_init_when_config_already_exists(config, capsys): +def test_init_when_config_already_exists(config: BaseConfig, capsys): # Set config path - path = os.sep.join(["tests", "pyproject.toml"]) - config.add_path(path) + path = Path("tests") / "pyproject.toml" + config.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): +def test_init_without_choosing_tag( + config: BaseConfig, mocker: MockFixture, tmp_path, monkeypatch +): mocker.patch( "commitizen.commands.init.get_tag_names", return_value=["0.0.2", "0.0.1"] ) @@ -110,32 +114,26 @@ def test_init_without_choosing_tag(config, mocker: MockFixture, tmpdir): 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 + monkeypatch.chdir(tmp_path) + with pytest.raises(NoAnswersError): + commands.Init(config)() -@pytest.fixture(scope="function") +@pytest.fixture def pre_commit_installed(mocker: MockFixture): # Assume the `pre-commit` is installed mocker.patch( - "commitizen.commands.init.ProjectInfo.is_pre_commit_installed", + "commitizen.project_info.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, + "commitizen.cmd.run", + return_value=cmd.Command("0.0.1", "", b"", b"", 0), ) -@pytest.fixture(scope="function", params=["pyproject.toml", ".cz.json", ".cz.yaml"]) +@pytest.fixture(params=["pyproject.toml", ".cz.json", ".cz.yaml"]) def default_choice(request, mocker: MockFixture): mocker.patch( "questionary.select", @@ -152,117 +150,336 @@ def default_choice(request, mocker: MockFixture): "questionary.checkbox", return_value=FakeQuestion(["commit-msg", "pre-push"]), ) - yield request.param + return request.param -def check_cz_config(config: str): +def check_cz_config(config_filepath: 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 + content = Path(config_filepath).read_text(encoding="utf-8") + if "json" in config_filepath: + assert json.loads(content) == EXPECTED_DICT_CONFIG + elif "yaml" in config_filepath: + assert yaml.load(content, Loader=yaml.FullLoader) == EXPECTED_DICT_CONFIG + else: + assert content == 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()) + pre_commit_config_data = yaml.safe_load( + Path(pre_commit_config_filename).read_text(encoding="utf-8") + ) 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_no_existing_pre_commit_config( + self, default_choice: str, tmp_path, monkeypatch, config: BaseConfig + ): + monkeypatch.chdir(tmp_path) + 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("") + def test_empty_pre_commit_config( + self, default_choice: str, tmp_path, monkeypatch, config: BaseConfig + ): + monkeypatch.chdir(tmp_path) + p = tmp_path / pre_commit_config_filename + p.write_text("") - commands.Init(config)() - check_cz_config(default_choice) - check_pre_commit_config([cz_hook_config]) + 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): + def test_pre_commit_config_without_cz_hook( + self, default_choice: str, tmp_path, monkeypatch, config: BaseConfig + ): 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]})) + monkeypatch.chdir(tmp_path) + p = tmp_path / pre_commit_config_filename + p.write_text(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]) + 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]})) + def test_cz_hook_exists_in_pre_commit_config( + self, default_choice: str, tmp_path, monkeypatch, config: BaseConfig + ): + monkeypatch.chdir(tmp_path) + p = tmp_path / pre_commit_config_filename + p.write_text(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]) + commands.Init(config)() + check_cz_config(default_choice) + # check that config is not duplicated + check_pre_commit_config([cz_hook_config]) class TestNoPreCommitInstalled: + @pytest.mark.usefixtures("default_choice") def test_pre_commit_not_installed( - _, mocker: MockFixture, config, default_choice, tmpdir + self, mocker: MockFixture, config: BaseConfig, tmp_path, monkeypatch ): # Assume `pre-commit` is not installed mocker.patch( - "commitizen.commands.init.ProjectInfo.is_pre_commit_installed", + "commitizen.project_info.is_pre_commit_installed", return_value=False, ) - with tmpdir.as_cwd(): - with pytest.raises(InitFailedError): - commands.Init(config)() + monkeypatch.chdir(tmp_path) + 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)() +class TestAskTagFormat: + def test_confirm_v_tag_format(self, mocker: MockFixture, config: BaseConfig): + init = commands.Init(config) + mocker.patch("questionary.confirm", return_value=FakeQuestion(True)) + + result = init._ask_tag_format("v1.0.0") + assert result == r"v$version" + + def test_reject_v_tag_format(self, mocker: MockFixture, config: BaseConfig): + init = commands.Init(config) + mocker.patch("questionary.confirm", return_value=FakeQuestion(False)) + mocker.patch("questionary.text", return_value=FakeQuestion("custom-$version")) + + result = init._ask_tag_format("v1.0.0") + assert result == "custom-$version" + + def test_non_v_tag_format(self, mocker: MockFixture, config: BaseConfig): + init = commands.Init(config) + mocker.patch("questionary.text", return_value=FakeQuestion("custom-$version")) -@skip_below_py_3_10 -def test_init_command_shows_description_when_use_help_option( - mocker: MockFixture, capsys, file_regression + result = init._ask_tag_format("1.0.0") + assert result == "custom-$version" + + def test_empty_input_returns_default(self, mocker: MockFixture, config: BaseConfig): + init = commands.Init(config) + mocker.patch("questionary.confirm", return_value=FakeQuestion(False)) + mocker.patch("questionary.text", return_value=FakeQuestion("")) + + result = init._ask_tag_format("v1.0.0") + assert result == "$version" # This is the default format from DEFAULT_SETTINGS + + +def test_init_with_confirmed_tag_format( + config: BaseConfig, mocker: MockFixture, tmp_path, monkeypatch ): - testargs = ["cz", "init", "--help"] - mocker.patch.object(sys, "argv", testargs) - with pytest.raises(SystemExit): - cli.main() + mocker.patch( + "commitizen.commands.init.get_tag_names", return_value=["v0.0.2", "v0.0.1"] + ) + mocker.patch("commitizen.commands.init.get_latest_tag_name", return_value="v0.0.2") + 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")) + mocker.patch("questionary.checkbox", return_value=FakeQuestion(None)) + + monkeypatch.chdir(tmp_path) + commands.Init(config)() + assert 'tag_format = "v$version"' in Path("pyproject.toml").read_text( + encoding="utf-8" + ) + - out, _ = capsys.readouterr() - file_regression.check(out, extension=".txt") +def test_init_with_no_existing_tags( + config: BaseConfig, mocker: MockFixture, tmp_path, monkeypatch +): + mocker.patch("commitizen.commands.init.get_tag_names", return_value=[]) + mocker.patch("commitizen.commands.init.get_latest_tag_name", return_value="v1.0.0") + mocker.patch( + "questionary.select", + side_effect=[ + FakeQuestion("pyproject.toml"), + FakeQuestion("cz_conventional_commits"), + FakeQuestion("commitizen"), + FakeQuestion("semver"), + ], + ) + mocker.patch("questionary.confirm", return_value=FakeQuestion(False)) + mocker.patch("questionary.text", return_value=FakeQuestion("$version")) + mocker.patch("questionary.checkbox", return_value=FakeQuestion(None)) + + monkeypatch.chdir(tmp_path) + commands.Init(config)() + assert 'version = "0.0.1"' in Path("pyproject.toml").read_text(encoding="utf-8") + + +def test_init_with_no_existing_latest_tag( + config: BaseConfig, mocker: MockFixture, tmp_path, monkeypatch +): + mocker.patch("commitizen.commands.init.get_latest_tag_name", return_value=None) + 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")) + mocker.patch("questionary.checkbox", return_value=FakeQuestion(None)) + + monkeypatch.chdir(tmp_path) + commands.Init(config)() + assert 'version = "0.0.1"' in Path("pyproject.toml").read_text(encoding="utf-8") + + +def test_init_with_existing_tags( + config: BaseConfig, mocker: MockFixture, tmp_path, monkeypatch +): + expected_tags = ["v1.0.0", "v0.9.0", "v0.8.0"] + mocker.patch("commitizen.commands.init.get_tag_names", return_value=expected_tags) + mocker.patch("commitizen.commands.init.get_latest_tag_name", return_value="v1.0.0") + mocker.patch( + "questionary.select", + side_effect=[ + FakeQuestion("pyproject.toml"), + FakeQuestion("cz_conventional_commits"), + FakeQuestion("commitizen"), + FakeQuestion("semver"), # Select version scheme first + FakeQuestion("v1.0.0"), # Then select the latest tag + ], + ) + mocker.patch("questionary.confirm", return_value=FakeQuestion(True)) + mocker.patch("questionary.text", return_value=FakeQuestion("$version")) + mocker.patch("questionary.checkbox", return_value=FakeQuestion(None)) + + monkeypatch.chdir(tmp_path) + commands.Init(config)() + assert 'version = "1.0.0"' in Path("pyproject.toml").read_text(encoding="utf-8") + + +def test_init_with_valid_tag_selection( + config: BaseConfig, mocker: MockFixture, tmp_path, monkeypatch +): + expected_tags = ["v1.0.0", "v0.9.0", "v0.8.0"] + mocker.patch("commitizen.commands.init.get_tag_names", return_value=expected_tags) + mocker.patch("commitizen.commands.init.get_latest_tag_name", return_value="v1.0.0") + + # Mock all questionary.select calls in the exact order they appear in Init.__call__ + mocker.patch( + "questionary.select", + side_effect=[ + FakeQuestion("pyproject.toml"), # _ask_config_path + FakeQuestion("cz_conventional_commits"), # _ask_name + FakeQuestion("commitizen"), # _ask_version_provider + FakeQuestion("v0.9.0"), # _ask_tag (after confirm=False) + FakeQuestion("semver"), # _ask_version_scheme + ], + ) + + mocker.patch( + "questionary.confirm", return_value=FakeQuestion(False) + ) # Don't confirm latest tag + mocker.patch("questionary.text", return_value=FakeQuestion("$version")) + mocker.patch("questionary.checkbox", return_value=FakeQuestion(None)) + + monkeypatch.chdir(tmp_path) + commands.Init(config)() + content = Path("pyproject.toml").read_text(encoding="utf-8") + assert 'version = "0.9.0"' in content + assert 'version_scheme = "semver"' in content + + +def test_init_configuration_settings( + tmp_path, monkeypatch, mocker: MockFixture, config: BaseConfig +): + """Test that all configuration settings are properly initialized.""" + 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")) + mocker.patch("questionary.checkbox", return_value=FakeQuestion(None)) + + monkeypatch.chdir(tmp_path) + commands.Init(config)() + + config_data = Path("pyproject.toml").read_text(encoding="utf-8") + + # Verify all expected settings are present + assert 'name = "cz_conventional_commits"' in config_data + assert 'tag_format = "$version"' in config_data + assert 'version_scheme = "semver"' in config_data + assert 'version = "0.0.1"' in config_data + assert "update_changelog_on_bump = true" in config_data + assert "major_version_zero = true" in config_data + + +def test_init_configuration_with_version_provider( + tmp_path, monkeypatch, mocker: MockFixture, config: BaseConfig +): + """Test configuration initialization with a different version provider.""" + mocker.patch( + "questionary.select", + side_effect=[ + FakeQuestion("pyproject.toml"), + FakeQuestion("cz_conventional_commits"), + FakeQuestion("pep621"), # Different version provider + 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(None)) + + monkeypatch.chdir(tmp_path) + commands.Init(config)() + + config_data = Path("pyproject.toml").read_text(encoding="utf-8") + + # Verify version provider is set instead of version + assert 'name = "cz_conventional_commits"' in config_data + assert 'tag_format = "$version"' in config_data + assert 'version_scheme = "semver"' in config_data + assert 'version_provider = "pep621"' in config_data + assert "update_changelog_on_bump = true" in config_data + assert "major_version_zero = true" in config_data + assert ( + "version = " not in config_data + ) # Version should not be set when using version_provider + + +def test_construct_name_choice_from_registry(config: BaseConfig): + """Test the construction of cz name choices with descriptions.""" + choices = commands.Init(config)._construct_name_choices_from_registry() + assert choices[0].title == "cz_conventional_commits" + assert choices[0].value == "cz_conventional_commits" + assert choices[0].description == "<type>(<scope>): <subject>" + assert choices[1].title == "cz_customize" + assert choices[1].value == "cz_customize" + assert choices[1].description is None + assert choices[2].title == "cz_jira" + assert choices[2].value == "cz_jira" + assert ( + choices[2].description + == "<ignored text> <ISSUE_KEY> <ignored text> #<COMMAND> <optional COMMAND_ARGUMENTS>" + ) diff --git a/tests/commands/test_ls_command.py b/tests/commands/test_ls_command.py deleted file mode 100644 index 7225d2a85c..0000000000 --- a/tests/commands/test_ls_command.py +++ /dev/null @@ -1,26 +0,0 @@ -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_schema_command.py b/tests/commands/test_schema_command.py deleted file mode 100644 index 5e571721c5..0000000000 --- a/tests/commands/test_schema_command.py +++ /dev/null @@ -1,26 +0,0 @@ -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_version_command.py b/tests/commands/test_version_command.py index f7d38c202d..dce710ac84 100644 --- a/tests/commands/test_version_command.py +++ b/tests/commands/test_version_command.py @@ -4,64 +4,60 @@ import pytest from pytest_mock import MockerFixture -from commitizen import cli, commands +from commitizen import 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 +def test_version_for_showing_project_version_error(config, capsys): + # No version specified in config commands.Version( config, - {"report": False, "project": True, "commitizen": False, "verbose": False}, + {"project": True}, )() captured = capsys.readouterr() assert "No project information in this project." in captured.err + +def test_version_for_showing_project_version(config, capsys): config.settings["version"] = "v0.0.1" commands.Version( config, - {"report": False, "project": True, "commitizen": False, "verbose": False}, + {"project": True}, )() 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 +@pytest.mark.parametrize("project", [True, False]) +def test_version_for_showing_commitizen_version(config, capsys, project: bool): commands.Version( config, - {"report": False, "project": False, "commitizen": False, "verbose": False}, + {"project": project, "commitizen": True}, )() captured = capsys.readouterr() assert f"{__version__}" in captured.out -def test_version_for_showing_both_versions(config, capsys): +def test_version_for_showing_both_versions_no_project(config, capsys): commands.Version( config, - {"report": False, "project": False, "commitizen": False, "verbose": True}, + {"verbose": True}, )() captured = capsys.readouterr() assert f"Installed Commitizen Version: {__version__}" in captured.out assert "No project information in this project." in captured.err + +def test_version_for_showing_both_versions(config, capsys): config.settings["version"] = "v0.0.1" commands.Version( config, - {"report": False, "project": False, "commitizen": False, "verbose": True}, + {"verbose": True}, )() captured = capsys.readouterr() expected_out = ( - f"Installed Commitizen Version: {__version__}\n" f"Project Version: v0.0.1" + f"Installed Commitizen Version: {__version__}\nProject Version: v0.0.1" ) assert expected_out in captured.out @@ -69,7 +65,7 @@ def test_version_for_showing_both_versions(config, capsys): def test_version_for_showing_commitizen_system_info(config, capsys): commands.Version( config, - {"report": True, "project": False, "commitizen": False, "verbose": False}, + {"report": True}, )() captured = capsys.readouterr() assert f"Commitizen Version: {__version__}" in captured.out @@ -77,7 +73,7 @@ def test_version_for_showing_commitizen_system_info(config, capsys): assert f"Operating System: {platform.system()}" in captured.out -@pytest.mark.parametrize("project", (True, False)) +@pytest.mark.parametrize("project", [True, False]) @pytest.mark.usefixtures("tmp_git_project") def test_version_use_version_provider( mocker: MockerFixture, @@ -95,9 +91,7 @@ def test_version_use_version_provider( commands.Version( config, { - "report": False, "project": project, - "commitizen": False, "verbose": not project, }, )() @@ -109,14 +103,100 @@ def test_version_use_version_provider( 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 +@pytest.mark.parametrize( + ("version", "expected_version"), + [ + ("1.0.0", "1\n"), + ("2.1.3", "2\n"), + ("0.0.1", "0\n"), + ("0.1.0", "0\n"), + ], +) +def test_version_just_major(config, capsys, version: str, expected_version: str): + config.settings["version"] = version + commands.Version( + config, + { + "project": True, + "major": True, + }, + )() + captured = capsys.readouterr() + assert expected_version == captured.out + + +@pytest.mark.parametrize( + ("version", "expected_version"), + [ + ("1.0.0", "0\n"), + ("2.1.3", "1\n"), + ("0.0.1", "0\n"), + ("0.1.0", "1\n"), + ], +) +def test_version_just_minor(config, capsys, version: str, expected_version: str): + config.settings["version"] = version + commands.Version( + config, + { + "project": True, + "minor": True, + }, + )() + captured = capsys.readouterr() + assert expected_version == captured.out + + +@pytest.mark.parametrize("argument", ["major", "minor"]) +def test_version_just_major_error_no_project(config, capsys, argument: str): + commands.Version( + config, + { + argument: True, # type: ignore[misc] + }, + )() + captured = capsys.readouterr() + assert not captured.out + assert ( + "Major or minor version can only be used with --project or --verbose." + in captured.err + ) + + +@pytest.mark.parametrize( + ("version", "tag_format", "expected_output"), + [ + ("1.2.3", "v$version", "v1.2.3\n"), + ("1.2.3", "$version", "1.2.3\n"), + ("2.0.0", "release-$version", "release-2.0.0\n"), + ("0.1.0", "ver$version", "ver0.1.0\n"), + ], +) +def test_version_with_tag_format( + config, capsys, version: str, tag_format: str, expected_output: str ): - testargs = ["cz", "version", "--help"] - mocker.patch.object(sys, "argv", testargs) - with pytest.raises(SystemExit): - cli.main() + """Test --tag option applies tag_format to version""" + config.settings["version"] = version + config.settings["tag_format"] = tag_format + commands.Version( + config, + { + "project": True, + "tag": True, + }, + )() + captured = capsys.readouterr() + assert captured.out == expected_output + - out, _ = capsys.readouterr() - file_regression.check(out, extension=".txt") +def test_version_tag_without_project_error(config, capsys): + """Test --tag requires --project or --verbose""" + commands.Version( + config, + { + "tag": True, + }, + )() + captured = capsys.readouterr() + assert not captured.out + assert "Tag can only be used with --project or --verbose." in captured.err 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 deleted file mode 100644 index c461b10bcd..0000000000 --- a/tests/commands/test_version_command/test_version_command_shows_description_when_use_help_option.txt +++ /dev/null @@ -1,12 +0,0 @@ -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 index cc306ac6d4..178b92200a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,12 +2,13 @@ import os import re +import subprocess +import sys import tempfile from pathlib import Path -from typing import Iterator +from typing import TYPE_CHECKING, cast import pytest -from pytest_mock import MockerFixture from commitizen import cmd, defaults from commitizen.changelog_formats import ( @@ -17,30 +18,51 @@ from commitizen.config import BaseConfig from commitizen.cz import registry from commitizen.cz.base import BaseCommitizen -from tests.utils import create_file_and_commit + +if TYPE_CHECKING: + from collections.abc import Iterator, Mapping + + from pytest_mock import MockerFixture + + from commitizen.question import CzQuestion + from tests.utils import UtilFixture + SIGNER = "GitHub Action" SIGNER_MAIL = "action@github.com" +PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}" +pytest_plugins = [ + "tests.utils", +] + + +@pytest.fixture +def repo_root() -> Path: + return Path(__file__).parent.parent -@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 - cmd.run("git config --global init.defaultBranch master") +@pytest.fixture +def in_repo_root(repo_root: Path) -> Iterator[Path]: + cwd = os.getcwd() + os.chdir(repo_root) + yield repo_root + os.chdir(cwd) + + +@pytest.fixture +def data_dir(repo_root: Path) -> Path: + return repo_root / "tests" / "data" + + +@pytest.fixture(scope="session") +def set_default_gitconfig() -> dict[str, str]: + return { + "user.name": "SIGNER", + "user.email": SIGNER_MAIL, + "safe.directory": "*", + "init.defaultBranch": "master", + } @pytest.fixture @@ -51,49 +73,53 @@ def chdir(tmp_path: Path) -> Iterator[Path]: os.chdir(cwd) -@pytest.fixture(scope="function") -def tmp_git_project(tmpdir): - with tmpdir.as_cwd(): - cmd.run("git init") +@pytest.fixture +def tmp_git_project(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + monkeypatch.chdir(tmp_path) + cmd.run("git init") - yield tmpdir + return tmp_path -@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]\n" 'version="0.1.0"\n') +@pytest.fixture +def tmp_commitizen_project(tmp_git_project: Path): + (tmp_git_project / "pyproject.toml").write_text( + '[tool.commitizen]\nversion="0.1.0"\n' + ) + + return tmp_git_project - yield tmp_git_project +@pytest.fixture +def pyproject(tmp_commitizen_project: Path) -> Path: + return tmp_commitizen_project / "pyproject.toml" -@pytest.fixture(scope="function") -def tmp_commitizen_project_initial(tmp_git_project): + +@pytest.fixture +def tmp_commitizen_project_initial( + tmp_git_project: Path, util: UtilFixture, monkeypatch: pytest.MonkeyPatch +): 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]\n" f'version="{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 + monkeypatch.chdir(tmp_git_project) + tmp_commitizen_cfg_file = tmp_git_project / "pyproject.toml" + tmp_commitizen_cfg_file.write_text(f'[tool.commitizen]\nversion="{version}"\n') + tmp_version_file = tmp_git_project / "__version__.py" + tmp_version_file.write_text(version) + tmp_version_file_string = str(tmp_version_file).replace("\\", "/") + with tmp_commitizen_cfg_file.open("a", encoding="utf-8") as f: + f.write(f'\nversion_files = ["{tmp_version_file_string}"]\n') + if config_extra: + with tmp_commitizen_cfg_file.open("a", encoding="utf-8") as f: + f.write(config_extra) + util.create_file_and_commit(initial_commit) + + return tmp_git_project + + return _initial def _get_gpg_keyid(signer_mail): @@ -105,42 +131,53 @@ def _get_gpg_keyid(signer_mail): return _m.group(1) if _m else None -@pytest.fixture(scope="function") +@pytest.fixture 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") + old_gnupghome = os.environ.get("GNUPGHOME") 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 + try: + # create a key (a keyring will be generated within GPUPGHOME) + subprocess.run( + [ + "gpg", + "--batch", + "--yes", + "--debug-quick-random", + "--passphrase", + "", + "--quick-gen-key", + f"{SIGNER} {SIGNER_MAIL}", + ], + check=True, + ) + 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 + finally: + if old_gnupghome is not None: + os.environ["GNUPGHOME"] = old_gnupghome + elif "GNUPGHOME" in os.environ and os.name != "nt": + os.environ.pop("GNUPGHOME") + gpg_home.cleanup() -@pytest.fixture() +@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. @@ -164,14 +201,14 @@ class SemverCommitizen(BaseCommitizen): "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 + commit_parser = r"^(?P<change_type>patch|minor|major)(?:\((?P<scope>[^()\r\n]*)\)|\()?:?\s(?P<message>.+)" change_type_map = { "major": "Breaking Changes", "minor": "Features", "patch": "Bugs", } - def questions(self) -> list: + def questions(self) -> list[CzQuestion]: return [ { "type": "list", @@ -204,30 +241,54 @@ def questions(self) -> list: }, ] - def message(self, answers: dict) -> str: + def message(self, answers: Mapping) -> str: prefix = answers["prefix"] subject = answers.get("subject", "default message").trim() return f"{prefix}: {subject}" + def example(self) -> str: + return "" + + def schema(self) -> str: + return "" + + def schema_pattern(self) -> str: + return "" -@pytest.fixture() + def info(self) -> str: + return "" + + +@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: + def questions(self) -> list[CzQuestion]: return [] - def message(self, answers: dict) -> str: + def message(self, answers: Mapping) -> str: + return "" + + def example(self) -> str: + return "" + + def schema(self) -> str: + return "" + + def schema_pattern(self) -> str: + return "" + + def info(self) -> str: return "" @pytest.fixture def mock_plugin(mocker: MockerFixture, config: BaseConfig) -> BaseCommitizen: mock = MockPlugin(config) - mocker.patch("commitizen.factory.commiter_factory", return_value=mock) + mocker.patch("commitizen.factory.committer_factory", return_value=mock) return mock @@ -244,7 +305,8 @@ def changelog_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()}\n" f'changelog_format = "{format}"\n') + with pyproject.open("a", encoding="utf-8") as f: + f.write(f'\nchangelog_format = "{format}"\n') return get_changelog_format(config) @@ -253,3 +315,20 @@ 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) + + +@pytest.fixture(params=[pytest.param(PYTHON_VERSION, id=f"py_{PYTHON_VERSION}")]) +def python_version(request: pytest.FixtureRequest) -> str: + """The current python version in '{major}.{minor}' format""" + return cast("str", request.param) + + +@pytest.fixture +def consistent_terminal_output(monkeypatch: pytest.MonkeyPatch): + """Force consistent terminal output.""" + monkeypatch.setenv("COLUMNS", "80") + monkeypatch.setenv("TERM", "dumb") + monkeypatch.setenv("LC_ALL", "C") + monkeypatch.setenv("LANG", "C") + monkeypatch.setenv("NO_COLOR", "1") + monkeypatch.setenv("PAGER", "cat") diff --git a/tests/CHANGELOG_FOR_TEST.md b/tests/data/CHANGELOG_FOR_TEST.md similarity index 100% rename from tests/CHANGELOG_FOR_TEST.md rename to tests/data/CHANGELOG_FOR_TEST.md diff --git a/tests/data/encoding_test_composer.json b/tests/data/encoding_test_composer.json new file mode 100644 index 0000000000..2cbf2e70cc --- /dev/null +++ b/tests/data/encoding_test_composer.json @@ -0,0 +1,6 @@ +{ + "name": "encoding-test-composer", + "description": "Тест описания для проверки кодировки", + "version": "0.1.0" +} + diff --git a/tests/data/encoding_test_pyproject.toml b/tests/data/encoding_test_pyproject.toml new file mode 100644 index 0000000000..6e47e88ecd --- /dev/null +++ b/tests/data/encoding_test_pyproject.toml @@ -0,0 +1,36 @@ +[project] +name = "pythonproject-test" +version = "0.4.1" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] + + +[tool.commitizen] +name = "cz_customize" +tag_format = "v$version" +version_scheme = "pep440" +version_provider = "uv" +update_changelog_on_bump = true +changelog_start_rev = "v1.1.0" + +[tool.commitizen.customize] +message_template = "{{ change_type }}{% if scope != 'none' %}({{ scope }}){% endif %}: {{ message }}" +commit_parser = '^(?P<change_type>feat|fix|refactor|test|perf|misc):\s(?P<message>.*)' +schema_pattern = '(feat|fix|refactor|test|perf|misc)(\((api|core)\))?:\s(.{3,})' +bump_pattern = "^(feat|fix|refactor|test|perf|misc)" +change_type_map = { "feat" = "Новое", "fix" = "Исправление" } + +[[tool.commitizen.customize.questions]] +name = "change_type" +type = "list" +message = "Выберите тип изменений" +choices = [ + { value = "feat", name = "feat: Новая функциональность" }, + { value = "fix", name = "fix: Исправление" }, + { value = "refactor", name = "refactor: Рефакторинг" }, + { value = "test", name = "test: Изменение авто-тестов" }, + { value = "perf", name = "perf: Оптимизации" }, + { value = "misc", name = "misc: Другое" }, +] \ No newline at end of file diff --git a/tests/providers/conftest.py b/tests/providers/conftest.py deleted file mode 100644 index b4432ca524..0000000000 --- a/tests/providers/conftest.py +++ /dev/null @@ -1,15 +0,0 @@ -from __future__ import annotations - -import os -from pathlib import Path -from typing import Iterator - -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 index 482bd698ea..4129fa8c22 100644 --- a/tests/providers/test_base_provider.py +++ b/tests/providers/test_base_provider.py @@ -1,11 +1,20 @@ from __future__ import annotations +from typing import TYPE_CHECKING + 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 +from commitizen.providers.composer_provider import ComposerProvider +from commitizen.providers.pep621_provider import Pep621Provider +from commitizen.providers.uv_provider import UvProvider + +if TYPE_CHECKING: + from pathlib import Path + + from commitizen.config.base_config import BaseConfig def test_default_version_provider_is_commitizen_config(config: BaseConfig): @@ -18,3 +27,98 @@ def test_raise_for_unknown_provider(config: BaseConfig): config.settings["version_provider"] = "unknown" with pytest.raises(VersionProviderUnknown): get_provider(config) + + +@pytest.mark.parametrize("encoding", ["utf-8", "latin-1"]) +def test_file_provider_get_encoding(config: BaseConfig, encoding: str): + """_get_encoding should return the configured encoding.""" + config.settings["encoding"] = encoding + provider = ComposerProvider(config) + assert provider._get_encoding() == encoding + + +def test_json_provider_uses_encoding_with_encoding_fixture( + config: BaseConfig, + chdir: Path, + data_dir: Path, +): + """JsonProvider should correctly read a JSON file with non-ASCII content.""" + source = data_dir / "encoding_test_composer.json" + target = chdir / "composer.json" + target.write_text(source.read_text(encoding="utf-8"), encoding="utf-8") + + config.settings["encoding"] = "utf-8" + config.settings["version_provider"] = "composer" + + provider = get_provider(config) + assert isinstance(provider, ComposerProvider) + assert provider.get_version() == "0.1.0" + + +def test_toml_provider_uses_encoding_with_encoding_fixture( + config: BaseConfig, + chdir: Path, + data_dir: Path, +): + """TomlProvider should correctly read a TOML file with non-ASCII content.""" + source = data_dir / "encoding_test_pyproject.toml" + target = chdir / "pyproject.toml" + target.write_text(source.read_text(encoding="utf-8"), encoding="utf-8") + + config.settings["encoding"] = "utf-8" + config.settings["version_provider"] = "uv" + + provider = get_provider(config) + assert isinstance(provider, UvProvider) + assert provider.get_version() == "0.4.1" + + +def test_json_provider_handles_various_unicode_characters( + config: BaseConfig, + chdir: Path, +): + """JsonProvider should handle a wide range of Unicode characters.""" + config.settings["encoding"] = "utf-8" + config.settings["version_provider"] = "composer" + + filename = ComposerProvider.filename + file = chdir / filename + file.write_text( + ( + "{\n" + ' "name": "多言語-имя-árbol",\n' + ' "description": "Emoji 😀 – 漢字 – العربية",\n' + ' "version": "0.1.0"\n' + "}\n" + ), + encoding="utf-8", + ) + + provider = get_provider(config) + assert isinstance(provider, ComposerProvider) + assert provider.get_version() == "0.1.0" + + +def test_toml_provider_handles_various_unicode_characters( + config: BaseConfig, + chdir: Path, +): + """TomlProvider should handle a wide range of Unicode characters.""" + config.settings["encoding"] = "utf-8" + config.settings["version_provider"] = "pep621" + + filename = Pep621Provider.filename + file = chdir / filename + file.write_text( + ( + "[project]\n" + 'name = "多言語-имя-árbol"\n' + 'description = "Emoji 😀 – 漢字 – العربية"\n' + 'version = "0.1.0"\n' + ), + encoding="utf-8", + ) + + provider = get_provider(config) + assert isinstance(provider, Pep621Provider) + assert provider.get_version() == "0.1.0" diff --git a/tests/providers/test_cargo_provider.py b/tests/providers/test_cargo_provider.py index 646ef3a53d..63f143b291 100644 --- a/tests/providers/test_cargo_provider.py +++ b/tests/providers/test_cargo_provider.py @@ -1,45 +1,239 @@ from __future__ import annotations +import os from pathlib import Path from textwrap import dedent +from typing import TYPE_CHECKING import pytest -from commitizen.config.base_config import BaseConfig from commitizen.providers import get_provider from commitizen.providers.cargo_provider import CargoProvider +if TYPE_CHECKING: + from commitizen.config.base_config import BaseConfig + CARGO_TOML = """\ [package] name = "whatever" version = "0.1.0" """ -CARGO_EXPECTED = """\ +CARGO_TOML_EXPECTED = """\ [package] name = "whatever" version = "42.1" """ CARGO_WORKSPACE_TOML = """\ +[workspace] +members = ["member1", "folder/member2", "crates/*"] +exclude = ["crates/member4", "folder/member5"] + [workspace.package] -name = "whatever" version = "0.1.0" """ -CARGO_WORKSPACE_EXPECTED = """\ +CARGO_WORKSPACE_MEMBERS = [ + { + "path": "member1", + "content": """\ +[package] +name = "member1" +version.workspace = true +""", + }, + { + "path": "folder/member2", + "content": """\ +[package] +name = "member2" +version.workspace = "1.1.1" +""", + }, + { + "path": "crates/member3", + "content": """\ +[package] +name = "member3" +version.workspace = true +""", + }, + { + "path": "crates/member4", + "content": """\ +[package] +name = "member4" +version.workspace = "2.2.2" +""", + }, + { + "path": "folder/member5", + "content": """\ +[package] +name = "member5" +version.workspace = "3.3.3" +""", + }, +] + + +CARGO_WORKSPACE_TOML_EXPECTED = """\ +[workspace] +members = ["member1", "folder/member2", "crates/*"] +exclude = ["crates/member4", "folder/member5"] + [workspace.package] +version = "42.1" +""" + +CARGO_LOCK = """\ +[[package]] +name = "whatever" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123abc" +dependencies = [ + "packageA", + "packageB", + "packageC", +] +""" + +CARGO_LOCK_EXPECTED = """\ +[[package]] name = "whatever" version = "42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123abc" +dependencies = [ + "packageA", + "packageB", + "packageC", +] +""" + +CARGO_WORKSPACE_LOCK = """\ +[[package]] +name = "member1" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123abc" +dependencies = [ + "packageA", + "packageB", + "packageC", +] + +[[package]] +name = "member2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123abc" +dependencies = [ + "packageA", + "packageB", + "packageC", +] + +[[package]] +name = "member3" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123abc" +dependencies = [ + "packageA", + "packageB", + "packageC", +] + +[[package]] +name = "member4" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123abc" +dependencies = [ + "packageA", + "packageB", + "packageC", +] + +[[package]] +name = "member5" +version = "3.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123abc" +dependencies = [ + "packageA", + "packageB", + "packageC", +] +""" + +CARGO_WORKSPACE_LOCK_EXPECTED = """\ +[[package]] +name = "member1" +version = "42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123abc" +dependencies = [ + "packageA", + "packageB", + "packageC", +] + +[[package]] +name = "member2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123abc" +dependencies = [ + "packageA", + "packageB", + "packageC", +] + +[[package]] +name = "member3" +version = "42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123abc" +dependencies = [ + "packageA", + "packageB", + "packageC", +] + +[[package]] +name = "member4" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123abc" +dependencies = [ + "packageA", + "packageB", + "packageC", +] + +[[package]] +name = "member5" +version = "3.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123abc" +dependencies = [ + "packageA", + "packageB", + "packageC", +] """ @pytest.mark.parametrize( - "content, expected", - ( - (CARGO_TOML, CARGO_EXPECTED), - (CARGO_WORKSPACE_TOML, CARGO_WORKSPACE_EXPECTED), - ), + ("content", "expected"), + [ + (CARGO_TOML, CARGO_TOML_EXPECTED), + (CARGO_WORKSPACE_TOML, CARGO_WORKSPACE_TOML_EXPECTED), + ], ) def test_cargo_provider( config: BaseConfig, @@ -58,3 +252,202 @@ def test_cargo_provider( provider.set_version("42.1") assert file.read_text() == dedent(expected) + + +@pytest.mark.parametrize( + ("toml_content", "lock_content", "toml_expected", "lock_expected"), + [ + ( + CARGO_TOML, + CARGO_LOCK, + CARGO_TOML_EXPECTED, + CARGO_LOCK_EXPECTED, + ), + ( + CARGO_WORKSPACE_TOML, + CARGO_WORKSPACE_LOCK, + CARGO_WORKSPACE_TOML_EXPECTED, + CARGO_WORKSPACE_LOCK_EXPECTED, + ), + ], +) +def test_cargo_provider_with_lock( + config: BaseConfig, + chdir: Path, + toml_content: str, + lock_content: str, + toml_expected: str, + lock_expected: str, +): + filename = CargoProvider.filename + file = chdir / filename + file.write_text(dedent(toml_content)) + + # Create workspace members + os.mkdir(chdir / "crates") + os.mkdir(chdir / "folder") + for i in range(0, 5): + member_folder = Path(CARGO_WORKSPACE_MEMBERS[i]["path"]) + os.mkdir(member_folder) + member_file = member_folder / "Cargo.toml" + member_file.write_text(dedent(CARGO_WORKSPACE_MEMBERS[i]["content"])) + + lock_filename = CargoProvider.lock_filename + lock_file = chdir / lock_filename + lock_file.write_text(dedent(lock_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(toml_expected) + assert lock_file.read_text() == dedent(lock_expected) + + +def test_cargo_provider_workspace_member_without_version_key( + config: BaseConfig, + chdir: Path, +): + """Test workspace member that has no version key at all (should not crash).""" + workspace_toml = """\ +[workspace] +members = ["member_without_version"] + +[workspace.package] +version = "0.1.0" +""" + + # Create a member that has no version key at all + member_content = """\ +[package] +name = "member_without_version" +# No version key - this should trigger NonExistentKey exception +""" + + lock_content = """\ +[[package]] +name = "member_without_version" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123abc" +""" + + expected_workspace_toml = """\ +[workspace] +members = ["member_without_version"] + +[workspace.package] +version = "42.1" +""" + + expected_lock_content = """\ +[[package]] +name = "member_without_version" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123abc" +""" + + # Create the workspace file + filename = CargoProvider.filename + file = chdir / filename + file.write_text(dedent(workspace_toml)) + + # Create the member directory and file + os.mkdir(chdir / "member_without_version") + member_file = chdir / "member_without_version" / "Cargo.toml" + member_file.write_text(dedent(member_content)) + + # Create the lock file + lock_filename = CargoProvider.lock_filename + lock_file = chdir / lock_filename + lock_file.write_text(dedent(lock_content)) + + config.settings["version_provider"] = "cargo" + + provider = get_provider(config) + assert isinstance(provider, CargoProvider) + assert provider.get_version() == "0.1.0" + + # This should not crash even though the member has no version key + provider.set_version("42.1") + assert file.read_text() == dedent(expected_workspace_toml) + # The lock file should remain unchanged since the member doesn't inherit workspace version + assert lock_file.read_text() == dedent(expected_lock_content) + + +def test_cargo_provider_workspace_member_without_workspace_key( + config: BaseConfig, + chdir: Path, +): + """Test workspace member that has version key but no workspace subkey.""" + workspace_toml = """\ +[workspace] +members = ["member_without_workspace"] + +[workspace.package] +version = "0.1.0" +""" + + # Create a member that has version as a table but no workspace subkey + # This should trigger NonExistentKey when trying to access version["workspace"] + member_content = """\ +[package] +name = "member_without_workspace" + +[package.version] +# Has version table but no workspace key - should trigger NonExistentKey +""" + + lock_content = """\ +[[package]] +name = "member_without_workspace" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123abc" +""" + + expected_workspace_toml = """\ +[workspace] +members = ["member_without_workspace"] + +[workspace.package] +version = "42.1" +""" + + expected_lock_content = """\ +[[package]] +name = "member_without_workspace" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "123abc" +""" + + # Create the workspace file + filename = CargoProvider.filename + file = chdir / filename + file.write_text(dedent(workspace_toml)) + + # Create the member directory and file + os.mkdir(chdir / "member_without_workspace") + member_file = chdir / "member_without_workspace" / "Cargo.toml" + member_file.write_text(dedent(member_content)) + + # Create the lock file + lock_filename = CargoProvider.lock_filename + lock_file = chdir / lock_filename + lock_file.write_text(dedent(lock_content)) + + config.settings["version_provider"] = "cargo" + + provider = get_provider(config) + assert isinstance(provider, CargoProvider) + assert provider.get_version() == "0.1.0" + + # This should not crash even though the member has no version.workspace key + provider.set_version("42.1") + assert file.read_text() == dedent(expected_workspace_toml) + # The lock file should remain unchanged since the member doesn't inherit workspace version + assert lock_file.read_text() == dedent(expected_lock_content) diff --git a/tests/providers/test_commitizen_provider.py b/tests/providers/test_commitizen_provider.py index b8df60da93..c28dc90f79 100644 --- a/tests/providers/test_commitizen_provider.py +++ b/tests/providers/test_commitizen_provider.py @@ -2,12 +2,13 @@ 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 + from commitizen.config.base_config import BaseConfig + def test_commitizen_provider(config: BaseConfig, mocker: MockerFixture): config.settings["version"] = "42" diff --git a/tests/providers/test_composer_provider.py b/tests/providers/test_composer_provider.py index 45cbc8afa4..b3068a6fd6 100644 --- a/tests/providers/test_composer_provider.py +++ b/tests/providers/test_composer_provider.py @@ -1,14 +1,18 @@ from __future__ import annotations -from pathlib import Path from textwrap import dedent +from typing import TYPE_CHECKING import pytest -from commitizen.config.base_config import BaseConfig from commitizen.providers import get_provider from commitizen.providers.composer_provider import ComposerProvider +if TYPE_CHECKING: + from pathlib import Path + + from commitizen.config.base_config import BaseConfig + COMPOSER_JSON = """\ { "name": "whatever", @@ -25,8 +29,8 @@ @pytest.mark.parametrize( - "content, expected", - ((COMPOSER_JSON, COMPOSER_EXPECTED),), + ("content", "expected"), + [(COMPOSER_JSON, COMPOSER_EXPECTED)], ) def test_composer_provider( config: BaseConfig, diff --git a/tests/providers/test_npm_provider.py b/tests/providers/test_npm_provider.py index bc9399916d..429b46fac9 100644 --- a/tests/providers/test_npm_provider.py +++ b/tests/providers/test_npm_provider.py @@ -1,14 +1,18 @@ from __future__ import annotations -from pathlib import Path from textwrap import dedent +from typing import TYPE_CHECKING import pytest -from commitizen.config.base_config import BaseConfig from commitizen.providers import get_provider from commitizen.providers.npm_provider import NpmProvider +if TYPE_CHECKING: + from pathlib import Path + + from commitizen.config.base_config import BaseConfig + NPM_PACKAGE_JSON = """\ { "name": "whatever", @@ -59,14 +63,36 @@ } """ +NPM_PACKAGE_JSON_LATIN1 = """\ +{ + "name": "calf\u00e9-n\u00famero", + "version": "0.1.0" +} +""" + +NPM_LOCKFILE_JSON_LATIN1 = """\ +{ + "name": "calf\u00e9-n\u00famero", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "calf\u00e9-n\u00famero", + "version": "0.1.0" + } + } +} +""" + @pytest.mark.parametrize( - "pkg_shrinkwrap_content, pkg_shrinkwrap_expected", - ((NPM_LOCKFILE_JSON, NPM_LOCKFILE_EXPECTED), (None, None)), + ("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)), + ("pkg_lock_content", "pkg_lock_expected"), + [(NPM_LOCKFILE_JSON, NPM_LOCKFILE_EXPECTED), (None, None)], ) def test_npm_provider( config: BaseConfig, @@ -96,3 +122,37 @@ def test_npm_provider( assert pkg_lock.read_text() == dedent(pkg_lock_expected) if pkg_shrinkwrap_content: assert pkg_shrinkwrap.read_text() == dedent(pkg_shrinkwrap_expected) + + +def test_npm_provider_respects_configured_encoding_for_all_files( + config: BaseConfig, + chdir: Path, +): + """NpmProvider should use the configured encoding for all files it touches.""" + config.settings["encoding"] = "latin-1" + config.settings["version_provider"] = "npm" + + pkg = chdir / NpmProvider.package_filename + pkg_lock = chdir / NpmProvider.lock_filename + pkg_shrinkwrap = chdir / NpmProvider.shrinkwrap_filename + + # Write initial contents using latin-1 encoding + pkg.write_text(dedent(NPM_PACKAGE_JSON_LATIN1), encoding="latin-1") + pkg_lock.write_text(dedent(NPM_LOCKFILE_JSON_LATIN1), encoding="latin-1") + pkg_shrinkwrap.write_text(dedent(NPM_LOCKFILE_JSON_LATIN1), encoding="latin-1") + + provider = get_provider(config) + assert isinstance(provider, NpmProvider) + assert provider.get_version() == "0.1.0" + + provider.set_version("42.1") + + # Verify that the files can be read back using the configured encoding + pkg_text = pkg.read_text(encoding="latin-1") + pkg_lock_text = pkg_lock.read_text(encoding="latin-1") + pkg_shrinkwrap_text = pkg_shrinkwrap.read_text(encoding="latin-1") + + # Version was updated everywhere + assert '"version": "42.1"' in pkg_text + assert '"version": "42.1"' in pkg_lock_text + assert '"version": "42.1"' in pkg_shrinkwrap_text diff --git a/tests/providers/test_pep621_provider.py b/tests/providers/test_pep621_provider.py index 16bb479cc4..e0ae0ba949 100644 --- a/tests/providers/test_pep621_provider.py +++ b/tests/providers/test_pep621_provider.py @@ -1,14 +1,18 @@ from __future__ import annotations -from pathlib import Path from textwrap import dedent +from typing import TYPE_CHECKING import pytest -from commitizen.config.base_config import BaseConfig from commitizen.providers import get_provider from commitizen.providers.pep621_provider import Pep621Provider +if TYPE_CHECKING: + from pathlib import Path + + from commitizen.config.base_config import BaseConfig + PEP621_TOML = """\ [project] version = "0.1.0" @@ -21,8 +25,8 @@ @pytest.mark.parametrize( - "content, expected", - ((PEP621_TOML, PEP621_EXPECTED),), + ("content", "expected"), + [(PEP621_TOML, PEP621_EXPECTED)], ) def test_cargo_provider( config: BaseConfig, diff --git a/tests/providers/test_poetry_provider.py b/tests/providers/test_poetry_provider.py index e26e2a44fb..2a7841b8a1 100644 --- a/tests/providers/test_poetry_provider.py +++ b/tests/providers/test_poetry_provider.py @@ -1,14 +1,18 @@ from __future__ import annotations -from pathlib import Path from textwrap import dedent +from typing import TYPE_CHECKING import pytest -from commitizen.config.base_config import BaseConfig from commitizen.providers import get_provider from commitizen.providers.poetry_provider import PoetryProvider +if TYPE_CHECKING: + from pathlib import Path + + from commitizen.config.base_config import BaseConfig + POETRY_TOML = """\ [tool.poetry] version = "0.1.0" @@ -21,8 +25,8 @@ @pytest.mark.parametrize( - "content, expected", - ((POETRY_TOML, POETRY_EXPECTED),), + ("content", "expected"), + [(POETRY_TOML, POETRY_EXPECTED)], ) def test_cargo_provider( config: BaseConfig, diff --git a/tests/providers/test_scm_provider.py b/tests/providers/test_scm_provider.py index 01e7ab9943..d3f0299f47 100644 --- a/tests/providers/test_scm_provider.py +++ b/tests/providers/test_scm_provider.py @@ -1,22 +1,20 @@ from __future__ import annotations +from typing import TYPE_CHECKING + 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, -) + +if TYPE_CHECKING: + from commitizen.config.base_config import BaseConfig + from tests.utils import UtilFixture @pytest.mark.parametrize( - "tag_format,tag,expected_version", - ( + ("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"), @@ -38,16 +36,20 @@ ("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 + config: BaseConfig, + tag_format: str, + tag: str, + expected_version: str, + util: UtilFixture, ): - create_file_and_commit("test: fake commit") - create_tag(tag) - create_file_and_commit("test: fake commit") - create_tag("should-not-match") + util.create_file_and_commit("test: fake commit") + util.create_tag(tag) + util.create_file_and_commit("test: fake commit") + util.create_tag("should-not-match") config.settings["version_provider"] = "scm" config.settings["tag_format"] = tag_format @@ -71,45 +73,70 @@ def test_scm_provider_default_without_commits_and_tags(config: BaseConfig): @pytest.mark.usefixtures("tmp_git_project") -def test_scm_provider_default_with_commits_and_tags(config: BaseConfig): +def test_scm_provider_default_with_commits_and_tags( + config: BaseConfig, util: UtilFixture +): 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") + util.create_file_and_commit("Initial state") + util.create_tag("1.0.0") # create develop - create_branch("develop") - switch_branch("develop") + util.create_branch("develop") + util.switch_branch("develop") # add a feature to develop - create_file_and_commit("develop: add beta feature1") + util.create_file_and_commit("develop: add beta feature1") assert provider.get_version() == "1.0.0" - create_tag("1.1.0b0") + util.create_tag("1.1.0b0") # create staging - create_branch("staging") - switch_branch("staging") - create_file_and_commit("staging: Starting release candidate") + util.create_branch("staging") + util.switch_branch("staging") + util.create_file_and_commit("staging: Starting release candidate") assert provider.get_version() == "1.1.0b0" - create_tag("1.1.0rc0") + util.create_tag("1.1.0rc0") # add another feature to develop - switch_branch("develop") - create_file_and_commit("develop: add beta feature2") + util.switch_branch("develop") + util.create_file_and_commit("develop: add beta feature2") assert provider.get_version() == "1.1.0b0" - create_tag("1.2.0b0") + util.create_tag("1.2.0b0") # add a hotfix to master - switch_branch("master") - create_file_and_commit("master: add hotfix") + util.switch_branch("master") + util.create_file_and_commit("master: add hotfix") assert provider.get_version() == "1.0.0" - create_tag("1.0.1") + util.create_tag("1.0.1") # merge the hotfix to staging - switch_branch("staging") - merge_branch("master") + util.switch_branch("staging") + util.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, util: UtilFixture): + config.settings["version_provider"] = "scm" + config.settings["tag_format"] = "v${version}" + config.settings["legacy_tag_formats"] = [ + "legacy-${version}", + "old-${version}", + ] + provider = get_provider(config) + + util.create_file_and_commit("test: fake commit") + util.create_tag("old-0.4.1") + assert provider.get_version() == "0.4.1" + + util.create_file_and_commit("test: fake commit") + util.create_tag("legacy-0.4.2") + assert provider.get_version() == "0.4.2" + + util.create_file_and_commit("test: fake commit") + util.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 0000000000..e0da979160 --- /dev/null +++ b/tests/providers/test_uv_provider.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from commitizen.providers import get_provider +from commitizen.providers.uv_provider import UvProvider + +if TYPE_CHECKING: + from pytest_regressions.file_regression import FileRegressionFixture + + from commitizen.config.base_config import BaseConfig + + +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"] +""" + +PYPROJECT_TOML_UNDERSCORE = """ +[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" }, +] +""" + + +@pytest.mark.parametrize( + "pyproject_content", + [ + pytest.param(PYPROJECT_TOML, id="hyphenated"), + pytest.param(PYPROJECT_TOML_UNDERSCORE, id="underscore"), + ], +) +def test_uv_provider( + config: BaseConfig, + tmp_path, + monkeypatch, + file_regression: FileRegressionFixture, + pyproject_content: str, +): + monkeypatch.chdir(tmp_path) + pyproject_toml_file = tmp_path / UvProvider.filename + pyproject_toml_file.write_text(pyproject_content, encoding="utf-8") + + uv_lock_file = tmp_path / 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_hyphenated_.lock b/tests/providers/test_uv_provider/test_uv_provider_hyphenated_.lock new file mode 100644 index 0000000000..d353763ce3 --- /dev/null +++ b/tests/providers/test_uv_provider/test_uv_provider_hyphenated_.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_hyphenated_.toml b/tests/providers/test_uv_provider/test_uv_provider_hyphenated_.toml new file mode 100644 index 0000000000..9fdb6eb5aa --- /dev/null +++ b/tests/providers/test_uv_provider/test_uv_provider_hyphenated_.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/providers/test_uv_provider/test_uv_provider_underscore_.lock b/tests/providers/test_uv_provider/test_uv_provider_underscore_.lock new file mode 100644 index 0000000000..d353763ce3 --- /dev/null +++ b/tests/providers/test_uv_provider/test_uv_provider_underscore_.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_underscore_.toml b/tests/providers/test_uv_provider/test_uv_provider_underscore_.toml new file mode 100644 index 0000000000..99b24ee8f2 --- /dev/null +++ b/tests/providers/test_uv_provider/test_uv_provider_underscore_.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 index 517c7a0459..54b375335d 100644 --- a/tests/test_bump_create_commit_message.py +++ b/tests/test_bump_create_commit_message.py @@ -3,9 +3,9 @@ from textwrap import dedent import pytest -from pytest_mock import MockFixture -from commitizen import bump, cli, cmd, exceptions +from commitizen import bump, cmd, exceptions +from tests.utils import UtilFixture conversion = [ ( @@ -17,23 +17,35 @@ ] -@pytest.mark.parametrize("test_input,expected", conversion) +@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", (True, False)) +@pytest.mark.parametrize("hook_runner", ["pre-commit", "prek"]) +@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"] +def test_bump_pre_commit_changelog(util: UtilFixture, retry, hook_runner): + util.freezer.move_to("2022-04-01") + bump_args = ["bump", "--changelog", "--yes"] if retry: - testargs.append("--retry") + bump_args.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( @@ -58,8 +70,8 @@ def test_bump_pre_commit_changelog(mocker: MockFixture, freezer, retry): ) cmd.run("git add -A") cmd.run('git commit -m "fix: _test"') - cmd.run("pre-commit install") - cli.main() + cmd.run(f"{hook_runner} install") + util.run_cli(*bump_args) # Pre-commit fixed last line adding extra indent and "\" char assert Path("CHANGELOG.md").read_text() == dedent( """\ @@ -72,14 +84,14 @@ def test_bump_pre_commit_changelog(mocker: MockFixture, freezer, retry): ) -@pytest.mark.parametrize("retry", (True, False)) +@pytest.mark.parametrize("hook_runner", ["pre-commit", "prek"]) +@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"] +def test_bump_pre_commit_changelog_fails_always(util: UtilFixture, retry, hook_runner): + util.freezer.move_to("2022-04-01") + bump_args = ["bump", "--changelog", "--yes"] if retry: - testargs.append("--retry") - mocker.patch.object(sys, "argv", testargs) + bump_args.append("--retry") Path(".pre-commit-config.yaml").write_text( dedent( """\ @@ -96,22 +108,21 @@ def test_bump_pre_commit_changelog_fails_always(mocker: MockFixture, freezer, re ) cmd.run("git add -A") cmd.run('git commit -m "feat: forbid changelogs"') - cmd.run("pre-commit install") + cmd.run(f"{hook_runner} install") with pytest.raises(exceptions.BumpCommitFailedError): - cli.main() + util.run_cli(*bump_args) @pytest.mark.usefixtures("tmp_commitizen_project") -def test_bump_with_build_metadata(mocker: MockFixture, freezer): +def test_bump_with_build_metadata(util: UtilFixture): 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() + cz_args = ["bump", "--changelog", "--yes"] + args + util.run_cli(*cz_args) - freezer.move_to("2024-01-01") + util.freezer.move_to("2024-01-01") _add_entry("a", ["--build-metadata", "a.b.c"]) _add_entry("b", []) diff --git a/tests/test_bump_find_increment.py b/tests/test_bump_find_increment.py index ff24ff17a7..8209278ed5 100644 --- a/tests/test_bump_find_increment.py +++ b/tests/test_bump_find_increment.py @@ -32,14 +32,14 @@ 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 + "BREAKING CHANGE: `extends` key in config file is now used for extending other config files", "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 + "BREAKING-CHANGE: `extends` key in config file is now used for extending other config files", "fix(setup.py): future is now required for every python version", ] @@ -85,8 +85,8 @@ @pytest.mark.parametrize( - "messages, expected_type", - ( + ("messages", "expected_type"), + [ (PATCH_INCREMENTS_CC, "PATCH"), (MINOR_INCREMENTS_CC, "MINOR"), (MAJOR_INCREMENTS_BREAKING_CHANGE_CC, "MAJOR"), @@ -96,7 +96,7 @@ (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] @@ -109,12 +109,12 @@ def test_find_increment(messages, expected_type): @pytest.mark.parametrize( - "messages, expected_type", - ( + ("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] diff --git a/tests/test_bump_hooks.py b/tests/test_bump_hooks.py index 70ed7fe0b1..739d1ce6ad 100644 --- a/tests/test_bump_hooks.py +++ b/tests/test_bump_hooks.py @@ -38,5 +38,5 @@ def test_run_error(mocker: MockFixture): 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" + assert result["TEST_FOO"] == "bar" + assert result["TEST_BAR"] == "baz" diff --git a/tests/test_bump_normalize_tag.py b/tests/test_bump_normalize_tag.py index c1eb696afd..558550c5ad 100644 --- a/tests/test_bump_normalize_tag.py +++ b/tests/test_bump_normalize_tag.py @@ -1,6 +1,6 @@ import pytest -from commitizen import bump +from commitizen.tags import TagRules conversion = [ (("1.2.3", "v$version"), "v1.2.3"), @@ -15,8 +15,9 @@ ] -@pytest.mark.parametrize("test_input,expected", conversion) +@pytest.mark.parametrize(("test_input", "expected"), conversion) def test_create_tag(test_input, expected): version, format = test_input - new_tag = bump.normalize_tag(version, format) + 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 index 850b59c166..80823a4e1d 100644 --- a/tests/test_bump_update_version_in_files.py +++ b/tests/test_bump_update_version_in_files.py @@ -1,7 +1,10 @@ +from collections.abc import Callable from pathlib import Path from shutil import copyfile +from typing import TypeAlias import pytest +from _pytest.fixtures import FixtureRequest from commitizen import bump from commitizen.exceptions import CurrentVersionNotFoundError @@ -9,93 +12,89 @@ 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" +SampleFileFixture: TypeAlias = Callable[[str, str], Path] -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 +def sample_file(tmp_path: Path, data_dir: Path) -> SampleFileFixture: + def fixture(source: str, destination: str) -> Path: + tmp_file = tmp_path / destination + copyfile(data_dir / source, 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" - ) + return fixture -@pytest.fixture(scope="function") -def python_version_file(tmpdir, request): - return _copy_sample_file_to_tmpdir(tmpdir, "sample_version.py", "__version__.py") +@pytest.fixture +def commitizen_config_file(sample_file: SampleFileFixture) -> Path: + return sample_file("sample_pyproject.toml", "pyproject.toml") -@pytest.fixture(scope="function") -def inconsistent_python_version_file(tmpdir): - return _copy_sample_file_to_tmpdir( - tmpdir, "inconsistent_version.py", "__version__.py" - ) +@pytest.fixture +def python_version_file(sample_file: SampleFileFixture) -> Path: + return sample_file("sample_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 +def inconsistent_python_version_file(sample_file: SampleFileFixture) -> Path: + return sample_file("inconsistent_version.py", "__version__.py") -@pytest.fixture(scope="function") -def version_repeated_file(tmpdir): - return _copy_sample_file_to_tmpdir( - tmpdir, "repeated_version_number.json", "package.json" - ) +@pytest.fixture +def random_location_version_file(sample_file: SampleFileFixture) -> Path: + return sample_file("sample_cargo.lock", "Cargo.lock") -@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 +def version_repeated_file(sample_file: SampleFileFixture) -> Path: + return sample_file("repeated_version_number.json", "package.json") + + +@pytest.fixture +def docker_compose_file(sample_file: SampleFileFixture) -> Path: + return sample_file("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") +def multiple_versions_to_update_poetry_lock( + sample_file: SampleFileFixture, request: FixtureRequest +) -> Path: + return sample_file(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) +@pytest.fixture +def multiple_versions_increase_string(tmp_path: Path) -> str: + tmp_file = tmp_path / "anyfile" + tmp_file.write_text(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) +@pytest.fixture +def multiple_versions_reduce_string(tmp_path: Path) -> str: + tmp_file = tmp_path / "anyfile" + tmp_file.write_text(MULTIPLE_VERSIONS_REDUCE_STRING) return str(tmp_file) -@pytest.fixture(scope="function") +@pytest.fixture def version_files( - commitizen_config_file, - python_version_file, - version_repeated_file, - docker_compose_file, -): + commitizen_config_file: Path, + python_version_file: Path, + version_repeated_file: Path, + docker_compose_file: Path, +) -> tuple[str, ...]: return ( - commitizen_config_file, - python_version_file, - version_repeated_file, - docker_compose_file, + str(commitizen_config_file), + str(python_version_file), + str(version_repeated_file), + str(docker_compose_file), ) @@ -103,13 +102,16 @@ 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" + old_version, + new_version, + version_files, + check_consistency=False, + encoding="utf-8", ) file_contents = "" for filepath in version_files: - with open(filepath, encoding="utf-8") as f: - file_contents += f.read() + file_contents += Path(filepath).read_text(encoding="utf-8") file_regression.check(file_contents, extension=".txt") @@ -119,9 +121,12 @@ def test_partial_update_of_file(version_repeated_file, file_regression): 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") + bump.update_version_in_files( + old_version, new_version, [location], check_consistency=False, encoding="utf-8" + ) + file_regression.check( + version_repeated_file.read_text(encoding="utf-8"), extension=".json" + ) def test_random_location(random_location_version_file, file_regression): @@ -129,9 +134,12 @@ def test_random_location(random_location_version_file, file_regression): 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") + bump.update_version_in_files( + old_version, new_version, [location], check_consistency=False, encoding="utf-8" + ) + file_regression.check( + random_location_version_file.read_text(encoding="utf-8"), extension=".lock" + ) def test_duplicates_are_change_with_no_regex( @@ -141,9 +149,12 @@ def test_duplicates_are_change_with_no_regex( 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") + bump.update_version_in_files( + old_version, new_version, [location], check_consistency=False, encoding="utf-8" + ) + file_regression.check( + random_location_version_file.read_text(encoding="utf-8"), extension=".lock" + ) def test_version_bump_increase_string_length( @@ -153,9 +164,13 @@ def test_version_bump_increase_string_length( 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") + bump.update_version_in_files( + old_version, new_version, [location], check_consistency=False, encoding="utf-8" + ) + file_regression.check( + Path(multiple_versions_increase_string).read_text(encoding="utf-8"), + extension=".txt", + ) def test_version_bump_reduce_string_length( @@ -165,9 +180,13 @@ def test_version_bump_reduce_string_length( 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") + bump.update_version_in_files( + old_version, new_version, [location], check_consistency=False, encoding="utf-8" + ) + file_regression.check( + Path(multiple_versions_reduce_string).read_text(encoding="utf-8"), + extension=".txt", + ) def test_file_version_inconsistent_error( @@ -197,31 +216,184 @@ def test_file_version_inconsistent_error( assert expected_msg in str(excinfo.value) -def test_multiplt_versions_to_bump( +def test_multiple_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") + bump.update_version_in_files( + old_version, new_version, [location], check_consistency=False, encoding="utf-8" + ) + file_regression.check( + multiple_versions_to_update_poetry_lock.read_text(encoding="utf-8"), + 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) + other = commitizen_config_file.parent / "other.toml" + copyfile(commitizen_config_file, other) - # Prepend full ppath as test assume absolute paths or cwd-relative - version_files = [commitizen_config_file.dirpath("*.toml")] + # Prepend full path as test assume absolute paths or cwd-relative + version_files = [ + str(file_path) for file_path in commitizen_config_file.parent.glob("*.toml") + ] bump.update_version_in_files( - old_version, new_version, version_files, encoding="utf-8" + old_version, + new_version, + version_files, + check_consistency=False, + encoding="utf-8", ) for file in commitizen_config_file, other: file_regression.check(file.read_text("utf-8"), extension=".toml") + + +def test_update_version_in_files_with_check_consistency_true( + version_files: tuple[str, ...], +): + """Test update_version_in_files with check_consistency=True (success case).""" + old_version = "1.2.3" + new_version = "2.0.0" + + # This should succeed because all files contain the current version + updated_files: list[str] = bump.update_version_in_files( + old_version, + new_version, + version_files, + check_consistency=True, + encoding="utf-8", + ) + + # Verify that all files were updated + assert set(updated_files) == set(version_files) + + +def test_update_version_in_files_with_check_consistency_true_failure( + commitizen_config_file, inconsistent_python_version_file +): + """Test update_version_in_files with check_consistency=True (failure case).""" + old_version = "1.2.3" + new_version = "2.0.0" + version_files = [commitizen_config_file, inconsistent_python_version_file] + + # This should fail because inconsistent_python_version_file doesn't contain the current version + 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 {old_version} 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) + + +@pytest.mark.parametrize( + ("encoding", "filename"), + [ + ("latin-1", "test_latin1.txt"), + ("utf-16", "test_utf16.txt"), + ], + ids=["latin-1", "utf-16"], +) +def test_update_version_in_files_with_different_encodings(tmp_path, encoding, filename): + """Test update_version_in_files with different encodings.""" + # Create a test file with the specified encoding + test_file = tmp_path / filename + content = f'version = "1.2.3"\n# This is a test file with {encoding} encoding\n' + test_file.write_text(content, encoding=encoding) + + old_version = "1.2.3" + new_version = "2.0.0" + + updated_files = bump.update_version_in_files( + old_version, + new_version, + [str(test_file)], + check_consistency=True, + encoding=encoding, + ) + + # Verify the file was updated + assert len(updated_files) == 1 + assert str(test_file) in updated_files + + # Verify the content was updated correctly + updated_content = test_file.read_text(encoding=encoding) + assert f'version = "{new_version}"' in updated_content + assert f'version = "{old_version}"' not in updated_content + + +def test_update_version_in_files_return_value(version_files): + """Test that update_version_in_files returns the correct list of updated files.""" + old_version = "1.2.3" + new_version = "2.0.0" + + updated_files = bump.update_version_in_files( + old_version, + new_version, + version_files, + check_consistency=False, + encoding="utf-8", + ) + + # Verify return value is a list + assert isinstance(updated_files, list) + + # Verify all files in the input are in the returned list + assert set(version_files) == set(updated_files) + + # Verify the returned paths are strings + assert all(isinstance(file_path, str) for file_path in updated_files) + + +def test_update_version_in_files_return_value_partial_update(tmp_path): + """Test return value when only some files are updated.""" + # Create two test files + file1 = tmp_path / "file1.txt" + file2 = tmp_path / "file2.txt" + + # File1 contains the version to update + file1.write_text('version = "1.2.3"\n') + + # File2 doesn't contain the version + file2.write_text("some other content\n") + + old_version = "1.2.3" + new_version = "2.0.0" + + updated_files = bump.update_version_in_files( + old_version, + new_version, + [str(file1), str(file2)], + check_consistency=False, + encoding="utf-8", + ) + + # Verify return value + assert isinstance(updated_files, list) + assert len(updated_files) == 2 # Both files should be in the list + assert str(file1) in updated_files + assert str(file2) in updated_files + + # Verify file1 was actually updated + content1 = file1.read_text(encoding="utf-8") + assert f'version = "{new_version}"' in content1 + + # Verify file2 was not changed + content2 = file2.read_text(encoding="utf-8") + assert content2 == "some other content\n" diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 20d59488d3..11c3a60446 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -1,20 +1,30 @@ +from __future__ import annotations + import re +from dataclasses import dataclass from pathlib import Path -from typing import Optional +from typing import TYPE_CHECKING, Any +from unittest.mock import Mock import pytest from jinja2 import FileSystemLoader from commitizen import changelog, git -from commitizen.changelog_formats import ChangelogFormat +from commitizen.commands.changelog import Changelog +from commitizen.config import BaseConfig from commitizen.cz.conventional_commits.conventional_commits import ( ConventionalCommitsCz, ) from commitizen.exceptions import InvalidConfigurationError +from commitizen.version_schemes import Pep440 + +if TYPE_CHECKING: + from commitizen.changelog_formats import ChangelogFormat -COMMITS_DATA = [ +COMMITS_DATA: list[dict[str, Any]] = [ { "rev": "141ee441c9c9da0809c554103a558eb17c30ed17", + "parents": ["6c4948501031b7d6405b54b21d3d635827f9421b"], "title": "bump: version 1.1.1 → 1.2.0", "body": "", "author": "Commitizen", @@ -22,6 +32,7 @@ }, { "rev": "6c4948501031b7d6405b54b21d3d635827f9421b", + "parents": ["ddd220ad515502200fe2dde443614c1075d26238"], "title": "docs: how to create custom bumps", "body": "", "author": "Commitizen", @@ -29,6 +40,7 @@ }, { "rev": "ddd220ad515502200fe2dde443614c1075d26238", + "parents": ["ad17acff2e3a2e141cbc3c6efd7705e4e6de9bfc"], "title": "feat: custom cz plugins now support bumping version", "body": "", "author": "Commitizen", @@ -36,6 +48,7 @@ }, { "rev": "ad17acff2e3a2e141cbc3c6efd7705e4e6de9bfc", + "parents": ["56c8a8da84e42b526bcbe130bd194306f7c7e813"], "title": "docs: added bump gif", "body": "", "author": "Commitizen", @@ -43,6 +56,7 @@ }, { "rev": "56c8a8da84e42b526bcbe130bd194306f7c7e813", + "parents": ["74c6134b1b2e6bb8b07ed53410faabe99b204f36"], "title": "bump: version 1.1.0 → 1.1.1", "body": "", "author": "Commitizen", @@ -50,6 +64,7 @@ }, { "rev": "74c6134b1b2e6bb8b07ed53410faabe99b204f36", + "parents": ["cbc7b5f22c4e74deff4bc92d14e19bd93524711e"], "title": "refactor: changed stdout statements", "body": "", "author": "Commitizen", @@ -57,6 +72,7 @@ }, { "rev": "cbc7b5f22c4e74deff4bc92d14e19bd93524711e", + "parents": ["1ba46f2a63cb9d6e7472eaece21528c8cd28b118"], "title": "fix(bump): commit message now fits better with semver", "body": "", "author": "Commitizen", @@ -64,6 +80,7 @@ }, { "rev": "1ba46f2a63cb9d6e7472eaece21528c8cd28b118", + "parents": ["c35dbffd1bb98bb0b3d1593797e79d1c3366af8f"], "title": "fix: conventional commit 'breaking change' in body instead of title", "body": "closes #16", "author": "Commitizen", @@ -71,6 +88,7 @@ }, { "rev": "c35dbffd1bb98bb0b3d1593797e79d1c3366af8f", + "parents": ["25313397a4ac3dc5b5c986017bee2a614399509d"], "title": "refactor(schema): command logic removed from commitizen base", "body": "", "author": "Commitizen", @@ -78,6 +96,7 @@ }, { "rev": "25313397a4ac3dc5b5c986017bee2a614399509d", + "parents": ["d2f13ac41b4e48995b3b619d931c82451886e6ff"], "title": "refactor(info): command logic removed from commitizen base", "body": "", "author": "Commitizen", @@ -85,6 +104,7 @@ }, { "rev": "d2f13ac41b4e48995b3b619d931c82451886e6ff", + "parents": ["d839e317e5b26671b010584ad8cc6bf362400fa1"], "title": "refactor(example): command logic removed from commitizen base", "body": "", "author": "Commitizen", @@ -92,6 +112,7 @@ }, { "rev": "d839e317e5b26671b010584ad8cc6bf362400fa1", + "parents": ["12d0e65beda969f7983c444ceedc2a01584f4e08"], "title": "refactor(commit): moved most of the commit logic to the commit command", "body": "", "author": "Commitizen", @@ -99,6 +120,7 @@ }, { "rev": "12d0e65beda969f7983c444ceedc2a01584f4e08", + "parents": ["fb4c85abe51c228e50773e424cbd885a8b6c610d"], "title": "docs(README): updated documentation url)", "body": "", "author": "Commitizen", @@ -106,6 +128,7 @@ }, { "rev": "fb4c85abe51c228e50773e424cbd885a8b6c610d", + "parents": ["17efb44d2cd16f6621413691a543e467c7d2dda6"], "title": "docs: mkdocs documentation", "body": "", "author": "Commitizen", @@ -113,6 +136,7 @@ }, { "rev": "17efb44d2cd16f6621413691a543e467c7d2dda6", + "parents": ["6012d9eecfce8163d75c8fff179788e9ad5347da"], "title": "Bump version 1.0.0 → 1.1.0", "body": "", "author": "Commitizen", @@ -120,6 +144,7 @@ }, { "rev": "6012d9eecfce8163d75c8fff179788e9ad5347da", + "parents": ["0c7fb0ca0168864dfc55d83c210da57771a18319"], "title": "test: fixed issues with conf", "body": "", "author": "Commitizen", @@ -127,6 +152,7 @@ }, { "rev": "0c7fb0ca0168864dfc55d83c210da57771a18319", + "parents": ["cb1dd2019d522644da5bdc2594dd6dee17122d7f"], "title": "docs(README): some new information about bump", "body": "", "author": "Commitizen", @@ -134,6 +160,7 @@ }, { "rev": "cb1dd2019d522644da5bdc2594dd6dee17122d7f", + "parents": ["9c7450f85df6bf6be508e79abf00855a30c3c73c"], "title": "feat: new working bump command", "body": "", "author": "Commitizen", @@ -141,6 +168,7 @@ }, { "rev": "9c7450f85df6bf6be508e79abf00855a30c3c73c", + "parents": ["9f3af3772baab167e3fd8775d37f041440184251"], "title": "feat: create version tag", "body": "", "author": "Commitizen", @@ -148,6 +176,7 @@ }, { "rev": "9f3af3772baab167e3fd8775d37f041440184251", + "parents": ["b0d6a3defbfde14e676e7eb34946409297d0221b"], "title": "docs: added new changelog", "body": "", "author": "Commitizen", @@ -155,6 +184,7 @@ }, { "rev": "b0d6a3defbfde14e676e7eb34946409297d0221b", + "parents": ["d630d07d912e420f0880551f3ac94e933f9d3beb"], "title": "feat: update given files with new version", "body": "", "author": "Commitizen", @@ -162,6 +192,7 @@ }, { "rev": "d630d07d912e420f0880551f3ac94e933f9d3beb", + "parents": ["1792b8980c58787906dbe6836f93f31971b1ec2d"], "title": "fix: removed all from commit", "body": "", "author": "Commitizen", @@ -169,6 +200,7 @@ }, { "rev": "1792b8980c58787906dbe6836f93f31971b1ec2d", + "parents": ["52def1ea3555185ba4b936b463311949907e31ec"], "title": "feat(config): new set key, used to set version to cfg", "body": "", "author": "Commitizen", @@ -176,6 +208,7 @@ }, { "rev": "52def1ea3555185ba4b936b463311949907e31ec", + "parents": ["3127e05077288a5e2b62893345590bf1096141b7"], "title": "feat: support for pyproject.toml", "body": "", "author": "Commitizen", @@ -183,6 +216,7 @@ }, { "rev": "3127e05077288a5e2b62893345590bf1096141b7", + "parents": ["fd480ed90a80a6ffa540549408403d5b60d0e90c"], "title": "feat: first semantic version bump implementation", "body": "", "author": "Commitizen", @@ -190,6 +224,7 @@ }, { "rev": "fd480ed90a80a6ffa540549408403d5b60d0e90c", + "parents": ["e4840a059731c0bf488381ffc77e989e85dd81ad"], "title": "fix: fix config file not working", "body": "", "author": "Commitizen", @@ -197,6 +232,7 @@ }, { "rev": "e4840a059731c0bf488381ffc77e989e85dd81ad", + "parents": ["aa44a92d68014d0da98965c0c2cb8c07957d4362"], "title": "refactor: added commands folder, better integration with decli", "body": "", "author": "Commitizen", @@ -204,6 +240,7 @@ }, { "rev": "aa44a92d68014d0da98965c0c2cb8c07957d4362", + "parents": ["58bb709765380dbd46b74ce6e8978515764eb955"], "title": "Bump version: 1.0.0b2 → 1.0.0", "body": "", "author": "Commitizen", @@ -211,6 +248,7 @@ }, { "rev": "58bb709765380dbd46b74ce6e8978515764eb955", + "parents": ["97afb0bb48e72b6feca793091a8a23c706693257"], "title": "docs(README): new badges", "body": "", "author": "Commitizen", @@ -218,6 +256,10 @@ }, { "rev": "97afb0bb48e72b6feca793091a8a23c706693257", + "parents": [ + "9cecb9224aa7fa68d4afeac37eba2a25770ef251", + "e004a90b81ea5b374f118759bce5951202d03d69", + ], "title": "Merge pull request #10 from Woile/feat/decli", "body": "Feat/decli", "author": "Commitizen", @@ -225,6 +267,7 @@ }, { "rev": "9cecb9224aa7fa68d4afeac37eba2a25770ef251", + "parents": ["f5781d1a2954d71c14ade2a6a1a95b91310b2577"], "title": "style: black to files", "body": "", "author": "Commitizen", @@ -232,6 +275,7 @@ }, { "rev": "f5781d1a2954d71c14ade2a6a1a95b91310b2577", + "parents": ["80105fb3c6d45369bc0cbf787bd329fba603864c"], "title": "ci: added travis", "body": "", "author": "Commitizen", @@ -239,6 +283,7 @@ }, { "rev": "80105fb3c6d45369bc0cbf787bd329fba603864c", + "parents": ["a96008496ffefb6b1dd9b251cb479eac6a0487f7"], "title": "refactor: removed delegator, added decli and many tests", "body": "BREAKING CHANGE: API is stable", "author": "Commitizen", @@ -246,6 +291,7 @@ }, { "rev": "a96008496ffefb6b1dd9b251cb479eac6a0487f7", + "parents": ["aab33d13110f26604fb786878856ec0b9e5fc32b"], "title": "docs: updated test command", "body": "", "author": "Commitizen", @@ -253,6 +299,7 @@ }, { "rev": "aab33d13110f26604fb786878856ec0b9e5fc32b", + "parents": ["b73791563d2f218806786090fb49ef70faa51a3a"], "title": "Bump version: 1.0.0b1 → 1.0.0b2", "body": "", "author": "Commitizen", @@ -260,6 +307,7 @@ }, { "rev": "b73791563d2f218806786090fb49ef70faa51a3a", + "parents": ["7aa06a454fb717408b3657faa590731fb4ab3719"], "title": "docs(README): updated to reflect current state", "body": "", "author": "Commitizen", @@ -267,6 +315,10 @@ }, { "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", @@ -274,6 +326,7 @@ }, { "rev": "7c7e96b723c2aaa1aec3a52561f680adf0b60e97", + "parents": ["ed830019581c83ba633bfd734720e6758eca6061"], "title": "Bump version: 0.9.11 → 1.0.0b1", "body": "", "author": "Commitizen", @@ -281,6 +334,7 @@ }, { "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", @@ -288,6 +342,7 @@ }, { "rev": "c52eca6f74f844ab3ffbde61d98ef96071e132b7", + "parents": ["0326652b2657083929507ee66d4d1a0899e861ba"], "title": "Bump version: 0.9.10 → 0.9.11", "body": "", "author": "Commitizen", @@ -295,6 +350,7 @@ }, { "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", @@ -302,6 +358,7 @@ }, { "rev": "b3f89892222340150e32631ae6b7aab65230036f", + "parents": ["5e837bf8ef0735193597372cd2d85e31a8f715b9"], "title": "Bump version: 0.9.9 → 0.9.10", "body": "", "author": "Commitizen", @@ -309,6 +366,7 @@ }, { "rev": "5e837bf8ef0735193597372cd2d85e31a8f715b9", + "parents": ["684e0259cc95c7c5e94854608cd3dcebbd53219e"], "title": "fix: parse scope (this is my punishment for not having tests)", "body": "", "author": "Commitizen", @@ -316,6 +374,7 @@ }, { "rev": "684e0259cc95c7c5e94854608cd3dcebbd53219e", + "parents": ["ca38eac6ff09870851b5c76a6ff0a2a8e5ecda15"], "title": "Bump version: 0.9.8 → 0.9.9", "body": "", "author": "Commitizen", @@ -323,6 +382,7 @@ }, { "rev": "ca38eac6ff09870851b5c76a6ff0a2a8e5ecda15", + "parents": ["64168f18d4628718c49689ee16430549e96c5d4b"], "title": "fix: parse scope empty", "body": "", "author": "Commitizen", @@ -330,6 +390,7 @@ }, { "rev": "64168f18d4628718c49689ee16430549e96c5d4b", + "parents": ["9d4def716ef235a1fa5ae61614366423fbc8256f"], "title": "Bump version: 0.9.7 → 0.9.8", "body": "", "author": "Commitizen", @@ -337,6 +398,7 @@ }, { "rev": "9d4def716ef235a1fa5ae61614366423fbc8256f", + "parents": ["33b0bf1a0a4dc60aac45ed47476d2e5473add09e"], "title": "fix(scope): parse correctly again", "body": "", "author": "Commitizen", @@ -344,6 +406,7 @@ }, { "rev": "33b0bf1a0a4dc60aac45ed47476d2e5473add09e", + "parents": ["696885e891ec35775daeb5fec3ba2ab92c2629e1"], "title": "Bump version: 0.9.6 → 0.9.7", "body": "", "author": "Commitizen", @@ -351,6 +414,7 @@ }, { "rev": "696885e891ec35775daeb5fec3ba2ab92c2629e1", + "parents": ["bef4a86761a3bda309c962bae5d22ce9b57119e4"], "title": "fix(scope): parse correctly", "body": "", "author": "Commitizen", @@ -358,6 +422,7 @@ }, { "rev": "bef4a86761a3bda309c962bae5d22ce9b57119e4", + "parents": ["72472efb80f08ee3fd844660afa012c8cb256e4b"], "title": "Bump version: 0.9.5 → 0.9.6", "body": "", "author": "Commitizen", @@ -365,6 +430,7 @@ }, { "rev": "72472efb80f08ee3fd844660afa012c8cb256e4b", + "parents": ["b5561ce0ab3b56bb87712c8f90bcf37cf2474f1b"], "title": "refactor(conventionalCommit): moved filters to questions instead of message", "body": "", "author": "Commitizen", @@ -372,6 +438,7 @@ }, { "rev": "b5561ce0ab3b56bb87712c8f90bcf37cf2474f1b", + "parents": ["3e31714dc737029d96898f412e4ecd2be1bcd0ce"], "title": "fix(manifest): included missing files", "body": "", "author": "Commitizen", @@ -379,6 +446,7 @@ }, { "rev": "3e31714dc737029d96898f412e4ecd2be1bcd0ce", + "parents": ["9df721e06595fdd216884c36a28770438b4f4a39"], "title": "Bump version: 0.9.4 → 0.9.5", "body": "", "author": "Commitizen", @@ -386,6 +454,7 @@ }, { "rev": "9df721e06595fdd216884c36a28770438b4f4a39", + "parents": ["0cf6ada372470c8d09e6c9e68ebf94bbd5a1656f"], "title": "fix(config): home path for python versions between 3.0 and 3.5", "body": "", "author": "Commitizen", @@ -393,6 +462,7 @@ }, { "rev": "0cf6ada372470c8d09e6c9e68ebf94bbd5a1656f", + "parents": ["973c6b3e100f6f69a3fe48bd8ee55c135b96c318"], "title": "Bump version: 0.9.3 → 0.9.4", "body": "", "author": "Commitizen", @@ -400,6 +470,7 @@ }, { "rev": "973c6b3e100f6f69a3fe48bd8ee55c135b96c318", + "parents": ["dacc86159b260ee98eb5f57941c99ba731a01399"], "title": "feat(cli): added version", "body": "", "author": "Commitizen", @@ -407,6 +478,7 @@ }, { "rev": "dacc86159b260ee98eb5f57941c99ba731a01399", + "parents": ["4368f3c3cbfd4a1ced339212230d854bc5bab496"], "title": "Bump version: 0.9.2 → 0.9.3", "body": "", "author": "Commitizen", @@ -414,6 +486,7 @@ }, { "rev": "4368f3c3cbfd4a1ced339212230d854bc5bab496", + "parents": ["da94133288727d35dae9b91866a25045038f2d38"], "title": "feat(committer): conventional commit is a bit more intelligent now", "body": "", "author": "Commitizen", @@ -421,6 +494,7 @@ }, { "rev": "da94133288727d35dae9b91866a25045038f2d38", + "parents": ["1541f54503d2e1cf39bd777c0ca5ab5eb78772ba"], "title": "docs(README): motivation", "body": "", "author": "Commitizen", @@ -428,6 +502,7 @@ }, { "rev": "1541f54503d2e1cf39bd777c0ca5ab5eb78772ba", + "parents": ["ddc855a637b7879108308b8dbd85a0fd27c7e0e7"], "title": "Bump version: 0.9.1 → 0.9.2", "body": "", "author": "Commitizen", @@ -435,6 +510,7 @@ }, { "rev": "ddc855a637b7879108308b8dbd85a0fd27c7e0e7", + "parents": ["46e9032e18a819e466618c7a014bcb0e9981af9e"], "title": "refactor: renamed conventional_changelog to conventional_commits, not backward compatible", "body": "", "author": "Commitizen", @@ -442,6 +518,7 @@ }, { "rev": "46e9032e18a819e466618c7a014bcb0e9981af9e", + "parents": ["0fef73cd7dc77a25b82e197e7c1d3144a58c1350"], "title": "Bump version: 0.9.0 → 0.9.1", "body": "", "author": "Commitizen", @@ -449,6 +526,7 @@ }, { "rev": "0fef73cd7dc77a25b82e197e7c1d3144a58c1350", + "parents": [], "title": "fix(setup.py): future is now required for every python version", "body": "", "author": "Commitizen", @@ -479,55 +557,53 @@ @pytest.fixture -def gitcommits() -> list: - commits = [ +def gitcommits() -> list[git.GitCommit]: + return [ 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 +def tags() -> list[git.GitTag]: + return [git.GitTag(*tag) for tag in 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 changelog_content(data_dir: Path) -> str: + changelog = data_dir / "CHANGELOG_FOR_TEST.md" + return changelog.read_text(encoding="utf-8") 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 + assert changelog.get_commit_tag(commit, tags) == 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 + assert changelog.get_commit_tag(commit, tags) is None @pytest.mark.parametrize("test_input", TAGS) def test_valid_tag_included_in_changelog(test_input): tag = git.GitTag(*test_input) - assert changelog.tag_included_in_changelog(tag, [], False) + rules = changelog.TagRules() + assert rules.include_in_changelog(tag) is True def test_invalid_tag_included_in_changelog(): tag = git.GitTag("not_a_version", "rev", "date") - assert not changelog.tag_included_in_changelog(tag, [], False) + rules = changelog.TagRules() + assert rules.include_in_changelog(tag) is False COMMITS_TREE = ( @@ -1076,12 +1152,15 @@ def test_invalid_tag_included_in_changelog(): ) -@pytest.mark.parametrize("merge_prereleases", (True, False)) +@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, merge_prerelease=merge_prereleases + gitcommits, tags, parser, changelog_pattern, rules=rules ) expected = ( COMMITS_TREE_AFTER_MERGED_PRERELEASES if merge_prereleases else COMMITS_TREE @@ -1101,22 +1180,20 @@ def test_generate_tree_from_commits(gitcommits, tags, merge_prereleases): 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 - ) + tree = changelog.generate_tree_from_commits([], tags, parser, changelog_pattern) assert tuple(tree) == ({"changes": {}, "date": "", "version": "Unreleased"},) @pytest.mark.parametrize( - "change_type_order, expected_reordering", - ( + ("change_type_order", "expected_reordering"), + [ ([], {}), ( ["BREAKING CHANGE", "refactor"], @@ -1131,30 +1208,30 @@ def test_generate_tree_from_commits_with_no_commits(tags): }, }, ), - ), + ], ) -def test_order_changelog_tree(change_type_order, expected_reordering): - tree = changelog.order_changelog_tree(COMMITS_TREE, change_type_order) +def test_generate_ordered_changelog_tree(change_type_order, expected_reordering): + tree = changelog.generate_ordered_changelog_tree(COMMITS_TREE, change_type_order) for index, entry in enumerate(tuple(tree)): - version = tree[index]["version"] + version = entry["version"] if version in expected_reordering: # Verify that all keys are present - assert [*tree[index].keys()] == [*COMMITS_TREE[index].keys()] + assert [*entry.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 [*entry["changes"].keys()] == expected["sorted"] assert [*COMMITS_TREE[index]["changes"].keys()] == expected["original"] else: - assert [*entry["changes"].keys()] == [*tree[index]["changes"].keys()] + assert [*entry["changes"].keys()] == [*entry["changes"].keys()] -def test_order_changelog_tree_raises(): +def test_generate_ordered_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) + list(changelog.generate_ordered_changelog_tree(COMMITS_TREE, change_type_order)) - assert "Change types contain duplicates types" in str(excinfo) + assert "Change types contain duplicated types" in str(excinfo) def test_render_changelog( @@ -1367,7 +1444,7 @@ def test_render_changelog_with_changelog_message_builder_hook_multiple_entries( 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}" + msg["message"] = f"Message #{idx}" return messages parser = ConventionalCommitsCz.commit_parser @@ -1384,7 +1461,7 @@ def changelog_message_builder_hook(message: dict, commit: git.GitCommit): result = changelog.render_changelog(tree, loader, template) for idx in range(3): - assert "Message #{idx}" in result + assert f"Message #{idx}" in result def test_changelog_message_builder_hook_can_access_and_modify_change_type( @@ -1413,15 +1490,15 @@ def changelog_message_builder_hook(message: dict, commit: git.GitCommit): 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" + 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: + def changelog_release_hook(release: dict, tag: git.GitTag | None) -> dict: release["extra"] = "whatever" return release @@ -1451,3 +1528,158 @@ 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) + + +def test_get_next_tag_name_after_version(tags): + # Test finding next tag after a version + next_tag_name = changelog.get_next_tag_name_after_version(tags, "v1.2.0") + assert next_tag_name == "v1.1.1" + + next_tag_name = changelog.get_next_tag_name_after_version(tags, "v1.1.0") + assert next_tag_name == "v1.0.0" + + # Test finding last tag when given version is last + last_tag_name = changelog.get_next_tag_name_after_version(tags, "v0.9.1") + assert last_tag_name is None + + # Test error when version not found + with pytest.raises(changelog.NoCommitsFoundError) as exc_info: + changelog.get_next_tag_name_after_version(tags, "nonexistent") + assert "Could not find a valid revision range" in str(exc_info.value) + + +@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("Invalid version tag:") == 2 + assert captured.err.count("not-a-version") == 2 + + +@pytest.mark.usefixtures("in_repo_root") +def test_changelog_file_name_from_args_and_config(): + mock_config = Mock(spec=BaseConfig) + mock_path = Mock(spec=Path) + mock_path.parent = Path("/my/project") + mock_config.path = mock_path + mock_config.settings = { + "name": "cz_conventional_commits", + "changelog_file": "CHANGELOG.md", + "encoding": "utf-8", + "changelog_start_rev": "v1.0.0", + "tag_format": "$version", + "legacy_tag_formats": [], + "ignored_tag_formats": [], + "incremental": True, + "changelog_merge_prerelease": True, + } + + args = { + "file_name": "CUSTOM.md", + "unreleased_version": "1.0.1", + } + changelog = Changelog(mock_config, args) + assert ( + Path(changelog.file_name).resolve() == Path("/my/project/CUSTOM.md").resolve() + ) + + args = {"unreleased_version": "1.0.1"} + changelog = Changelog(mock_config, args) + assert ( + Path(changelog.file_name).resolve() + == Path("/my/project/CHANGELOG.md").resolve() + ) diff --git a/tests/test_changelog_format_asciidoc.py b/tests/test_changelog_format_asciidoc.py index 89740d2147..a56a05ba08 100644 --- a/tests/test_changelog_format_asciidoc.py +++ b/tests/test_changelog_format_asciidoc.py @@ -1,12 +1,16 @@ from __future__ import annotations -from pathlib import Path +from typing import TYPE_CHECKING import pytest -from commitizen.changelog import Metadata +from commitizen.changelog import IncrementalMergeInfo, Metadata from commitizen.changelog_formats.asciidoc import AsciiDoc -from commitizen.config.base_config import BaseConfig + +if TYPE_CHECKING: + from pathlib import Path + + from commitizen.config.base_config import BaseConfig CHANGELOG_A = """ = Changelog @@ -55,6 +59,7 @@ """ EXPECTED_C = Metadata( latest_version="1.0.0", + latest_version_tag="v1.0.0", latest_version_position=3, unreleased_end=3, unreleased_start=1, @@ -72,23 +77,54 @@ 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] - 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", "10.0.0-next.3"), ), - ("=== 0.19.1 (Jan 7, 2020)", "0.19.1"), - ("== 1.0.0", "1.0.0"), - ("== v1.0.0", "1.0.0"), - ("== v1.0.0 - (2012-24-32)", "1.0.0"), - ("= version 2020.03.24", "2020.03.24"), + ("=== 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), @@ -96,9 +132,9 @@ def format(config: BaseConfig) -> AsciiDoc: ] -@pytest.mark.parametrize("line_from_changelog,output_version", VERSIONS_EXAMPLES) +@pytest.mark.parametrize(("line_from_changelog", "output_version"), VERSIONS_EXAMPLES) def test_changelog_detect_version( - line_from_changelog: str, output_version: str, format: AsciiDoc + 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 @@ -111,7 +147,7 @@ def test_changelog_detect_version( ] -@pytest.mark.parametrize("line_from_changelog,output_title", TITLES_EXAMPLES) +@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 ): @@ -120,18 +156,54 @@ def test_parse_title_type_of_line( @pytest.mark.parametrize( - "content, expected", - ( + ("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( +def test_get_metadata( 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 + + +def test_get_latest_full_release_no_file(format: AsciiDoc): + assert format.get_latest_full_release("/nonexistent") == IncrementalMergeInfo() + + +@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 index ab7c65453c..1b75ed1f7f 100644 --- a/tests/test_changelog_format_markdown.py +++ b/tests/test_changelog_format_markdown.py @@ -1,12 +1,16 @@ from __future__ import annotations -from pathlib import Path +from typing import TYPE_CHECKING import pytest from commitizen.changelog import Metadata from commitizen.changelog_formats.markdown import Markdown -from commitizen.config.base_config import BaseConfig + +if TYPE_CHECKING: + from pathlib import Path + + from commitizen.config.base_config import BaseConfig CHANGELOG_A = """ # Changelog @@ -55,6 +59,7 @@ """ EXPECTED_C = Metadata( latest_version="1.0.0", + latest_version_tag="v1.0.0", latest_version_position=3, unreleased_end=3, unreleased_start=1, @@ -72,23 +77,54 @@ 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] - 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", "10.0.0-next.3"), ), - ("### 0.19.1 (Jan 7, 2020)", "0.19.1"), - ("## 1.0.0", "1.0.0"), - ("## v1.0.0", "1.0.0"), - ("## v1.0.0 - (2012-24-32)", "1.0.0"), - ("# version 2020.03.24", "2020.03.24"), + ("### 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), @@ -96,9 +132,9 @@ def format(config: BaseConfig) -> Markdown: ] -@pytest.mark.parametrize("line_from_changelog,output_version", VERSIONS_EXAMPLES) +@pytest.mark.parametrize(("line_from_changelog", "output_version"), VERSIONS_EXAMPLES) def test_changelog_detect_version( - line_from_changelog: str, output_version: str, format: Markdown + 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 @@ -111,7 +147,7 @@ def test_changelog_detect_version( ] -@pytest.mark.parametrize("line_from_changelog,output_title", TITLES_EXAMPLES) +@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 ): @@ -120,18 +156,56 @@ def test_parse_title_type_of_line( @pytest.mark.parametrize( - "content, expected", - ( + ("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( +def test_get_metadata( 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 index 46a11ebcdf..00f7740737 100644 --- a/tests/test_changelog_format_restructuredtext.py +++ b/tests/test_changelog_format_restructuredtext.py @@ -1,18 +1,24 @@ 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 +from commitizen.changelog_formats.restructuredtext import ( + RestructuredText, + _is_overlined_title, + _is_underlined_title, +) if TYPE_CHECKING: + from pathlib import Path + from _pytest.mark.structures import ParameterSet + from commitizen.config.base_config import BaseConfig + CASES: list[ParameterSet] = [] @@ -22,6 +28,7 @@ def case( 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, ): @@ -30,6 +37,7 @@ def case( 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, @@ -93,6 +101,7 @@ def case( ====== """, latest_version="1.0.0", + latest_version_tag="v1.0.0", latest_version_position=3, unreleased_start=0, unreleased_end=3, @@ -273,14 +282,42 @@ def case( """, ) +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.mark.parametrize("content, expected", CASES) -def test_get_matadata( +@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_metadata( tmp_path: Path, format: RestructuredText, content: str, expected: Metadata ): changelog = tmp_path / format.default_changelog_file @@ -290,21 +327,61 @@ def test_get_matadata( @pytest.mark.parametrize( - "text, expected", + ("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): +def test_is_underlined_title(text: str, expected: bool): _, first, second = dedent(text).splitlines() - assert format.is_underlined_title(first, second) is expected + assert _is_underlined_title(first, second) is expected @pytest.mark.parametrize( - "text, expected", + ("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): +def test_is_overlined_title(text: str, expected: bool): _, first, second, third = dedent(text).splitlines() - assert format.is_overlined_title(first, second, third) is expected + assert _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 index e382e1c746..481f903a7a 100644 --- a/tests/test_changelog_format_textile.py +++ b/tests/test_changelog_format_textile.py @@ -1,12 +1,16 @@ from __future__ import annotations -from pathlib import Path +from typing import TYPE_CHECKING import pytest from commitizen.changelog import Metadata from commitizen.changelog_formats.textile import Textile -from commitizen.config.base_config import BaseConfig + +if TYPE_CHECKING: + from pathlib import Path + + from commitizen.config.base_config import BaseConfig CHANGELOG_A = """ h1. Changelog @@ -55,6 +59,7 @@ """ EXPECTED_C = Metadata( latest_version="1.0.0", + latest_version_tag="v1.0.0", latest_version_position=3, unreleased_end=3, unreleased_start=1, @@ -72,23 +77,47 @@ 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"), + ("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", "10.0.0-next.3"), ), - ("h3. 0.19.1 (Jan 7, 2020)", "0.19.1"), - ("h2. 1.0.0", "1.0.0"), - ("h2. v1.0.0", "1.0.0"), - ("h2. v1.0.0 - (2012-24-32)", "1.0.0"), - ("h1. version 2020.03.24", "2020.03.24"), + ("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), @@ -96,9 +125,9 @@ def format(config: BaseConfig) -> Textile: ] -@pytest.mark.parametrize("line_from_changelog,output_version", VERSIONS_EXAMPLES) +@pytest.mark.parametrize(("line_from_changelog", "output_version"), VERSIONS_EXAMPLES) def test_changelog_detect_version( - line_from_changelog: str, output_version: str, format: Textile + 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 @@ -111,7 +140,7 @@ def test_changelog_detect_version( ] -@pytest.mark.parametrize("line_from_changelog,output_title", TITLES_EXAMPLES) +@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 ): @@ -120,18 +149,51 @@ def test_parse_title_type_of_line( @pytest.mark.parametrize( - "content, expected", - ( + ("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( +def test_get_metadata( 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 index dec23720dc..6ffbc8dc33 100644 --- a/tests/test_changelog_formats.py +++ b/tests/test_changelog_formats.py @@ -1,32 +1,36 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest from commitizen import defaults from commitizen.changelog_formats import ( KNOWN_CHANGELOG_FORMATS, ChangelogFormat, + _guess_changelog_format, get_changelog_format, - guess_changelog_format, ) -from commitizen.config.base_config import BaseConfig from commitizen.exceptions import ChangelogFormatUnknown +if TYPE_CHECKING: + from commitizen.config.base_config import BaseConfig + @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 + 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 + assert _guess_changelog_format(f"CHANGELOG.{ext}") is format -@pytest.mark.parametrize("filename", ("CHANGELOG", "NEWS", "file.unknown", None)) +@pytest.mark.parametrize("filename", ["CHANGELOG", "NEWS", "file.unknown", None]) def test_guess_format_unknown(filename: str): - assert guess_changelog_format(filename) is None + assert _guess_changelog_format(filename) is None @pytest.mark.parametrize( - "name, expected", + ("name", "expected"), [ pytest.param(name, format, id=name) for name, format in KNOWN_CHANGELOG_FORMATS.items() @@ -37,7 +41,7 @@ def test_get_format(config: BaseConfig, name: str, expected: type[ChangelogForma assert isinstance(get_changelog_format(config), expected) -@pytest.mark.parametrize("filename", (None, "")) +@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( @@ -46,14 +50,14 @@ def test_get_format_empty_filename(config: BaseConfig, filename: str | None): ) -@pytest.mark.parametrize("filename", (None, "")) +@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")) +@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 index a91e633128..c1f6d5beda 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,6 +1,7 @@ import os import subprocess import sys +import types from functools import partial import pytest @@ -14,69 +15,69 @@ NoCommandFoundError, NotAGitProjectError, ) +from tests.utils import UtilFixture -def test_sysexit_no_argv(mocker: MockFixture, capsys): - testargs = ["cz"] - mocker.patch.object(sys, "argv", testargs) - +@pytest.mark.usefixtures("python_version", "consistent_terminal_output") +def test_no_argv(util: UtilFixture, capsys, file_regression): with pytest.raises(ExpectedExit): - cli.main() - out, _ = capsys.readouterr() - assert out.startswith("usage") + util.run_cli() + out, err = capsys.readouterr() + assert out == "" + file_regression.check(err, extension=".txt") -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) +@pytest.mark.parametrize( + "arg", + [ + "--invalid-arg", + "invalidCommand", + ], +) +@pytest.mark.usefixtures("python_version", "consistent_terminal_output") +def test_invalid_command(util: UtilFixture, capsys, file_regression, arg): + with pytest.raises(NoCommandFoundError): + util.run_cli(arg) + out, err = capsys.readouterr() + assert out == "" + file_regression.check(err, extension=".txt") + +def test_cz_config_file_without_correct_file_path(util: UtilFixture, capsys): with pytest.raises(ConfigFileNotFound) as excinfo: - cli.main() + util.run_cli("--config", "./config/pyproject.toml", "example") 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) - +def test_cz_with_arg_but_without_command(util: UtilFixture): with pytest.raises(NoCommandFoundError) as excinfo: - cli.main() + util.run_cli("--name", "cz_jira") 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() +def test_name(util: UtilFixture, capsys): + util.run_cli("-n", "cz_jira", "example") 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() +def test_name_default_value(util: UtilFixture, capsys): + util.run_cli("example") 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() +def test_ls(util: UtilFixture, capsys): + util.run_cli("-n", "cz_jira", "ls") 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() +def test_arg_debug(util: UtilFixture): + util.run_cli("--debug", "info") excepthook = sys.excepthook # `sys.excepthook` is replaced by a `partial` in `cli.main` # it's not a normal function @@ -136,49 +137,98 @@ def test_commitizen_excepthook_no_raises(capsys): assert excinfo.value.code == 0 -def test_parse_no_raise_single_integer(): - input_str = "1" +@pytest.mark.parametrize( + ("input_str", "expected_result"), + [ + pytest.param("1", [1], id="single_code"), + pytest.param("1,2,3", [1, 2, 3], id="multiple_number_codes"), + pytest.param( + "NO_COMMITIZEN_FOUND,NO_COMMITS_FOUND,NO_PATTERN_MAP", + [1, 3, 5], + id="string_codes", + ), + pytest.param( + "NO_COMMITIZEN_FOUND,2,NO_COMMITS_FOUND,4", + [1, 2, 3, 4], + id="number_and_string_codes", + ), + pytest.param( + "NO_COMMITIZEN_FOUND,2,nothing,4", + [1, 2, 4], + id="number_and_string_codes_and_invalid_code", + ), + ], +) +def test_parse_no_raise(input_str, expected_result): result = cli.parse_no_raise(input_str) - assert result == [1] + assert result == expected_result -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_unknown_args_raises(util: UtilFixture): + with pytest.raises(InvalidCommandArgumentError) as excinfo: + util.run_cli("c", "-this_arg_is_not_supported") + assert "Invalid commitizen arguments were found" in str(excinfo.value) -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_unknown_args_before_double_dash_raises(util: UtilFixture): + with pytest.raises(InvalidCommandArgumentError) as excinfo: + util.run_cli("c", "-this_arg_is_not_supported", "--") + assert "Invalid commitizen arguments were found before -- separator" in str( + excinfo.value + ) -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_commitizen_excepthook_non_commitizen_exception(mocker: MockFixture): + """Test that commitizen_excepthook delegates to sys.__excepthook__ for non-CommitizenException.""" + # Mock the original excepthook + mock_original_excepthook = mocker.Mock() + mocker.patch("commitizen.cli.sys.__excepthook__", mock_original_excepthook) + # Create a regular exception + test_exception = ValueError("test error") -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] + # Call commitizen_excepthook with the regular exception + cli.commitizen_excepthook(ValueError, test_exception, None) + # Verify sys.__excepthook__ was called with correct arguments + mock_original_excepthook.assert_called_once_with(ValueError, test_exception, None) -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_commitizen_excepthook_non_commitizen_exception_with_traceback( + mocker: MockFixture, +): + """Test that commitizen_excepthook handles traceback correctly for non-CommitizenException.""" + # Mock the original excepthook + mock_original_excepthook = mocker.Mock() + mocker.patch("commitizen.cli.sys.__excepthook__", mock_original_excepthook) -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 + # Create a regular exception with a traceback + test_exception = ValueError("test error") + test_traceback = mocker.Mock(spec=types.TracebackType) + + # Call commitizen_excepthook with the regular exception and traceback + cli.commitizen_excepthook(ValueError, test_exception, test_traceback) + + # Verify sys.__excepthook__ was called with correct arguments including traceback + mock_original_excepthook.assert_called_once_with( + ValueError, test_exception, test_traceback ) + + +def test_commitizen_excepthook_non_commitizen_exception_with_invalid_traceback( + mocker: MockFixture, +): + """Test that commitizen_excepthook handles invalid traceback correctly for non-CommitizenException.""" + # Mock the original excepthook + mock_original_excepthook = mocker.Mock() + mocker.patch("commitizen.cli.sys.__excepthook__", mock_original_excepthook) + + # Create a regular exception with an invalid traceback + test_exception = ValueError("test error") + test_traceback = mocker.Mock() # Not a TracebackType + + # Call commitizen_excepthook with the regular exception and invalid traceback + cli.commitizen_excepthook(ValueError, test_exception, test_traceback) + + # Verify sys.__excepthook__ was called with None as traceback + mock_original_excepthook.assert_called_once_with(ValueError, test_exception, None) diff --git a/tests/test_cli/test_invalid_command_py_3_10___invalid_arg_.txt b/tests/test_cli/test_invalid_command_py_3_10___invalid_arg_.txt new file mode 100644 index 0000000000..148b4eacdb --- /dev/null +++ b/tests/test_cli/test_invalid_command_py_3_10___invalid_arg_.txt @@ -0,0 +1,4 @@ +usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE] + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} + ... +cz: error: the following arguments are required: {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} diff --git a/tests/test_cli/test_invalid_command_py_3_10_invalidCommand_.txt b/tests/test_cli/test_invalid_command_py_3_10_invalidCommand_.txt new file mode 100644 index 0000000000..e2d4416b81 --- /dev/null +++ b/tests/test_cli/test_invalid_command_py_3_10_invalidCommand_.txt @@ -0,0 +1,4 @@ +usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE] + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} + ... +cz: error: argument {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}: invalid choice: 'invalidCommand' (choose from 'init', 'commit', 'c', 'ls', 'example', 'info', 'schema', 'bump', 'changelog', 'ch', 'check', 'version') diff --git a/tests/test_cli/test_invalid_command_py_3_11___invalid_arg_.txt b/tests/test_cli/test_invalid_command_py_3_11___invalid_arg_.txt new file mode 100644 index 0000000000..148b4eacdb --- /dev/null +++ b/tests/test_cli/test_invalid_command_py_3_11___invalid_arg_.txt @@ -0,0 +1,4 @@ +usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE] + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} + ... +cz: error: the following arguments are required: {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} diff --git a/tests/test_cli/test_invalid_command_py_3_11_invalidCommand_.txt b/tests/test_cli/test_invalid_command_py_3_11_invalidCommand_.txt new file mode 100644 index 0000000000..e2d4416b81 --- /dev/null +++ b/tests/test_cli/test_invalid_command_py_3_11_invalidCommand_.txt @@ -0,0 +1,4 @@ +usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE] + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} + ... +cz: error: argument {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}: invalid choice: 'invalidCommand' (choose from 'init', 'commit', 'c', 'ls', 'example', 'info', 'schema', 'bump', 'changelog', 'ch', 'check', 'version') diff --git a/tests/test_cli/test_invalid_command_py_3_12___invalid_arg_.txt b/tests/test_cli/test_invalid_command_py_3_12___invalid_arg_.txt new file mode 100644 index 0000000000..148b4eacdb --- /dev/null +++ b/tests/test_cli/test_invalid_command_py_3_12___invalid_arg_.txt @@ -0,0 +1,4 @@ +usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE] + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} + ... +cz: error: the following arguments are required: {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} diff --git a/tests/test_cli/test_invalid_command_py_3_12_invalidCommand_.txt b/tests/test_cli/test_invalid_command_py_3_12_invalidCommand_.txt new file mode 100644 index 0000000000..c92220c4dc --- /dev/null +++ b/tests/test_cli/test_invalid_command_py_3_12_invalidCommand_.txt @@ -0,0 +1,4 @@ +usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE] + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} + ... +cz: error: argument {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}: invalid choice: 'invalidCommand' (choose from init, commit, c, ls, example, info, schema, bump, changelog, ch, check, version) diff --git a/tests/test_cli/test_invalid_command_py_3_13___invalid_arg_.txt b/tests/test_cli/test_invalid_command_py_3_13___invalid_arg_.txt new file mode 100644 index 0000000000..4f0ba2b148 --- /dev/null +++ b/tests/test_cli/test_invalid_command_py_3_13___invalid_arg_.txt @@ -0,0 +1,3 @@ +usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE] + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} ... +cz: error: the following arguments are required: {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} diff --git a/tests/test_cli/test_invalid_command_py_3_13_invalidCommand_.txt b/tests/test_cli/test_invalid_command_py_3_13_invalidCommand_.txt new file mode 100644 index 0000000000..749066c556 --- /dev/null +++ b/tests/test_cli/test_invalid_command_py_3_13_invalidCommand_.txt @@ -0,0 +1,3 @@ +usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE] + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} ... +cz: error: argument {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}: invalid choice: 'invalidCommand' (choose from init, commit, c, ls, example, info, schema, bump, changelog, ch, check, version) diff --git a/tests/test_cli/test_invalid_command_py_3_14___invalid_arg_.txt b/tests/test_cli/test_invalid_command_py_3_14___invalid_arg_.txt new file mode 100644 index 0000000000..4f0ba2b148 --- /dev/null +++ b/tests/test_cli/test_invalid_command_py_3_14___invalid_arg_.txt @@ -0,0 +1,3 @@ +usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE] + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} ... +cz: error: the following arguments are required: {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} diff --git a/tests/test_cli/test_invalid_command_py_3_14_invalidCommand_.txt b/tests/test_cli/test_invalid_command_py_3_14_invalidCommand_.txt new file mode 100644 index 0000000000..749066c556 --- /dev/null +++ b/tests/test_cli/test_invalid_command_py_3_14_invalidCommand_.txt @@ -0,0 +1,3 @@ +usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE] + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} ... +cz: error: argument {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}: invalid choice: 'invalidCommand' (choose from init, commit, c, ls, example, info, schema, bump, changelog, ch, check, version) diff --git a/tests/test_cli/test_no_argv_py_3_10_.txt b/tests/test_cli/test_no_argv_py_3_10_.txt new file mode 100644 index 0000000000..69f410e96d --- /dev/null +++ b/tests/test_cli/test_no_argv_py_3_10_.txt @@ -0,0 +1,34 @@ +usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE] + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} + ... + +Commitizen is a powerful release management tool that helps teams maintain consistent and meaningful commit messages while automating version management. +For more information, please visit https://commitizen-tools.github.io/commitizen + +options: + -h, --help show this help message and exit + --config CONFIG The path to the 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 raise 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 Initialize 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 files). + check Validate that a commit message matches the commitizen + schema. + version Get the version of the installed commitizen or the + current project (default: installed commitizen). diff --git a/tests/test_cli/test_no_argv_py_3_11_.txt b/tests/test_cli/test_no_argv_py_3_11_.txt new file mode 100644 index 0000000000..69f410e96d --- /dev/null +++ b/tests/test_cli/test_no_argv_py_3_11_.txt @@ -0,0 +1,34 @@ +usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE] + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} + ... + +Commitizen is a powerful release management tool that helps teams maintain consistent and meaningful commit messages while automating version management. +For more information, please visit https://commitizen-tools.github.io/commitizen + +options: + -h, --help show this help message and exit + --config CONFIG The path to the 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 raise 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 Initialize 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 files). + check Validate that a commit message matches the commitizen + schema. + version Get the version of the installed commitizen or the + current project (default: installed commitizen). diff --git a/tests/test_cli/test_no_argv_py_3_12_.txt b/tests/test_cli/test_no_argv_py_3_12_.txt new file mode 100644 index 0000000000..69f410e96d --- /dev/null +++ b/tests/test_cli/test_no_argv_py_3_12_.txt @@ -0,0 +1,34 @@ +usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE] + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} + ... + +Commitizen is a powerful release management tool that helps teams maintain consistent and meaningful commit messages while automating version management. +For more information, please visit https://commitizen-tools.github.io/commitizen + +options: + -h, --help show this help message and exit + --config CONFIG The path to the 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 raise 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 Initialize 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 files). + check Validate that a commit message matches the commitizen + schema. + version Get the version of the installed commitizen or the + current project (default: installed commitizen). diff --git a/tests/test_cli/test_no_argv_py_3_13_.txt b/tests/test_cli/test_no_argv_py_3_13_.txt new file mode 100644 index 0000000000..b47528ec3e --- /dev/null +++ b/tests/test_cli/test_no_argv_py_3_13_.txt @@ -0,0 +1,33 @@ +usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE] + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} ... + +Commitizen is a powerful release management tool that helps teams maintain consistent and meaningful commit messages while automating version management. +For more information, please visit https://commitizen-tools.github.io/commitizen + +options: + -h, --help show this help message and exit + --config CONFIG The path to the configuration file. + --debug Use debug mode. + -n, --name NAME Use the given commitizen (default: + cz_conventional_commits). + -nr, --no-raise NO_RAISE + Comma-separated error codes that won't raise 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 Initialize 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 files). + check Validate that a commit message matches the commitizen + schema. + version Get the version of the installed commitizen or the + current project (default: installed commitizen). diff --git a/tests/test_cli/test_no_argv_py_3_14_.txt b/tests/test_cli/test_no_argv_py_3_14_.txt new file mode 100644 index 0000000000..b47528ec3e --- /dev/null +++ b/tests/test_cli/test_no_argv_py_3_14_.txt @@ -0,0 +1,33 @@ +usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE] + {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version} ... + +Commitizen is a powerful release management tool that helps teams maintain consistent and meaningful commit messages while automating version management. +For more information, please visit https://commitizen-tools.github.io/commitizen + +options: + -h, --help show this help message and exit + --config CONFIG The path to the configuration file. + --debug Use debug mode. + -n, --name NAME Use the given commitizen (default: + cz_conventional_commits). + -nr, --no-raise NO_RAISE + Comma-separated error codes that won't raise 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 Initialize 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 files). + check Validate that a commit message matches the commitizen + schema. + version Get the version of the installed commitizen or the + current project (default: installed commitizen). diff --git a/tests/test_cmd.py b/tests/test_cmd.py index e8a869e01c..77a67cf4e2 100644 --- a/tests/test_cmd.py +++ b/tests/test_cmd.py @@ -5,15 +5,18 @@ # https://docs.python.org/3/howto/unicode.html -def test_valid_utf8_encoded_strings(): - valid_strings = ( +@pytest.mark.parametrize( + "s", + [ "", "ascii", "🤦🏻‍♂️", "﷽", "\u0000", - ) - assert all(s == cmd._try_decode(s.encode("utf-8")) for s in valid_strings) + ], +) +def test_valid_utf8_encoded_strings(s: str): + assert s == cmd._try_decode(s.encode("utf-8")) # A word of caution: just because an encoding can be guessed for a given @@ -22,24 +25,25 @@ def test_valid_utf8_encoded_strings(): # 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 = ( +@pytest.mark.parametrize( + "s", + [ "", "ascii", "äöüß", "ça va", "jak se máte", - ) - for s in valid_strings: - assert cmd._try_decode(s.encode("cp1250")) or True + ], +) +def test_valid_cp1250_encoded_strings(s: str): + """Pick a random, non-utf8 encoding to test.""" + # We just want to make sure it doesn't raise an exception + cmd._try_decode(s.encode("cp1250")) 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) + with pytest.raises(CharacterSetDecodeError): + cmd._try_decode(b"\x73\xe2\x9d\xff\x00") def test_always_fail_decode(): diff --git a/tests/test_conf.py b/tests/test_conf.py index 1cbbc57aca..94bca4e77b 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -8,10 +8,13 @@ import pytest import yaml -from commitizen import config, defaults, git +from commitizen import cmd, config, defaults, git +from commitizen.config.json_config import JsonConfig +from commitizen.config.toml_config import TomlConfig +from commitizen.config.yaml_config import YAMLConfig from commitizen.exceptions import ConfigFileIsEmpty, InvalidConfigurationError -PYPROJECT = """ +TOML_STR = """ [tool.commitizen] name = "cz_jira" version = "1.0.0" @@ -27,12 +30,17 @@ "scripts/generate_documentation.sh" ] post_bump_hooks = ["scripts/slack_notification.sh"] +""" + +PYPROJECT = f""" +{TOML_STR} [tool.black] line-length = 88 target-version = ['py36', 'py37', 'py38'] """ + DICT_CONFIG = { "commitizen": { "name": "cz_jira", @@ -72,10 +80,19 @@ "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!"], + "allowed_prefixes": [ + "Merge", + "Revert", + "Pull request", + "fixup!", + "squash!", + "amend!", + ], "version_files": ["commitizen/__version__.py", "pyproject.toml"], "style": [["pointer", "reverse"], ["question", "underline"]], "changelog_file": "CHANGELOG.md", @@ -93,6 +110,8 @@ "always_signoff": False, "template": None, "extras": {}, + "breaking_change_exclamation_in_title": False, + "message_length_limit": 0, } _new_settings: dict[str, Any] = { @@ -101,10 +120,19 @@ "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!"], + "allowed_prefixes": [ + "Merge", + "Revert", + "Pull request", + "fixup!", + "squash!", + "amend!", + ], "version_files": ["commitizen/__version__.py", "pyproject.toml"], "style": [["pointer", "reverse"], ["question", "underline"]], "changelog_file": "CHANGELOG.md", @@ -122,33 +150,36 @@ "always_signoff": False, "template": None, "extras": {}, + "breaking_change_exclamation_in_title": False, + "message_length_limit": 0, } @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): +def config_files_manager(request, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + filename = request.param + path = tmp_path / filename + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("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) + return + + +@pytest.mark.usefixtures("in_repo_root") +def test_find_git_project_root(tmp_path, monkeypatch): assert git.find_git_project_root() == Path(os.getcwd()) - with tmpdir.as_cwd() as _: - assert git.find_git_project_root() is None + monkeypatch.chdir(tmp_path) + assert git.find_git_project_root() is None -@pytest.mark.parametrize( - "config_files_manager", defaults.config_files.copy(), indirect=True -) +@pytest.mark.parametrize("config_files_manager", defaults.CONFIG_FILES, indirect=True) def test_set_key(config_files_manager): _conf = config.read_cfg() _conf.set_key("version", "2.0.0") @@ -158,150 +189,311 @@ def test_set_key(config_files_manager): class TestReadCfg: @pytest.mark.parametrize( - "config_files_manager", defaults.config_files.copy(), indirect=True + "config_files_manager", defaults.CONFIG_FILES, indirect=True ) - def test_load_conf(_, config_files_manager): + def test_load_conf(self, 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_conf_returns_default_when_no_files(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + 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) + def test_load_empty_pyproject_toml_and_cz_toml_with_config( + self, tmp_path, monkeypatch + ): + monkeypatch.chdir(tmp_path) + (tmp_path / "pyproject.toml").write_text("") + (tmp_path / ".cz.toml").write_text(TOML_STR) - cfg = config.read_cfg() - assert cfg.settings == _settings + 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) + def test_load_pyproject_toml_from_config_argument(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + _not_root_path = tmp_path / "not_in_root" / "pyproject.toml" + _not_root_path.parent.mkdir(parents=True, exist_ok=True) + _not_root_path.write_text(PYPROJECT) - cfg = config.read_cfg(filepath="./not_in_root/pyproject.toml") - assert cfg.settings == _settings + cfg = config.read_cfg(_not_root_path) + 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) + def test_load_cz_json_not_from_config_argument(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + _not_root_path = tmp_path / "not_in_root" / ".cz.json" + _not_root_path.parent.mkdir(parents=True, exist_ok=True) + _not_root_path.write_text(JSON_STR) + + cfg = config.read_cfg(_not_root_path) + json_cfg_by_class = 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(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + _not_root_path = tmp_path / "not_in_root" / ".cz.yaml" + _not_root_path.parent.mkdir(parents=True, exist_ok=True) + _not_root_path.write_text(YAML_STR) + + cfg = config.read_cfg(_not_root_path) + yaml_cfg_by_class = YAMLConfig(data=YAML_STR, path=_not_root_path) + assert cfg.settings == yaml_cfg_by_class._settings - 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_empty_pyproject_toml_from_config_argument( + self, tmp_path, monkeypatch + ): + monkeypatch.chdir(tmp_path) + _not_root_path = tmp_path / "not_in_root" / "pyproject.toml" + _not_root_path.parent.mkdir(parents=True, exist_ok=True) + _not_root_path.write_text("") + + with pytest.raises(ConfigFileIsEmpty): + config.read_cfg(_not_root_path) - def test_load_cz_yaml_not_from_config_argument(_, tmpdir): + def test_load_empty_json_from_config_argument(self, tmpdir): with tmpdir.as_cwd(): - _not_root_path = tmpdir.mkdir("not_in_root").join(".cz.yaml") - _not_root_path.write(YAML_STR) + _not_root_path = tmpdir.mkdir("not_in_root").join(".cz.json") + _not_root_path.write("") - 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 + with pytest.raises(ConfigFileIsEmpty): + config.read_cfg(filepath="./not_in_root/.cz.json") - def test_load_empty_pyproject_toml_from_config_argument(_, tmpdir): + def test_load_empty_yaml_from_config_argument(self, tmpdir): with tmpdir.as_cwd(): - _not_root_path = tmpdir.mkdir("not_in_root").join("pyproject.toml") + _not_root_path = tmpdir.mkdir("not_in_root").join(".cz.yaml") _not_root_path.write("") with pytest.raises(ConfigFileIsEmpty): - config.read_cfg(filepath="./not_in_root/pyproject.toml") + config.read_cfg(filepath="./not_in_root/.cz.yaml") + + +class TestWarnMultipleConfigFiles: + @pytest.mark.parametrize( + ("files", "expected_path"), + [ + # Same directory, different file types + ([(".cz.toml", TOML_STR), (".cz.json", JSON_STR)], ".cz.toml"), + ([(".cz.json", JSON_STR), (".cz.yaml", YAML_STR)], ".cz.json"), + ([(".cz.toml", TOML_STR), (".cz.yaml", YAML_STR)], ".cz.toml"), + # With pyproject.toml + ( + [("pyproject.toml", PYPROJECT), (".cz.json", JSON_STR)], + ".cz.json", + ), + ( + [("pyproject.toml", PYPROJECT), (".cz.toml", TOML_STR)], + ".cz.toml", + ), + ], + ) + def test_warn_multiple_config_files_same_dir( + self, tmp_path, monkeypatch, capsys, files, expected_path + ): + """Test warning when multiple config files exist in same directory.""" + monkeypatch.chdir(tmp_path) + for filename, content in files: + (tmp_path / filename).write_text(content) + + cfg = config.read_cfg() + captured = capsys.readouterr() + + assert "Multiple config files detected" in captured.err + for filename, _ in files: + assert filename in captured.err + assert f"Using config file: '{expected_path}'" in captured.err + + assert cfg.path == Path(expected_path) + + @pytest.mark.parametrize( + ("config_file", "content"), + [ + (".cz.json", JSON_STR), + (".cz.toml", TOML_STR), + (".cz.yaml", YAML_STR), + ("cz.toml", TOML_STR), + ("cz.json", JSON_STR), + ("cz.yaml", YAML_STR), + ], + ) + def test_warn_same_filename_different_directories_with_git( + self, tmp_path, monkeypatch, capsys, config_file, content + ): + """Test warning when same config filename exists in the current directory and in the git root.""" + monkeypatch.chdir(tmp_path) + cmd.run("git init") + + # Create config in git root + (tmp_path / config_file).write_text(content) + + # Create same filename in subdirectory + subdir = tmp_path / "subdir" + subdir.mkdir() + (subdir / config_file).write_text(content) + + monkeypatch.chdir(subdir) + cfg = config.read_cfg() + captured = capsys.readouterr() + + assert "Multiple config files detected" in captured.err + assert f"Using config file: '{config_file}'" in captured.err + assert cfg.path == Path(config_file) + + def test_no_warn_with_explicit_config_path(self, tmp_path, monkeypatch, capsys): + """Test that no warning is issued when user explicitly specifies config.""" + monkeypatch.chdir(tmp_path) + # Create multiple config files + (tmp_path / ".cz.toml").write_text(PYPROJECT) + (tmp_path / ".cz.json").write_text(JSON_STR) + + # Read config with explicit path + cfg = config.read_cfg(Path(".cz.json")) + + # No warning should be issued + captured = capsys.readouterr() + assert "Multiple config files detected" not in captured.err + + # Verify the explicitly specified config is loaded (compare to expected JSON config) + json_cfg_expected = JsonConfig(data=JSON_STR, path=Path(".cz.json")) + assert cfg.settings == json_cfg_expected.settings + + @pytest.mark.parametrize( + ("config_file", "content", "with_git"), + [ + (file, content, with_git) + for file, content in [ + (".cz.toml", TOML_STR), + (".cz.json", JSON_STR), + (".cz.yaml", YAML_STR), + ("pyproject.toml", PYPROJECT), + ("cz.toml", TOML_STR), + ("cz.json", JSON_STR), + ("cz.yaml", YAML_STR), + ] + for with_git in [True, False] + ], + ) + def test_no_warn_with_single_config_file( + self, tmp_path, monkeypatch, capsys, config_file, content, with_git + ): + """Test that no warning is issued when user explicitly specifies config.""" + monkeypatch.chdir(tmp_path) + if with_git: + cmd.run("git init") + + (tmp_path / config_file).write_text(content) + + cfg = config.read_cfg() + captured = capsys.readouterr() + + # No warning should be issued + assert "Multiple config files detected" not in captured.err + assert cfg.path == Path(config_file) + + def test_no_warn_with_no_commitizen_section_in_pyproject_toml_and_cz_toml( + self, tmp_path, monkeypatch, capsys + ): + monkeypatch.chdir(tmp_path) + (tmp_path / "pyproject.toml").write_text("[tool.foo]\nbar = 'baz'") + (tmp_path / ".cz.toml").write_text(TOML_STR) + + cfg = config.read_cfg() + captured = capsys.readouterr() + assert "Multiple config files detected" not in captured.err + assert cfg.path == Path(".cz.toml") @pytest.mark.parametrize( - "config_file, exception_string", + "config_file", [ - (".cz.toml", r"\.cz\.toml"), - ("cz.toml", r"cz\.toml"), - ("pyproject.toml", r"pyproject\.toml"), + ".cz.toml", + "cz.toml", + "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) + def test_init_empty_config_content(self, tmp_path, config_file): + path = tmp_path / "commitizen" / config_file + path.parent.mkdir(parents=True, exist_ok=True) + toml_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" + assert path.read_text(encoding="utf-8") == "[tool.commitizen]\n" def test_init_empty_config_content_with_existing_content( - self, tmpdir, config_file, exception_string + self, tmp_path, config_file ): - existing_content = "[tool.black]\n" "line-length = 88\n" + 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) + path = tmp_path / "commitizen" / config_file + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(existing_content) + toml_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" + assert ( + path.read_text(encoding="utf-8") + == existing_content + "\n[tool.commitizen]\n" + ) - def test_init_with_invalid_config_content( - self, tmpdir, config_file, exception_string - ): + def test_init_with_invalid_config_content(self, tmp_path, config_file): existing_content = "invalid toml content" - path = tmpdir.mkdir("commitizen").join(config_file) + path = tmp_path / "commitizen" / config_file + path.parent.mkdir(parents=True, exist_ok=True) - with pytest.raises(InvalidConfigurationError, match=exception_string): - config.TomlConfig(data=existing_content, path=path) + with pytest.raises(InvalidConfigurationError) as excinfo: + TomlConfig(data=existing_content, path=path) + assert config_file in str(excinfo.value) @pytest.mark.parametrize( - "config_file, exception_string", + "config_file", [ - (".cz.json", r"\.cz\.json"), - ("cz.json", r"cz\.json"), + ".cz.json", + "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) + def test_init_empty_config_content(self, tmp_path, config_file): + path = tmp_path / "commitizen" / config_file + path.parent.mkdir(parents=True, exist_ok=True) + json_config = JsonConfig(data="{}", path=path) json_config.init_empty_config_content() - with open(path, encoding="utf-8") as json_file: + with path.open(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 - ): + def test_init_with_invalid_config_content(self, tmp_path, config_file): existing_content = "invalid json content" - path = tmpdir.mkdir("commitizen").join(config_file) + path = tmp_path / "commitizen" / config_file + path.parent.mkdir(parents=True, exist_ok=True) - with pytest.raises(InvalidConfigurationError, match=exception_string): - config.JsonConfig(data=existing_content, path=path) + with pytest.raises(InvalidConfigurationError) as excinfo: + JsonConfig(data=existing_content, path=path) + assert config_file in str(excinfo.value) @pytest.mark.parametrize( - "config_file, exception_string", + "config_file", [ - (".cz.yaml", r"\.cz\.yaml"), - ("cz.yaml", r"cz\.yaml"), + ".cz.yaml", + "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) + def test_init_empty_config_content(self, tmp_path, config_file): + path = tmp_path / "commitizen" / config_file + path.parent.mkdir(parents=True, exist_ok=True) + yaml_config = YAMLConfig(data="{}", path=path) yaml_config.init_empty_config_content() - with open(path) as yaml_file: + with path.open() as yaml_file: assert yaml.safe_load(yaml_file) == {"commitizen": {}} - def test_init_with_invalid_content(self, tmpdir, config_file, exception_string): + def test_init_with_invalid_content(self, tmp_path, config_file): existing_content = "invalid: .cz.yaml: content: maybe?" - path = tmpdir.mkdir("commitizen").join(config_file) + path = tmp_path / "commitizen" / config_file + path.parent.mkdir(parents=True, exist_ok=True) - with pytest.raises(InvalidConfigurationError, match=exception_string): - config.YAMLConfig(data=existing_content, path=path) + with pytest.raises(InvalidConfigurationError) as excinfo: + YAMLConfig(data=existing_content, path=path) + assert config_file in str(excinfo.value) diff --git a/tests/test_cz_base.py b/tests/test_cz_base.py deleted file mode 100644 index 4ee1cc6eda..0000000000 --- a/tests/test_cz_base.py +++ /dev/null @@ -1,50 +0,0 @@ -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 index 04d0522174..fc78b3fd49 100644 --- a/tests/test_cz_conventional_commits.py +++ b/tests/test_cz_conventional_commits.py @@ -2,48 +2,42 @@ from commitizen.cz.conventional_commits.conventional_commits import ( ConventionalCommitsCz, - parse_scope, - parse_subject, + _parse_scope, + _parse_subject, ) from commitizen.cz.exceptions import AnswerRequiredError -valid_scopes = ["", "simple", "dash-separated", "camelCase" "UPPERCASE"] -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) +@pytest.mark.parametrize( + "valid_scope", ["", "simple", "dash-separated", "camelCaseUPPERCASE"] +) +def test_parse_scope_valid_values(valid_scope): + 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) +@pytest.mark.parametrize( + "scopes_transformation", [["with spaces", "with-spaces"], ["", ""]] +) +def test_scopes_transformations(scopes_transformation): + 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) +@pytest.mark.parametrize("valid_subject", ["this is a normal text", "aword"]) +def test_parse_subject_valid_values(valid_subject): + 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) +@pytest.mark.parametrize("invalid_subject", ["", " ", ".", " .", "\t\t."]) +def test_parse_subject_invalid_values(invalid_subject): + with pytest.raises(AnswerRequiredError): + _parse_subject(invalid_subject) -def test_subject_transformations(): - for subject_transformation in subjects_transformations: - invalid_subject, transformed_subject = subject_transformation - assert transformed_subject == parse_subject(invalid_subject) +@pytest.mark.parametrize("subject_transformation", [["with dot.", "with dot"]]) +def test_subject_transformations(subject_transformation): + invalid_subject, transformed_subject = subject_transformation + assert transformed_subject == _parse_subject(invalid_subject) def test_questions(config): @@ -89,7 +83,7 @@ def test_long_answer(config): message = conventional_commits.message(answers) assert ( message - == "fix(users): email pattern corrected\n\ncomplete content\n\ncloses #24" # noqa + == "fix(users): email pattern corrected\n\ncomplete content\n\ncloses #24" ) @@ -107,8 +101,57 @@ def test_breaking_change_in_footer(config): print(message) assert ( message - == "fix(users): email pattern corrected\n\ncomplete content\n\nBREAKING CHANGE: migrate by renaming user to users" # noqa + == "fix(users): email pattern corrected\n\ncomplete content\n\nBREAKING CHANGE: migrate by renaming user to users" + ) + + +@pytest.mark.parametrize( + ("scope", "breaking_change_exclamation_in_title", "expected_message"), + [ + # Test with scope and breaking_change_exclamation_in_title enabled + ( + "users", + True, + "feat(users)!: email pattern corrected\n\ncomplete content\n\nBREAKING CHANGE: migrate by renaming user to users", + ), + # Test without scope and breaking_change_exclamation_in_title enabled + ( + "", + True, + "feat!: email pattern corrected\n\ncomplete content\n\nBREAKING CHANGE: migrate by renaming user to users", + ), + # Test with scope and breaking_change_exclamation_in_title disabled + ( + "users", + False, + "feat(users): email pattern corrected\n\ncomplete content\n\nBREAKING CHANGE: migrate by renaming user to users", + ), + # Test without scope and breaking_change_exclamation_in_title disabled + ( + "", + False, + "feat: email pattern corrected\n\ncomplete content\n\nBREAKING CHANGE: migrate by renaming user to users", + ), + ], +) +def test_breaking_change_message_formats( + config, scope, breaking_change_exclamation_in_title, expected_message +): + # Set the breaking_change_exclamation_in_title setting + config.settings["breaking_change_exclamation_in_title"] = ( + breaking_change_exclamation_in_title ) + conventional_commits = ConventionalCommitsCz(config) + answers = { + "prefix": "feat", + "scope": scope, + "subject": "email pattern corrected", + "is_breaking_change": True, + "body": "complete content", + "footer": "migrate by renaming user to users", + } + message = conventional_commits.message(answers) + assert message == expected_message def test_example(config): @@ -130,26 +173,3 @@ def test_info(config): 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 index 20a17b3d9c..311eea19a9 100644 --- a/tests/test_cz_customize.py +++ b/tests/test_cz_customize.py @@ -1,6 +1,11 @@ +from pathlib import Path + import pytest -from commitizen.config import BaseConfig, JsonConfig, TomlConfig, YAMLConfig +from commitizen.config import BaseConfig +from commitizen.config.json_config import JsonConfig +from commitizen.config.toml_config import TomlConfig +from commitizen.config.yaml_config import YAMLConfig from commitizen.cz.customize import CustomizeCommitsCz from commitizen.exceptions import MissingCzCustomizeConfigError @@ -105,17 +110,22 @@ - commitizen/__version__.py - pyproject.toml customize: - message_template: "{{change_type}}:{% if show_message %} {{message}}{% endif %}" + 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)" + 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: ["perf", "BREAKING CHANGE", "feat", "fix", "refactor"] + change_type_order: ['perf', 'BREAKING CHANGE', 'feat', 'fix', 'refactor'] info: This is a customized cz. questions: - type: list @@ -319,8 +329,9 @@ @pytest.fixture( params=[ - TomlConfig(data=TOML_STR, path="not_exist.toml"), - JsonConfig(data=JSON_STR, path="not_exist.json"), + TomlConfig(data=TOML_STR, path=Path("not_exist.toml")), + JsonConfig(data=JSON_STR, path=Path("not_exist.json")), + YAMLConfig(data=YAML_STR, path=Path("not_exist.yaml")), ] ) def config(request): @@ -334,9 +345,9 @@ def config(request): @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"), + TomlConfig(data=TOML_STR_INFO_PATH, path=Path("not_exist.toml")), + JsonConfig(data=JSON_STR_INFO_PATH, path=Path("not_exist.json")), + YAMLConfig(data=YAML_STR_INFO_PATH, path=Path("not_exist.yaml")), ] ) def config_info(request): @@ -345,9 +356,9 @@ def config_info(request): @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"), + TomlConfig(data=TOML_STR_WITHOUT_INFO, path=Path("not_exist.toml")), + JsonConfig(data=JSON_STR_WITHOUT_PATH, path=Path("not_exist.json")), + YAMLConfig(data=YAML_STR_WITHOUT_PATH, path=Path("not_exist.yaml")), ] ) def config_without_info(request): @@ -356,8 +367,8 @@ def config_without_info(request): @pytest.fixture( params=[ - TomlConfig(data=TOML_WITH_UNICODE, path="not_exist.toml"), - JsonConfig(data=JSON_WITH_UNICODE, path="not_exist.json"), + TomlConfig(data=TOML_WITH_UNICODE, path=Path("not_exist.toml")), + JsonConfig(data=JSON_WITH_UNICODE, path=Path("not_exist.json")), ] ) def config_with_unicode(request): @@ -365,9 +376,9 @@ def config_with_unicode(request): def test_initialize_cz_customize_failed(): + config = BaseConfig() with pytest.raises(MissingCzCustomizeConfigError) as excinfo: - config = BaseConfig() - _ = CustomizeCommitsCz(config) + CustomizeCommitsCz(config) assert MissingCzCustomizeConfigError.message in str(excinfo.value) @@ -473,16 +484,16 @@ def test_answer(config): cz = CustomizeCommitsCz(config) answers = { "change_type": "feature", - "message": "this feature enaable customize through config file", + "message": "this feature enable customize through config file", "show_message": True, } message = cz.message(answers) - assert message == "feature: this feature enaable customize through config file" + assert message == "feature: this feature enable customize through config file" cz = CustomizeCommitsCz(config) answers = { "change_type": "feature", - "message": "this feature enaable customize through config file", + "message": "this feature enable customize through config file", "show_message": False, } message = cz.message(answers) @@ -553,18 +564,18 @@ def test_info_unicode(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") +def test_info_with_info_path(tmp_path, monkeypatch, config_info): + monkeypatch.chdir(tmp_path) + tmpfile = tmp_path / "info.txt" + tmpfile.write_text("Test info") - cz = CustomizeCommitsCz(config_info) - assert "Test info" in cz.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() is None + assert cz.info() == "" def test_commit_parser(config): diff --git a/tests/test_cz_search_filter.py b/tests/test_cz_search_filter.py new file mode 100644 index 0000000000..cbceb8b887 --- /dev/null +++ b/tests/test_cz_search_filter.py @@ -0,0 +1,76 @@ +import pytest + +from commitizen.config.toml_config import TomlConfig +from commitizen.cz.customize import CustomizeCommitsCz + +TOML_WITH_SEARCH_FILTER = r""" +[tool.commitizen] +name = "cz_customize" + +[tool.commitizen.customize] +message_template = "{{change_type}}:{% if scope %} ({{scope}}){% endif %}{% if breaking %}!{% endif %} {{message}}" + +[[tool.commitizen.customize.questions]] +type = "select" +name = "change_type" +message = "Select the type of change you are committing" +use_search_filter = true +use_jk_keys = false +choices = [ + {value = "fix", name = "fix: A bug fix. Correlates with PATCH in SemVer"}, + {value = "feat", name = "feat: A new feature. Correlates with MINOR in SemVer"}, + {value = "docs", name = "docs: Documentation only changes"}, + {value = "style", name = "style: Changes that do not affect the meaning of the code"}, + {value = "refactor", name = "refactor: A code change that neither fixes a bug nor adds a feature"}, + {value = "perf", name = "perf: A code change that improves performance"}, + {value = "test", name = "test: Adding missing or correcting existing tests"}, + {value = "build", name = "build: Changes that affect the build system or external dependencies"}, + {value = "ci", name = "ci: Changes to CI configuration files and scripts"} +] + +[[tool.commitizen.customize.questions]] +type = "input" +name = "scope" +message = "What is the scope of this change? (class or file name): (press [enter] to skip)" + +[[tool.commitizen.customize.questions]] +type = "input" +name = "message" +message = "Write a short and imperative summary of the code changes: (lower case and no period)" +""" + + +@pytest.fixture +def config(): + return TomlConfig(data=TOML_WITH_SEARCH_FILTER, path="not_exist.toml") + + +def test_questions_with_search_filter(config): + """Test that questions are properly configured with search filter""" + cz = CustomizeCommitsCz(config) + questions = cz.questions() + + # Test that the first question (change_type) has search filter enabled + assert questions[0]["type"] == "select" + assert questions[0]["name"] == "change_type" + assert questions[0]["use_search_filter"] is True + assert questions[0]["use_jk_keys"] is False + + # Test that the choices are properly configured + choices = questions[0]["choices"] + assert len(choices) == 9 # We have 9 commit types + assert choices[0]["value"] == "fix" + assert choices[1]["value"] == "feat" + + +def test_message_template(config): + """Test that the message template is properly configured""" + cz = CustomizeCommitsCz(config) + template = cz.message( + { + "change_type": "feat", + "scope": "search", + "message": "add search filter support", + } + ) + assert template == "feat: (search) add search filter support" diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py new file mode 100644 index 0000000000..ebaae82c37 --- /dev/null +++ b/tests/test_deprecated.py @@ -0,0 +1,35 @@ +import pytest + +from commitizen import changelog_formats, defaults + + +@pytest.mark.parametrize( + ("deprecated_var_getter", "replacement"), + [ + (lambda: defaults.bump_pattern, defaults.BUMP_PATTERN), + (lambda: defaults.bump_map, defaults.BUMP_MAP), + ( + lambda: defaults.bump_map_major_version_zero, + defaults.BUMP_MAP_MAJOR_VERSION_ZERO, + ), + (lambda: defaults.bump_message, defaults.BUMP_MESSAGE), + (lambda: defaults.change_type_order, defaults.CHANGE_TYPE_ORDER), + (lambda: defaults.encoding, defaults.ENCODING), + (lambda: defaults.name, defaults.DEFAULT_SETTINGS["name"]), + ( + lambda: changelog_formats.guess_changelog_format, + changelog_formats._guess_changelog_format, + ), + ], +) +def test_getattr_deprecated_vars(deprecated_var_getter, replacement): + # Test each deprecated variable + with pytest.warns(DeprecationWarning, match="is deprecated and will be removed"): + val = deprecated_var_getter() + assert val == replacement + + +def test_getattr_non_existent(): + # Test non-existent attribute + with pytest.raises(AttributeError, match="is not an attribute of"): + _ = defaults.non_existent_attribute diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 0000000000..f9ff733c95 --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,38 @@ +import pytest + +from commitizen.exceptions import ExitCode + + +def test_from_str_with_decimal(): + """Test from_str with decimal values.""" + assert ExitCode.from_str("0") == ExitCode.EXPECTED_EXIT + assert ExitCode.from_str("1") == ExitCode.NO_COMMITIZEN_FOUND + assert ExitCode.from_str("32") == ExitCode.COMMIT_MESSAGE_LENGTH_LIMIT_EXCEEDED + + +def test_from_str_with_enum_name(): + """Test from_str with enum names.""" + assert ExitCode.from_str("EXPECTED_EXIT") == ExitCode.EXPECTED_EXIT + assert ExitCode.from_str("NO_COMMITIZEN_FOUND") == ExitCode.NO_COMMITIZEN_FOUND + assert ( + ExitCode.from_str("COMMIT_MESSAGE_LENGTH_LIMIT_EXCEEDED") + == ExitCode.COMMIT_MESSAGE_LENGTH_LIMIT_EXCEEDED + ) + + +def test_from_str_with_whitespace(): + """Test from_str with whitespace in enum names.""" + assert ExitCode.from_str(" EXPECTED_EXIT ") == ExitCode.EXPECTED_EXIT + assert ExitCode.from_str("\tNO_COMMITIZEN_FOUND\t") == ExitCode.NO_COMMITIZEN_FOUND + + +def test_from_str_with_invalid_values(): + """Test from_str with invalid values.""" + with pytest.raises(KeyError): + ExitCode.from_str("invalid_name") + with pytest.raises(ValueError, match="is not a valid ExitCode"): + ExitCode.from_str("999") # Out of range decimal + with pytest.raises(KeyError): + ExitCode.from_str("") + with pytest.raises(KeyError): + ExitCode.from_str(" ") diff --git a/tests/test_factory.py b/tests/test_factory.py index 390742f467..303ae4e728 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -1,11 +1,7 @@ import sys +from importlib import metadata 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 @@ -28,7 +24,7 @@ class OtherPlugin: def test_factory(): config = BaseConfig() config.settings.update({"name": defaults.DEFAULT_SETTINGS["name"]}) - r = factory.commiter_factory(config) + r = factory.committer_factory(config) assert isinstance(r, BaseCommitizen) @@ -36,7 +32,7 @@ def test_factory_fails(): config = BaseConfig() config.settings.update({"name": "Nothing"}) with pytest.raises(NoCommitizenFoundException) as excinfo: - factory.commiter_factory(config) + factory.committer_factory(config) assert "The committer has not been found in the system." in str(excinfo) @@ -56,14 +52,13 @@ class Plugin: pass ) sys.path.append(tmp_path.as_posix()) - with pytest.warns(UserWarning) as record: + with pytest.warns( + UserWarning, + match="Legacy plugin 'cz_legacy' has been ignored: please expose it the 'commitizen.plugin' entrypoint", + ): 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 diff --git a/tests/test_git.py b/tests/test_git.py index 6ada76be6d..59098528a4 100644 --- a/tests/test_git.py +++ b/tests/test_git.py @@ -3,19 +3,25 @@ import inspect import os import platform -import shutil +from typing import TYPE_CHECKING 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, -) + +from commitizen import cmd, git +from commitizen.exceptions import GitCommandError + +if TYPE_CHECKING: + from pytest_gitconfig import GitConfig + from pytest_mock import MockFixture + + from tests.utils import UtilFixture + + +@pytest.mark.parametrize("date", ["2020-01-21", "1970-01-01"]) +def test_git_tag_date(date: str): + git_tag = git.GitTag(rev="sha1-code", name="0.0.1", date="2025-05-30") + git_tag.date = date + assert git_tag.date == date def test_git_object_eq(): @@ -28,13 +34,13 @@ def test_git_object_eq(): assert git_commit != "sha1-code" -def test_get_tags(mocker: MockFixture): +def test_get_tags(util: UtilFixture): 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)) + util.mock_cmd(out=tag_str) git_tags = git.get_tags() latest_git_tag = git_tags[0] @@ -42,56 +48,49 @@ def test_get_tags(mocker: MockFixture): 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") - ) + util.mock_cmd(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") +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_get_reachable_tags(util: UtilFixture): + util.create_file_and_commit("Initial state") + util.create_tag("1.0.0") + # create develop + util.create_branch("develop") + util.switch_branch("develop") - # add a feature to develop - create_file_and_commit("develop") - create_tag("1.1.0b0") + # add a feature to develop + util.create_file_and_commit("develop") + util.create_tag("1.1.0b0") - # create staging - switch_branch("master") - create_file_and_commit("master") - create_tag("1.0.1") + # create staging + util.switch_branch("master") + util.create_file_and_commit("master") + util.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"} + 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.usefixtures("tmp_commitizen_project") @pytest.mark.parametrize("locale", ["en_US", "fr_FR"]) -def test_get_reachable_tags_with_commits( - tmp_commitizen_project, locale: str, monkeypatch: pytest.MonkeyPatch -): +def test_get_reachable_tags_with_commits(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 == [] + assert git.get_tags(reachable_only=True) == [] -def test_get_tag_names(mocker: MockFixture): - tag_str = "v1.0.0\n" "v0.5.0\n" "v0.0.1\n" - mocker.patch("commitizen.cmd.run", return_value=FakeCommand(out=tag_str)) +def test_get_tag_names(util: UtilFixture): + tag_str = "v1.0.0\nv0.5.0\nv0.0.1\n" + util.mock_cmd(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") - ) + util.mock_cmd(out="", err="No tag available") assert git.get_tag_names() == [] @@ -104,45 +103,50 @@ def test_git_message_with_empty_body(): @pytest.mark.usefixtures("tmp_commitizen_project") def test_get_log_as_str_list_empty(): - """ensure an exception or empty list in an empty project""" + """ + Ensure an exception is raised or empty list in an empty project. + The behavior is different depending on the version of git. + """ try: gitlog = git._get_log_as_str_list(start=None, end="HEAD", args="") - except exceptions.GitCommandError: + except 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") +def test_get_commits(util: UtilFixture): + util.create_file_and_commit("feat(users): add username") + util.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") +def test_get_commits_author_and_email(util: UtilFixture): + util.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): +def test_get_commits_without_email(util: UtilFixture): 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)) + util.mock_cmd(out=raw_commit) commits = git.get_commits() @@ -156,25 +160,28 @@ def test_get_commits_without_email(mocker: MockFixture): assert commits[1].title == "feat(users): add username" -def test_get_commits_without_breakline_in_each_commit(mocker: MockFixture): +def test_get_commits_without_breakline_in_each_commit(util: UtilFixture): 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)) + util.mock_cmd(out=raw_commit) commits = git.get_commits() @@ -193,110 +200,159 @@ def test_get_commits_without_breakline_in_each_commit(mocker: MockFixture): ) -def test_get_commits_with_signature(): - config_file = ".git/config" - config_backup = ".git/config.bak" - shutil.copy(config_file, config_backup) +def test_get_commits_with_and_without_parents(util: UtilFixture): + 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" + ) + util.mock_cmd(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 == [] - 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] +@pytest.mark.usefixtures("in_repo_root") +def test_get_commits_with_signature(gitconfig: GitConfig): + # temporarily turn on --show-signature + gitconfig.set("log.showsignature", "true") - assert commit.title.startswith("fix") - finally: - # restore the repo's original config - shutil.move(config_backup, config_file) + # retrieve a commit that we know has a signature + commit = git.get_commits( + start="bec20ebf433f2281c70f1eb4b0b6a1d0ed83e9b2", + end="9eae518235d051f145807ddf971ceb79ad49953a", + )[0] + + assert commit.title.startswith("fix") 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]" + assert arrow_annotation == "list[str]" + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_get_latest_tag_name(util: UtilFixture): + tag_name = git.get_latest_tag_name() + assert tag_name is None + + util.create_file_and_commit("feat(test): test") + util.create_tag("1.0") + tag_name = git.get_latest_tag_name() + assert tag_name == "1.0" -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 +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_is_staging_clean_when_adding_file(): + assert git.is_staging_clean() is True - 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" + cmd.run("touch test_file") + assert git.is_staging_clean() is True -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("git add test_file") - cmd.run("touch test_file") + assert git.is_staging_clean() is False - assert git.is_staging_clean() is True - cmd.run("git add test_file") +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_is_staging_clean_when_updating_file(): + assert git.is_staging_clean() is True - assert git.is_staging_clean() is False + 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 -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("git add test_file") - 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 False - assert git.is_staging_clean() is True - cmd.run("git add test_file") +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_get_eol_for_open(): + assert git.EOLType.for_open() == os.linesep - assert git.is_staging_clean() is False + cmd.run("git config core.eol lf") + assert git.EOLType.for_open() == "\n" + cmd.run("git config core.eol crlf") + assert git.EOLType.for_open() == "\r\n" -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 native") + assert git.EOLType.for_open() == os.linesep - 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 +def test_get_core_editor(monkeypatch: pytest.MonkeyPatch, util: UtilFixture): + monkeypatch.setenv("GIT_EDITOR", "nano") + assert git.get_core_editor() == "nano" - cmd.run("git config core.eol native") - assert git.get_eol_style() == git.EOLTypes.NATIVE + monkeypatch.delenv("GIT_EDITOR") + util.mock_cmd(out="vim") + assert git.get_core_editor() == "vim" -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" + util.mock_cmd() + 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.usefixtures("tmp_commitizen_project") +def test_create_tag_with_message(util: UtilFixture): + util.create_file_and_commit("feat(test): test") + tag_name = "1.0" + tag_message = "test message" + util.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", + ("file_path", "expected_cmd"), [ ( "/tmp/temp file", @@ -317,8 +373,10 @@ def test_create_tag_with_message(tmp_commitizen_project): "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()) +def test_commit_with_spaces_in_path( + mocker: MockFixture, file_path: str, expected_cmd: str, util: UtilFixture +): + mock_run = util.mock_cmd() mock_unlink = mocker.patch("os.unlink") mock_temp_file = mocker.patch("commitizen.git.NamedTemporaryFile") mock_temp_file.return_value.name = file_path @@ -327,3 +385,128 @@ def test_commit_with_spaces_in_path(mocker, file_path, expected_cmd): mock_run.assert_called_once_with(expected_cmd) mock_unlink.assert_called_once_with(file_path) + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_get_filenames_in_commit(util: UtilFixture): + """Test get_filenames_in_commit returns filenames from the last commit.""" + util.create_file_and_commit("feat: old feature", filename="old_file.txt") + + filename = "test_feature_file.txt" + util.create_file_and_commit("feat: add new feature", filename=filename) + + filenames = git.get_filenames_in_commit() + assert [filename] == filenames + + +@pytest.mark.usefixtures("tmp_commitizen_project") +def test_get_filenames_in_commit_with_git_reference(util: UtilFixture): + """Test get_filenames_in_commit with a specific git reference (commit SHA).""" + first_filename = "first_feature.txt" + util.create_file_and_commit("feat: first feature", filename=first_filename) + first_commit_rev = cmd.run("git rev-parse HEAD").out.strip() + + second_filename = "second_feature.txt" + util.create_file_and_commit("feat: second feature", filename=second_filename) + + # Query the first commit by its SHA + filenames = git.get_filenames_in_commit(git_reference=first_commit_rev) + assert first_filename in filenames + assert second_filename not in filenames + + +def test_get_filenames_in_commit_error(util: UtilFixture): + """Test that GitCommandError is raised when git command fails.""" + util.mock_cmd(err="fatal: bad object HEAD", return_code=1) + with pytest.raises(GitCommandError) as excinfo: + git.get_filenames_in_commit() + assert str(excinfo.value) == "fatal: bad object HEAD" + + +@pytest.mark.parametrize( + "linebreak", ["\n", "\r\n"], ids=["line_feed", "carriage_return"] +) +def test_git_commit_from_rev_and_commit(linebreak): + rev_and_commit = linebreak.join( + [ + "abc123", # rev + "def456 ghi789", # parents + "feat: add new feature", # title + "John Doe", # author + "john@example.com", # author_email + "This is a detailed description", # body + "of the new feature", + "with multiple lines", + ] + ) + + commit = git.GitCommit.from_rev_and_commit(rev_and_commit) + + assert commit.rev == "abc123" + assert commit.title == "feat: add new feature" + assert ( + commit.body + == "This is a detailed description\nof the new feature\nwith multiple lines" + ) + assert commit.author == "John Doe" + assert commit.author_email == "john@example.com" + assert commit.parents == ["def456", "ghi789"] + + # Test with minimal data + minimal_commit = linebreak.join( + [ + "abc123", # rev + "", # no parents + "feat: minimal commit", # title + "John Doe", # author + "john@example.com", # author_email + ] + ) + + commit = git.GitCommit.from_rev_and_commit(minimal_commit) + + assert commit.rev == "abc123" + assert commit.title == "feat: minimal commit" + assert commit.body == "" + assert commit.author == "John Doe" + assert commit.author_email == "john@example.com" + assert commit.parents == [] + + +@pytest.mark.parametrize( + ("os_name", "committer_date", "expected_cmd"), + [ + ( + "nt", + "2024-03-20", + 'cmd /v /c "set GIT_COMMITTER_DATE=2024-03-20&& git commit -F "temp.txt""', + ), + ( + "posix", + "2024-03-20", + 'GIT_COMMITTER_DATE=2024-03-20 git commit -F "temp.txt"', + ), + ("nt", None, 'git commit -F "temp.txt"'), + ("posix", None, 'git commit -F "temp.txt"'), + ], +) +def test_create_commit_cmd_string( + mocker: MockFixture, os_name: str, committer_date: str, expected_cmd: str +): + """Test the OS-specific behavior of _create_commit_cmd_string""" + mocker.patch("os.name", os_name) + result = git._create_commit_cmd_string("", committer_date, "temp.txt") + assert result == expected_cmd + + +def test_get_default_branch_success(util: UtilFixture): + util.mock_cmd(out="refs/remotes/origin/main\n") + assert git.get_default_branch() == "refs/remotes/origin/main" + + +def test_get_default_branch_error(util: UtilFixture): + util.mock_cmd( + err="fatal: ref refs/remotes/origin/HEAD is not a symbolic ref", return_code=1 + ) + with pytest.raises(GitCommandError): + git.get_default_branch() diff --git a/tests/test_incremental_build.py b/tests/test_incremental_build.py new file mode 100644 index 0000000000..13f123a651 --- /dev/null +++ b/tests/test_incremental_build.py @@ -0,0 +1,606 @@ +"""Tests for the incremental_build function in commitizen.changelog module.""" + +from commitizen.changelog import Metadata, incremental_build + + +class TestIncrementalBuild: + """Test cases for the incremental_build function.""" + + def test_basic_replacement_of_unreleased_section(self): + """Test basic functionality of replacing unreleased section with new content.""" + lines = [ + "# Changelog", + "", + "## Unreleased", + "", + "### Added", + "- New feature", + "", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + ] + + metadata = Metadata( + unreleased_start=2, + unreleased_end=6, + latest_version="1.0.0", + latest_version_position=7, + ) + + new_content = "## Unreleased\n\n### Added\n- Another new feature" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "# Changelog", + "", + "## Unreleased\n\n### Added\n- Another new feature", + "\n", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + ] + + assert result == expected + + def test_replacement_when_latest_version_position_is_none(self): + """Test replacement when latest_version_position is None (append to end).""" + lines = [ + "# Changelog", + "", + "## Unreleased", + "", + "### Added", + "- New feature", + ] + + metadata = Metadata( + unreleased_start=2, + unreleased_end=5, + latest_version=None, + latest_version_position=None, + ) + + new_content = "## Unreleased\n\n### Added\n- Another new feature" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "# Changelog", + "", + "## Unreleased\n\n### Added\n- Another new feature", + ] + + assert result == expected + + def test_replacement_when_latest_version_position_after_unreleased_end(self): + """Test replacement when latest_version_position is after unreleased_end.""" + lines = [ + "# Changelog", + "", + "## Unreleased", + "", + "### Added", + "- New feature", + "", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + ] + + metadata = Metadata( + unreleased_start=2, + unreleased_end=5, + latest_version="1.0.0", + latest_version_position=7, + ) + + new_content = "## Unreleased\n\n### Added\n- Another new feature" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "# Changelog", + "", + "", + "## Unreleased\n\n### Added\n- Another new feature", + "\n", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + ] + + assert result == expected + + def test_replacement_when_latest_version_position_before_unreleased_end(self): + """Test replacement when latest_version_position is before unreleased_end.""" + lines = [ + "# Changelog", + "", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + "", + "## Unreleased", + "", + "### Added", + "- New feature", + ] + + metadata = Metadata( + unreleased_start=7, + unreleased_end=10, + latest_version="1.0.0", + latest_version_position=2, + ) + + new_content = "## Unreleased\n\n### Added\n- Another new feature" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "# Changelog", + "", + "## Unreleased\n\n### Added\n- Another new feature", + "\n", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + "", + "- New feature", + ] + + assert result == expected + + def test_no_unreleased_section_append_to_end(self): + """Test appending new content when no unreleased section exists.""" + lines = [ + "# Changelog", + "", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + ] + + metadata = Metadata( + unreleased_start=None, + unreleased_end=None, + latest_version=None, + latest_version_position=None, + ) + + new_content = "## Unreleased\n\n### Added\n- New feature" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "# Changelog", + "", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + "\n", + "## Unreleased\n\n### Added\n- New feature", + ] + + assert result == expected + + def test_no_unreleased_section_with_latest_version_position(self): + """Test inserting new content at latest_version_position when no unreleased section.""" + lines = [ + "# Changelog", + "", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + ] + + metadata = Metadata( + unreleased_start=None, + unreleased_end=None, + latest_version="1.0.0", + latest_version_position=2, + ) + + new_content = "## Unreleased\n\n### Added\n- New feature" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "# Changelog", + "", + "## Unreleased\n\n### Added\n- New feature", + "\n", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + ] + + assert result == expected + + def test_empty_lines_list(self): + """Test behavior with empty lines list.""" + lines = [] + + metadata = Metadata( + unreleased_start=None, + unreleased_end=None, + latest_version=None, + latest_version_position=None, + ) + + new_content = "## Unreleased\n\n### Added\n- New feature" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "## Unreleased\n\n### Added\n- New feature", + ] + + assert result == expected + + def test_single_line_unreleased_section(self): + """Test replacement of single line unreleased section.""" + lines = [ + "# Changelog", + "## Unreleased", + "## 1.0.0 (2023-01-01)", + ] + + metadata = Metadata( + unreleased_start=1, + unreleased_end=1, + latest_version="1.0.0", + latest_version_position=2, + ) + + new_content = "## Unreleased\n\n### Added\n- New feature" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "# Changelog", + ] + + assert result == expected + + def test_unreleased_section_at_end_of_file(self): + """Test replacement when unreleased section is at the end of file.""" + lines = [ + "# Changelog", + "", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + "", + "## Unreleased", + "", + "### Added", + "- New feature", + ] + + metadata = Metadata( + unreleased_start=7, + unreleased_end=10, + latest_version="1.0.0", + latest_version_position=2, + ) + + new_content = "## Unreleased\n\n### Added\n- Another new feature" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "# Changelog", + "", + "## Unreleased\n\n### Added\n- Another new feature", + "\n", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + "", + "- New feature", + ] + + assert result == expected + + def test_blank_line_handling_when_appending(self): + """Test that blank line is added when appending to non-empty content.""" + lines = [ + "# Changelog", + "", + "## 1.0.0 (2023-01-01)", + ] + + metadata = Metadata( + unreleased_start=None, + unreleased_end=None, + latest_version=None, + latest_version_position=None, + ) + + new_content = "## Unreleased\n\n### Added\n- New feature" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "# Changelog", + "", + "## 1.0.0 (2023-01-01)", + "\n", + "## Unreleased\n\n### Added\n- New feature", + ] + + assert result == expected + + def test_no_blank_line_when_content_ends_with_blank_line(self): + """Test that no extra blank line is added when content already ends with blank line.""" + lines = [ + "# Changelog", + "", + "## 1.0.0 (2023-01-01)", + "", + ] + + metadata = Metadata( + unreleased_start=None, + unreleased_end=None, + latest_version=None, + latest_version_position=None, + ) + + new_content = "## Unreleased\n\n### Added\n- New feature" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "# Changelog", + "", + "## 1.0.0 (2023-01-01)", + "", + "## Unreleased\n\n### Added\n- New feature", + ] + + assert result == expected + + def test_empty_new_content(self): + """Test behavior with empty new content.""" + lines = [ + "# Changelog", + "", + "## Unreleased", + "", + "### Added", + "- New feature", + "", + "## 1.0.0 (2023-01-01)", + ] + + metadata = Metadata( + unreleased_start=2, + unreleased_end=5, + latest_version="1.0.0", + latest_version_position=7, + ) + + new_content = "" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "# Changelog", + "", + "", + "", + "\n", + "## 1.0.0 (2023-01-01)", + ] + + assert result == expected + + def test_multiline_new_content(self): + """Test behavior with multiline new content.""" + lines = [ + "# Changelog", + "", + "## Unreleased", + "", + "### Added", + "- New feature", + "", + "## 1.0.0 (2023-01-01)", + ] + + metadata = Metadata( + unreleased_start=2, + unreleased_end=5, + latest_version="1.0.0", + latest_version_position=7, + ) + + new_content = "## Unreleased\n\n### Added\n- Feature 1\n- Feature 2\n\n### Fixed\n- Bug fix" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "# Changelog", + "", + "", + "## Unreleased\n\n### Added\n- Feature 1\n- Feature 2\n\n### Fixed\n- Bug fix", + "\n", + "## 1.0.0 (2023-01-01)", + ] + + assert result == expected + + def test_metadata_with_none_values(self): + """Test behavior when metadata has None values for unreleased positions.""" + lines = [ + "# Changelog", + "", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + ] + + metadata = Metadata( + unreleased_start=None, + unreleased_end=None, + latest_version="1.0.0", + latest_version_position=2, + ) + + new_content = "## Unreleased\n\n### Added\n- New feature" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "# Changelog", + "", + "## Unreleased\n\n### Added\n- New feature", + "\n", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + ] + + assert result == expected + + def test_skip_behavior_during_unreleased_section(self): + """Test that lines are properly skipped during unreleased section processing.""" + lines = [ + "# Changelog", + "", + "## Unreleased", + "", + "### Added", + "- Old feature 1", + "- Old feature 2", + "", + "### Fixed", + "- Old bug fix", + "", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + ] + + metadata = Metadata( + unreleased_start=2, + unreleased_end=9, + latest_version="1.0.0", + latest_version_position=11, + ) + + new_content = "## Unreleased\n\n### Added\n- New feature" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "# Changelog", + "", + "", + "## Unreleased\n\n### Added\n- New feature", + "\n", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + ] + + assert result == expected + + def test_latest_version_position_at_unreleased_end(self): + """Test behavior when latest_version_position equals unreleased_end.""" + lines = [ + "# Changelog", + "", + "## Unreleased", + "", + "### Added", + "- New feature", + "", + "## 1.0.0 (2023-01-01)", + ] + + metadata = Metadata( + unreleased_start=2, + unreleased_end=5, + latest_version="1.0.0", + latest_version_position=5, + ) + + new_content = "## Unreleased\n\n### Added\n- Another new feature" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "# Changelog", + "", + "## Unreleased\n\n### Added\n- Another new feature", + "\n", + "- New feature", + "", + "## 1.0.0 (2023-01-01)", + ] + + assert result == expected + + def test_latest_version_position_before_unreleased_start(self): + """Test behavior when latest_version_position is before unreleased_start.""" + lines = [ + "# Changelog", + "", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + "", + "## Unreleased", + "", + "### Added", + "- New feature", + ] + + metadata = Metadata( + unreleased_start=7, + unreleased_end=9, + latest_version="1.0.0", + latest_version_position=2, + ) + + new_content = "## Unreleased\n\n### Added\n- Another new feature" + + result = incremental_build(new_content, lines, metadata) + + expected = [ + "# Changelog", + "", + "## Unreleased\n\n### Added\n- Another new feature", + "\n", + "## 1.0.0 (2023-01-01)", + "", + "### Fixed", + "- Bug fix", + "", + "### Added", + "- New feature", + ] + + assert result == expected diff --git a/tests/test_project_info.py b/tests/test_project_info.py new file mode 100644 index 0000000000..89359fe73e --- /dev/null +++ b/tests/test_project_info.py @@ -0,0 +1,91 @@ +"""Tests for project_info module.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from commitizen import project_info + + +def _create_project_files(files: dict[str, str | None]) -> None: + for file_path, content in files.items(): + path = Path(file_path) + if content is None: + path.touch() + else: + path.write_text(content) + + +@pytest.mark.parametrize( + ("which_return", "expected"), + [ + ("/usr/local/bin/pre-commit", True), + ("/usr/local/bin/prek", True), + (None, False), + ("", False), + ], +) +def test_is_pre_commit_installed(mocker, which_return, expected): + mocker.patch("shutil.which", return_value=which_return) + assert project_info.is_pre_commit_installed() is expected + + +@pytest.mark.parametrize( + ("files", "expected"), + [ + ( + {"pyproject.toml": '[tool.poetry]\nname = "test"\nversion = "0.1.0"'}, + "poetry", + ), + ({"pyproject.toml": "", "uv.lock": ""}, "uv"), + ( + {"pyproject.toml": '[tool.commitizen]\nversion = "0.1.0"'}, + "pep621", + ), + ({"setup.py": ""}, "pep621"), + ({"Cargo.toml": ""}, "cargo"), + ({"package.json": ""}, "npm"), + ({"composer.json": ""}, "composer"), + ({}, "commitizen"), + ( + { + "pyproject.toml": "", + "Cargo.toml": "", + "package.json": "", + "composer.json": "", + }, + "pep621", + ), + ], +) +def test_get_default_version_provider(chdir, files, expected): + _create_project_files(files) + assert project_info.get_default_version_provider() == expected + + +@pytest.mark.parametrize( + ("files", "expected"), + [ + ({"pyproject.toml": ""}, "pyproject.toml"), + ({}, ".cz.toml"), + ], +) +def test_get_default_config_filename(chdir, files, expected): + _create_project_files(files) + assert project_info.get_default_config_filename() == expected + + +@pytest.mark.parametrize( + ("files", "expected"), + [ + ({"pyproject.toml": ""}, "pep440"), + ({"setup.py": ""}, "pep440"), + ({"package.json": ""}, "semver"), + ({}, "semver"), + ], +) +def test_get_default_version_scheme(chdir, files, expected): + _create_project_files(files) + assert project_info.get_default_version_scheme() == expected diff --git a/tests/test_tags.py b/tests/test_tags.py new file mode 100644 index 0000000000..2471b8461b --- /dev/null +++ b/tests/test_tags.py @@ -0,0 +1,101 @@ +from commitizen.git import GitTag +from commitizen.tags import TagRules + + +def _git_tag(name: str) -> GitTag: + return GitTag(name, "rev", "2024-01-01") + + +def test_find_tag_for_partial_version_returns_latest_match(): + tags = [ + _git_tag("1.2.0"), + _git_tag("1.2.2"), + _git_tag("1.2.1"), + _git_tag("1.3.0"), + ] + + rules = TagRules() + + found = rules.find_tag_for(tags, "1.2") + + assert found is not None + assert found.name == "1.2.2" + + +def test_find_tag_for_full_version_remains_exact(): + tags = [ + _git_tag("1.2.0"), + _git_tag("1.2.2"), + _git_tag("1.2.1"), + ] + + rules = TagRules() + + found = rules.find_tag_for(tags, "1.2.1") + + assert found is not None + assert found.name == "1.2.1" + + +def test_find_tag_for_partial_version_with_prereleases_prefers_latest_version(): + tags = [ + _git_tag("1.2.0b1"), + _git_tag("1.2.0"), + _git_tag("1.2.1b1"), + ] + + rules = TagRules() + + found = rules.find_tag_for(tags, "1.2") + + assert found is not None + # 1.2.1b1 > 1.2.0 so it should be selected + assert found.name == "1.2.1b1" + + +def test_find_tag_for_partial_version_respects_tag_format(): + tags = [ + _git_tag("v1.2.0"), + _git_tag("v1.2.1"), + _git_tag("v1.3.0"), + ] + + rules = TagRules(tag_format="v$version") + + found = rules.find_tag_for(tags, "1.2") + + assert found is not None + assert found.name == "v1.2.1" + + found = rules.find_tag_for(tags, "1") + + assert found is not None + assert found.name == "v1.3.0" + + +def test_find_tag_for_partial_version_returns_none_when_no_match(): + tags = [ + _git_tag("2.0.0"), + _git_tag("2.1.0"), + ] + + rules = TagRules() + + found = rules.find_tag_for(tags, "1.2") + + assert found is None + + +def test_find_tag_for_partial_version_ignores_invalid_tags(): + tags = [ + _git_tag("not-a-version"), + _git_tag("1.2.0"), + _git_tag("1.2.1"), + ] + + rules = TagRules() + + found = rules.find_tag_for(tags, "1.2") + + assert found is not None + assert found.name == "1.2.1" diff --git a/tests/test_version_scheme_pep440.py b/tests/test_version_scheme_pep440.py index 6b1f621cb8..479c2f775d 100644 --- a/tests/test_version_scheme_pep440.py +++ b/tests/test_version_scheme_pep440.py @@ -1,258 +1,1315 @@ -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"), -] +from tests.utils import VersionSchemeTestArgs @pytest.mark.parametrize( - "test_input,expected", - itertools.chain( - tdd_cases, - weird_cases, - simple_flow, - linear_prerelease_cases, - prerelease_cases, - ), + ("version_args", "expected_version"), + [ + ( + VersionSchemeTestArgs( + current_version="0.1.1", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.1.2", + ), + ( + VersionSchemeTestArgs( + current_version="0.1.1", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.2.0", + ), + ( + VersionSchemeTestArgs( + current_version="2.1.1", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "3.0.0", + ), + ( + VersionSchemeTestArgs( + current_version="0.9.0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.9.1a0", + ), + ( + VersionSchemeTestArgs( + current_version="0.9.0", + increment="MINOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.10.0a0", + ), + ( + VersionSchemeTestArgs( + current_version="0.9.0", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0a0", + ), + ( + VersionSchemeTestArgs( + current_version="0.9.0", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=1, + devrelease=None, + ), + "1.0.0a1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a2", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0b0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a2", + increment=None, + prerelease="beta", + prerelease_offset=1, + devrelease=None, + ), + "1.0.0b1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0beta1", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0rc0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0rc1", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0rc2", + ), + # weird cases + ( + VersionSchemeTestArgs( + current_version="1.1", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.1.1", + ), + ( + VersionSchemeTestArgs( + current_version="1", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.1.0", + ), + ( + VersionSchemeTestArgs( + current_version="1", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "2.0.0", + ), + ( + VersionSchemeTestArgs( + current_version="1a0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0a1", + ), + ( + VersionSchemeTestArgs( + current_version="1a0", + increment=None, + prerelease="alpha", + prerelease_offset=1, + devrelease=None, + ), + "1.0.0a1", + ), + ( + VersionSchemeTestArgs( + current_version="1", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0b0", + ), + ( + VersionSchemeTestArgs( + current_version="1", + increment=None, + prerelease="beta", + prerelease_offset=1, + devrelease=None, + ), + "1.0.0b1", + ), + ( + VersionSchemeTestArgs( + current_version="1beta", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0b1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0alpha1", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0a2", + ), + ( + VersionSchemeTestArgs( + current_version="1", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0rc0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0rc1+e20d7b57f3eb", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.0.0", + ), + # simple flow + ( + VersionSchemeTestArgs( + current_version="0.1.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.1.1", + ), + ( + VersionSchemeTestArgs( + current_version="0.1.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=1, + ), + "0.1.1.dev1", + ), + ( + VersionSchemeTestArgs( + current_version="0.2.0", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.3.0", + ), + ( + VersionSchemeTestArgs( + current_version="0.2.0", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=1, + ), + "0.3.0.dev1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.3.1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.3.1a0", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.1a0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.3.1a1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=1, + devrelease=None, + ), + "0.3.1a1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.1a0", + increment=None, + prerelease="alpha", + prerelease_offset=1, + devrelease=None, + ), + "0.3.1a1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.1a0", + increment=None, + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.3.1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.1", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.3.2", + ), + ( + VersionSchemeTestArgs( + current_version="0.4.2", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0a0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0a1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a1", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0a2", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a1", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=1, + ), + "1.0.0a2.dev1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a2.dev0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=1, + ), + "1.0.0a3.dev1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a2.dev0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=0, + ), + "1.0.0a3.dev0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a1", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0b0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0b0", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0b1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0b1", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0rc0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0rc0", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0rc1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0rc0", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=1, + ), + "1.0.0rc1.dev1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0rc0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.0.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a3.dev0", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0b0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.0.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.1", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.0.2", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.2", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.1.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.1.0", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.2.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.2.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.2.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.2.1", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "2.0.0", + ), + # linear prerelease cases + ( + VersionSchemeTestArgs( + current_version="0.1.1b1", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.1.1b2", + ), + ( + VersionSchemeTestArgs( + current_version="0.1.1rc0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.1.1rc1", + ), + ( + VersionSchemeTestArgs( + current_version="0.1.1rc0", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "0.1.1rc1", + ), + # prerelease cases + ( + VersionSchemeTestArgs( + current_version="3.3.3", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "3.3.4a0", + ), + ( + VersionSchemeTestArgs( + current_version="3.3.4a0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "3.3.4a1", + ), + ( + VersionSchemeTestArgs( + current_version="3.3.4a1", + increment="MINOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "3.4.0a0", + ), + ( + VersionSchemeTestArgs( + current_version="3.4.0a0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "3.4.0a1", + ), + ( + VersionSchemeTestArgs( + current_version="3.4.0a1", + increment="MINOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "3.4.0a2", + ), + ( + VersionSchemeTestArgs( + current_version="3.4.0a2", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "4.0.0a0", + ), + ( + VersionSchemeTestArgs( + current_version="4.0.0a0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "4.0.0a1", + ), + ( + VersionSchemeTestArgs( + current_version="4.0.0a1", + increment="MINOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "4.0.0a2", + ), + ( + VersionSchemeTestArgs( + current_version="4.0.0a2", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "4.0.0a3", + ), + # + ( + VersionSchemeTestArgs( + current_version="1.0.0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.1a0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.1a0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.1a1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.1a1", + increment="MINOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.1.0a0", + ), + ( + VersionSchemeTestArgs( + current_version="1.1.0a0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.1.0a1", + ), + ( + VersionSchemeTestArgs( + current_version="1.1.0a1", + increment="MINOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.1.0a2", + ), + ( + VersionSchemeTestArgs( + current_version="1.1.0a2", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "2.0.0a0", + ), + # + ( + VersionSchemeTestArgs( + current_version="1.0.0", + increment="MINOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.1.0a0", + ), + ( + VersionSchemeTestArgs( + current_version="1.1.0a2", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.1.0a3", + ), + ( + VersionSchemeTestArgs( + current_version="1.1.0a3", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "2.0.0a0", + ), + # + ( + VersionSchemeTestArgs( + current_version="1.0.0", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "2.0.0a0", + ), + ( + VersionSchemeTestArgs( + current_version="2.0.0a0", + increment="MINOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "2.0.0a1", + ), + ( + VersionSchemeTestArgs( + current_version="2.0.0a1", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "2.0.0a2", + ), + ( + VersionSchemeTestArgs( + current_version="2.0.0a2", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "2.0.0a3", + ), + ( + VersionSchemeTestArgs( + current_version="2.0.0a3", + increment="MINOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "2.0.0a4", + ), + ( + VersionSchemeTestArgs( + current_version="2.0.0a4", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "2.0.0a5", + ), + ( + VersionSchemeTestArgs( + current_version="2.0.0a5", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "2.0.0a6", + ), + # + ( + VersionSchemeTestArgs( + current_version="2.0.0b0", + increment="MINOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "2.0.0b1", + ), + ( + VersionSchemeTestArgs( + current_version="2.0.0b0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "2.0.0b1", + ), + # + ( + VersionSchemeTestArgs( + current_version="1.0.1a0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.0.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.1a0", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.1.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.1a0", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "2.0.0", + ), + # + ( + VersionSchemeTestArgs( + current_version="1.1.0a0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.1.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.1.0a0", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.1.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.1.0a0", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "2.0.0", + ), + # + ( + VersionSchemeTestArgs( + current_version="2.0.0a0", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "2.0.0", + ), + ( + VersionSchemeTestArgs( + current_version="2.0.0a0", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "2.0.0", + ), + ( + VersionSchemeTestArgs( + current_version="2.0.0a0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "2.0.0", + ), + # + ( + VersionSchemeTestArgs( + current_version="3.0.0a1", + increment=None, + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "3.0.0", + ), + ( + VersionSchemeTestArgs( + current_version="3.0.0b1", + increment=None, + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "3.0.0", + ), + ( + VersionSchemeTestArgs( + current_version="3.0.0rc1", + increment=None, + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "3.0.0", + ), + # + ( + VersionSchemeTestArgs( + current_version="3.1.4", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "3.1.4a0", + ), + ( + VersionSchemeTestArgs( + current_version="3.1.4", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "3.1.4b0", + ), + ( + VersionSchemeTestArgs( + current_version="3.1.4", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "3.1.4rc0", + ), + ( + VersionSchemeTestArgs( + current_version="3.1.4a0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "3.1.4a1", + ), + ( + VersionSchemeTestArgs( + current_version="3.1.4a0", + increment="MINOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "3.2.0a0", + ), + ( + VersionSchemeTestArgs( + current_version="3.1.4a0", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "4.0.0a0", + ), + ], ) -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] +def test_bump_pep440_version(version_args, expected_version): assert ( str( - Pep440(current_version).bump( - increment=increment, - prerelease=prerelease, - prerelease_offset=prerelease_offset, - devrelease=devrelease, + Pep440(version_args.current_version).bump( + increment=version_args.increment, + prerelease=version_args.prerelease, + prerelease_offset=version_args.prerelease_offset, + devrelease=version_args.devrelease, ) ) - == expected + == expected_version ) -@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] +@pytest.mark.parametrize( + ("version_args", "expected_version"), + [ + ( + VersionSchemeTestArgs( + current_version="1.0.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.0.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.1.0", + ), + # with exact_increment=False: "1.0.0b0" + ( + VersionSchemeTestArgs( + current_version="1.0.0a1", + increment="PATCH", + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.1b0", + ), + # with exact_increment=False: "1.0.0b1" + ( + VersionSchemeTestArgs( + current_version="1.0.0b0", + increment="PATCH", + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.1b0", + ), + # with exact_increment=False: "1.0.0rc0" + ( + VersionSchemeTestArgs( + current_version="1.0.0b1", + increment="PATCH", + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.1rc0", + ), + # with exact_increment=False: "1.0.0-rc1" + ( + VersionSchemeTestArgs( + current_version="1.0.0rc0", + increment="PATCH", + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.1rc0", + ), + # with exact_increment=False: "1.0.0rc1-dev1" + ( + VersionSchemeTestArgs( + current_version="1.0.0rc0", + increment="PATCH", + prerelease="rc", + prerelease_offset=0, + devrelease=1, + ), + "1.0.1rc0.dev1", + ), + # with exact_increment=False: "1.0.0b0" + ( + VersionSchemeTestArgs( + current_version="1.0.0a1", + increment="MINOR", + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.1.0b0", + ), + # with exact_increment=False: "1.0.0b1" + ( + VersionSchemeTestArgs( + current_version="1.0.0b0", + increment="MINOR", + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.1.0b0", + ), + # with exact_increment=False: "1.0.0b1" + ( + VersionSchemeTestArgs( + current_version="1.0.0b0", + increment="MINOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.1.0a0", + ), + # with exact_increment=False: "1.0.0rc0" + ( + VersionSchemeTestArgs( + current_version="1.0.0b1", + increment="MINOR", + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.1.0rc0", + ), + # with exact_increment=False: "1.0.0rc1" + ( + VersionSchemeTestArgs( + current_version="1.0.0rc0", + increment="MINOR", + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.1.0rc0", + ), + # with exact_increment=False: "1.0.0rc1-dev1" + ( + VersionSchemeTestArgs( + current_version="1.0.0rc0", + increment="MINOR", + prerelease="rc", + prerelease_offset=0, + devrelease=1, + ), + "1.1.0rc0.dev1", + ), + # with exact_increment=False: "2.0.0" + ( + VersionSchemeTestArgs( + current_version="2.0.0b0", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "3.0.0", + ), + # with exact_increment=False: "2.0.0" + ( + VersionSchemeTestArgs( + current_version="2.0.0b0", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "2.1.0", + ), + # with exact_increment=False: "2.0.0" + ( + VersionSchemeTestArgs( + current_version="2.0.0b0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "2.0.1", + ), + # same with exact_increment=False + ( + VersionSchemeTestArgs( + current_version="2.0.0b0", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "3.0.0a0", + ), + # with exact_increment=False: "2.0.0b1" + ( + VersionSchemeTestArgs( + current_version="2.0.0b0", + increment="MINOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "2.1.0a0", + ), + # with exact_increment=False: "2.0.0b1" + ( + VersionSchemeTestArgs( + current_version="2.0.0b0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "2.0.1a0", + ), + ], +) +def test_bump_pep440_version_force(version_args, expected_version): assert ( str( - Pep440(current_version).bump( - increment=increment, - prerelease=prerelease, - prerelease_offset=prerelease_offset, - devrelease=devrelease, + Pep440(version_args.current_version).bump( + increment=version_args.increment, + prerelease=version_args.prerelease, + prerelease_offset=version_args.prerelease_offset, + devrelease=version_args.devrelease, exact_increment=True, ) ) - == expected + == expected_version ) -@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 +@pytest.mark.parametrize( + ("version_args", "expected_version"), + [ + ( + VersionSchemeTestArgs( + current_version="4.5.0+0.1.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "4.5.0+0.1.1", + ), + ( + VersionSchemeTestArgs( + current_version="4.5.0+0.1.1", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "4.5.0+0.2.0", + ), + ( + VersionSchemeTestArgs( + current_version="4.5.0+0.2.0", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "4.5.0+1.0.0", + ), + ], +) +def test_bump_pep440_version_local(version_args, expected_version): assert ( str( - Pep440(current_version).bump( - increment=increment, - prerelease=prerelease, - prerelease_offset=prerelease_offset, - devrelease=devrelease, - is_local_version=is_local_version, + Pep440(version_args.current_version).bump( + increment=version_args.increment, + prerelease=version_args.prerelease, + prerelease_offset=version_args.prerelease_offset, + devrelease=version_args.devrelease, + is_local_version=True, ) ) - == expected + == expected_version ) @@ -263,76 +1320,3 @@ def test_pep440_scheme_property(): 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 index 71d5e5876c..b5a275e980 100644 --- a/tests/test_version_scheme_semver.py +++ b/tests/test_version_scheme_semver.py @@ -1,189 +1,872 @@ -import itertools -import random +from __future__ import annotations 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"), -] +from tests.utils import VersionSchemeTestArgs @pytest.mark.parametrize( - "test_input, expected", - itertools.chain(tdd_cases, weird_cases, simple_flow, linear_prerelease_cases), + ("version_args", "expected_version"), + [ + ( + VersionSchemeTestArgs( + current_version="0.1.1", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.1.2", + ), + ( + VersionSchemeTestArgs( + current_version="0.1.1", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.2.0", + ), + ( + VersionSchemeTestArgs( + current_version="2.1.1", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "3.0.0", + ), + ( + VersionSchemeTestArgs( + current_version="0.9.0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.9.1-a0", + ), + ( + VersionSchemeTestArgs( + current_version="0.9.0", + increment="MINOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.10.0-a0", + ), + ( + VersionSchemeTestArgs( + current_version="0.9.0", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-a0", + ), + ( + VersionSchemeTestArgs( + current_version="0.9.0", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=1, + devrelease=None, + ), + "1.0.0-a1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a2", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-b0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a2", + increment=None, + prerelease="beta", + prerelease_offset=1, + devrelease=None, + ), + "1.0.0-b1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0beta1", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-rc0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0rc1", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-rc2", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-a0", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-rc0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-alpha1", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-a2", + ), + # weird cases + ( + VersionSchemeTestArgs( + current_version="1.1", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.1.1", + ), + ( + VersionSchemeTestArgs( + current_version="1", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.1.0", + ), + ( + VersionSchemeTestArgs( + current_version="1", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "2.0.0", + ), + ( + VersionSchemeTestArgs( + current_version="1a0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-a1", + ), + ( + VersionSchemeTestArgs( + current_version="1a0", + increment=None, + prerelease="alpha", + prerelease_offset=1, + devrelease=None, + ), + "1.0.0-a1", + ), + ( + VersionSchemeTestArgs( + current_version="1", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-b0", + ), + ( + VersionSchemeTestArgs( + current_version="1", + increment=None, + prerelease="beta", + prerelease_offset=1, + devrelease=None, + ), + "1.0.0-b1", + ), + ( + VersionSchemeTestArgs( + current_version="1beta", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-b1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0alpha1", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-a2", + ), + ( + VersionSchemeTestArgs( + current_version="1", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-rc0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0rc1+e20d7b57f3eb", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.0.0", + ), + # simple flow + ( + VersionSchemeTestArgs( + current_version="0.1.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.1.1", + ), + ( + VersionSchemeTestArgs( + current_version="0.1.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=1, + ), + "0.1.1-dev1", + ), + ( + VersionSchemeTestArgs( + current_version="0.2.0", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.3.0", + ), + ( + VersionSchemeTestArgs( + current_version="0.2.0", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=1, + ), + "0.3.0-dev1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.3.1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.3.1-a0", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.1a0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.3.1-a1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=1, + devrelease=None, + ), + "0.3.1-a1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.1a0", + increment=None, + prerelease="alpha", + prerelease_offset=1, + devrelease=None, + ), + "0.3.1-a1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.1a0", + increment=None, + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.3.1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.1", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.3.2", + ), + ( + VersionSchemeTestArgs( + current_version="0.4.2", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-a0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-a1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a1", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-a2", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a1", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=1, + ), + "1.0.0-a2-dev1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a2.dev0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=1, + ), + "1.0.0-a3-dev1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a2.dev0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=0, + ), + "1.0.0-a3-dev0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a1", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-b0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0b0", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-b1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0b1", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-rc0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0rc0", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-rc1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0rc0", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=1, + ), + "1.0.0-rc1-dev1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0rc0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.0.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0a3.dev0", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-b0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.0.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.1", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.0.2", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.2", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.1.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.1.0", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.2.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.2.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.2.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.2.1", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "2.0.0", + ), + # linear prerelease cases (never bump backwards on pre-releases) + ( + VersionSchemeTestArgs( + current_version="0.1.1b1", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.1.1-b2", + ), + ( + VersionSchemeTestArgs( + current_version="0.1.1rc0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.1.1-rc1", + ), + ( + VersionSchemeTestArgs( + current_version="0.1.1rc0", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "0.1.1-rc1", + ), + ], ) -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] +def test_bump_semver_version( + version_args: VersionSchemeTestArgs, expected_version: str +): assert ( str( - SemVer(current_version).bump( - increment=increment, - prerelease=prerelease, - prerelease_offset=prerelease_offset, - devrelease=devrelease, + SemVer(version_args.current_version).bump( + increment=version_args.increment, + prerelease=version_args.prerelease, + prerelease_offset=version_args.prerelease_offset, + devrelease=version_args.devrelease, ) ) - == expected + == expected_version ) -@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] +@pytest.mark.parametrize( + ("version_args", "expected_version"), + [ + ( + VersionSchemeTestArgs( + current_version="1.0.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.0.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.1.0", + ), + # with exact_increment=False: "1.0.0-b0" + ( + VersionSchemeTestArgs( + current_version="1.0.0a1", + increment="PATCH", + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.1-b0", + ), + # with exact_increment=False: "1.0.0-b1" + ( + VersionSchemeTestArgs( + current_version="1.0.0b0", + increment="PATCH", + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.1-b0", + ), + # with exact_increment=False: "1.0.0-rc0" + ( + VersionSchemeTestArgs( + current_version="1.0.0b1", + increment="PATCH", + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.1-rc0", + ), + # with exact_increment=False: "1.0.0-rc1" + ( + VersionSchemeTestArgs( + current_version="1.0.0rc0", + increment="PATCH", + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.1-rc0", + ), + # with exact_increment=False: "1.0.0-rc1-dev1" + ( + VersionSchemeTestArgs( + current_version="1.0.0rc0", + increment="PATCH", + prerelease="rc", + prerelease_offset=0, + devrelease=1, + ), + "1.0.1-rc0-dev1", + ), + # with exact_increment=False: "1.0.0-b0" + ( + VersionSchemeTestArgs( + current_version="1.0.0a1", + increment="MINOR", + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.1.0-b0", + ), + # with exact_increment=False: "1.0.0-b1" + ( + VersionSchemeTestArgs( + current_version="1.0.0b0", + increment="MINOR", + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.1.0-b0", + ), + # with exact_increment=False: "1.0.0-rc0" + ( + VersionSchemeTestArgs( + current_version="1.0.0b1", + increment="MINOR", + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.1.0-rc0", + ), + # with exact_increment=False: "1.0.0-rc1" + ( + VersionSchemeTestArgs( + current_version="1.0.0rc0", + increment="MINOR", + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.1.0-rc0", + ), + # with exact_increment=False: "1.0.0-rc1-dev1" + ( + VersionSchemeTestArgs( + current_version="1.0.0rc0", + increment="MINOR", + prerelease="rc", + prerelease_offset=0, + devrelease=1, + ), + "1.1.0-rc0-dev1", + ), + # with exact_increment=False: "2.0.0" + ( + VersionSchemeTestArgs( + current_version="2.0.0b0", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "3.0.0", + ), + # with exact_increment=False: "2.0.0" + ( + VersionSchemeTestArgs( + current_version="2.0.0b0", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "2.1.0", + ), + # with exact_increment=False: "2.0.0" + ( + VersionSchemeTestArgs( + current_version="2.0.0b0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "2.0.1", + ), + # same with exact_increment=False + ( + VersionSchemeTestArgs( + current_version="2.0.0b0", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "3.0.0-a0", + ), + # with exact_increment=False: "2.0.0b1" + ( + VersionSchemeTestArgs( + current_version="2.0.0b0", + increment="MINOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "2.1.0-a0", + ), + # with exact_increment=False: "2.0.0b1" + ( + VersionSchemeTestArgs( + current_version="2.0.0b0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "2.0.1-a0", + ), + ], +) +def test_bump_semver_version_force( + version_args: VersionSchemeTestArgs, expected_version: str +): assert ( str( - SemVer(current_version).bump( - increment=increment, - prerelease=prerelease, - prerelease_offset=prerelease_offset, - devrelease=devrelease, + SemVer(version_args.current_version).bump( + increment=version_args.increment, + prerelease=version_args.prerelease, + prerelease_offset=version_args.prerelease_offset, + devrelease=version_args.devrelease, exact_increment=True, ) ) - == expected + == expected_version ) -@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 +@pytest.mark.parametrize( + ("version_args", "expected_version"), + [ + ( + VersionSchemeTestArgs( + current_version="4.5.0+0.1.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "4.5.0+0.1.1", + ), + ( + VersionSchemeTestArgs( + current_version="4.5.0+0.1.1", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "4.5.0+0.2.0", + ), + ( + VersionSchemeTestArgs( + current_version="4.5.0+0.2.0", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "4.5.0+1.0.0", + ), + ], +) +def test_bump_semver_version_local( + version_args: VersionSchemeTestArgs, expected_version: str +): assert ( str( - SemVer(current_version).bump( - increment=increment, - prerelease=prerelease, - prerelease_offset=prerelease_offset, - devrelease=devrelease, - is_local_version=is_local_version, + SemVer(version_args.current_version).bump( + increment=version_args.increment, + prerelease=version_args.prerelease, + prerelease_offset=version_args.prerelease_offset, + devrelease=version_args.devrelease, + is_local_version=True, ) ) - == expected + == expected_version ) @@ -194,76 +877,3 @@ def test_semver_scheme_property(): 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 index d18a058a7c..ddd975bf7a 100644 --- a/tests/test_version_scheme_semver2.py +++ b/tests/test_version_scheme_semver2.py @@ -1,131 +1,624 @@ -import itertools -import random +from __future__ import annotations 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"), -] +from tests.utils import VersionSchemeTestArgs @pytest.mark.parametrize( - "test_input, expected", - itertools.chain(tdd_cases, weird_cases, simple_flow, linear_prerelease_cases), + ("version_args", "expected_version"), + [ + ( + VersionSchemeTestArgs( + current_version="0.1.1", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.1.2", + ), + ( + VersionSchemeTestArgs( + current_version="0.1.1", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.2.0", + ), + ( + VersionSchemeTestArgs( + current_version="2.1.1", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "3.0.0", + ), + ( + VersionSchemeTestArgs( + current_version="0.9.0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.9.1-alpha.0", + ), + ( + VersionSchemeTestArgs( + current_version="0.9.0", + increment="MINOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.10.0-alpha.0", + ), + ( + VersionSchemeTestArgs( + current_version="0.9.0", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-alpha.0", + ), + ( + VersionSchemeTestArgs( + current_version="0.9.0", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=1, + devrelease=None, + ), + "1.0.0-alpha.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-alpha.2", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-beta.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-alpha.2", + increment=None, + prerelease="beta", + prerelease_offset=1, + devrelease=None, + ), + "1.0.0-beta.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-beta.1", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-rc.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-rc.1", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-rc.2", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-alpha.0", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-rc.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-alpha.1", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-alpha.2", + ), + # weird_cases + ( + VersionSchemeTestArgs( + current_version="1.1", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.1.1", + ), + ( + VersionSchemeTestArgs( + current_version="1", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.1.0", + ), + ( + VersionSchemeTestArgs( + current_version="1", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "2.0.0", + ), + ( + VersionSchemeTestArgs( + current_version="1-alpha.0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-alpha.1", + ), + ( + VersionSchemeTestArgs( + current_version="1-alpha.0", + increment=None, + prerelease="alpha", + prerelease_offset=1, + devrelease=None, + ), + "1.0.0-alpha.1", + ), + ( + VersionSchemeTestArgs( + current_version="1", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-beta.0", + ), + ( + VersionSchemeTestArgs( + current_version="1", + increment=None, + prerelease="beta", + prerelease_offset=1, + devrelease=None, + ), + "1.0.0-beta.1", + ), + ( + VersionSchemeTestArgs( + current_version="1-beta", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-beta.1", + ), + ( + VersionSchemeTestArgs( + current_version="1", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-rc.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-rc.1+e20d7b57f3eb", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.0.0", + ), + # simple_flow + ( + VersionSchemeTestArgs( + current_version="0.1.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.1.1", + ), + ( + VersionSchemeTestArgs( + current_version="0.1.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=1, + ), + "0.1.1-dev.1", + ), + ( + VersionSchemeTestArgs( + current_version="0.2.0", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.3.0", + ), + ( + VersionSchemeTestArgs( + current_version="0.2.0", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=1, + ), + "0.3.0-dev.1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.3.1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.3.1-alpha.0", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.1-alpha.0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.3.1-alpha.1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.0", + increment="PATCH", + prerelease="alpha", + prerelease_offset=1, + devrelease=None, + ), + "0.3.1-alpha.1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.1-alpha.0", + increment=None, + prerelease="alpha", + prerelease_offset=1, + devrelease=None, + ), + "0.3.1-alpha.1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.1-alpha.0", + increment=None, + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.3.1", + ), + ( + VersionSchemeTestArgs( + current_version="0.3.1", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "0.3.2", + ), + ( + VersionSchemeTestArgs( + current_version="0.4.2", + increment="MAJOR", + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-alpha.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-alpha.0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-alpha.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-alpha.1", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=1, + ), + "1.0.0-alpha.2.dev.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-alpha.2.dev.0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=1, + ), + "1.0.0-alpha.3.dev.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-alpha.2.dev.0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=0, + ), + "1.0.0-alpha.3.dev.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-alpha.1", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-beta.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-beta.0", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-beta.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-rc.0", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-rc.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-rc.0", + increment=None, + prerelease="rc", + prerelease_offset=0, + devrelease=1, + ), + "1.0.0-rc.1.dev.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-rc.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.0.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0-alpha.3.dev.0", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "1.0.0-beta.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.0.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.1", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.0.2", + ), + ( + VersionSchemeTestArgs( + current_version="1.0.2", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.1.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.1.0", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.2.0", + ), + ( + VersionSchemeTestArgs( + current_version="1.2.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "1.2.1", + ), + ( + VersionSchemeTestArgs( + current_version="1.2.1", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "2.0.0", + ), + # linear prerelease cases (never bump backwards on pre-releases) + ( + VersionSchemeTestArgs( + current_version="0.1.1-beta.1", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.1.1-beta.2", + ), + ( + VersionSchemeTestArgs( + current_version="0.1.1-rc.0", + increment=None, + prerelease="alpha", + prerelease_offset=0, + devrelease=None, + ), + "0.1.1-rc.1", + ), + ( + VersionSchemeTestArgs( + current_version="0.1.1-rc.0", + increment=None, + prerelease="beta", + prerelease_offset=0, + devrelease=None, + ), + "0.1.1-rc.1", + ), + ], ) -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] +def test_bump_semver_version( + version_args: VersionSchemeTestArgs, expected_version: str +): assert ( str( - SemVer2(current_version).bump( - increment=increment, - prerelease=prerelease, - prerelease_offset=prerelease_offset, - devrelease=devrelease, + SemVer2(version_args.current_version).bump( + increment=version_args.increment, + prerelease=version_args.prerelease, + prerelease_offset=version_args.prerelease_offset, + devrelease=version_args.devrelease, ) ) - == expected + == expected_version ) -@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 +@pytest.mark.parametrize( + ("version_args", "expected_version"), + [ + ( + VersionSchemeTestArgs( + current_version="4.5.0+0.1.0", + increment="PATCH", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "4.5.0+0.1.1", + ), + ( + VersionSchemeTestArgs( + current_version="4.5.0+0.1.1", + increment="MINOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "4.5.0+0.2.0", + ), + ( + VersionSchemeTestArgs( + current_version="4.5.0+0.2.0", + increment="MAJOR", + prerelease=None, + prerelease_offset=0, + devrelease=None, + ), + "4.5.0+1.0.0", + ), + ], +) +def test_bump_semver_version_local( + version_args: VersionSchemeTestArgs, expected_version: str +): assert ( str( - SemVer2(current_version).bump( - increment=increment, - prerelease=prerelease, - prerelease_offset=prerelease_offset, - devrelease=devrelease, - is_local_version=is_local_version, + SemVer2(version_args.current_version).bump( + increment=version_args.increment, + prerelease=version_args.prerelease, + prerelease_offset=version_args.prerelease_offset, + devrelease=version_args.devrelease, + is_local_version=True, ) ) - == expected + == expected_version ) @@ -136,76 +629,3 @@ def test_semver_scheme_property(): 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 index 686c0bfde1..0f38f90d80 100644 --- a/tests/test_version_schemes.py +++ b/tests/test_version_schemes.py @@ -1,54 +1,53 @@ from __future__ import annotations -import sys - -if sys.version_info >= (3, 10): - from importlib import metadata -else: - import importlib_metadata as metadata +from importlib import metadata +from typing import TYPE_CHECKING 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 +if TYPE_CHECKING: + from pytest_mock import MockerFixture + + from commitizen.config.base_config import BaseConfig + def test_default_version_scheme_is_pep440(config: BaseConfig): - scheme = get_version_scheme(config) + 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) + 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, "semver") + 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, "unknown") + 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) + with pytest.warns(DeprecationWarning, match="Please use `version_scheme` instead"): + 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) + with pytest.warns(DeprecationWarning, match="Please use `version_scheme` instead"): + scheme = get_version_scheme(config.settings) assert scheme is Pep440 @@ -62,5 +61,8 @@ class NotVersionProtocol: ep.load.return_value = NotVersionProtocol mocker.patch.object(metadata, "entry_points", return_value=(ep,)) - with pytest.warns(match="VersionProtocol"): - get_version_scheme(config, "any") + with pytest.warns() as warnings: + get_version_scheme(config.settings, "any") + assert "Version scheme any does not implement the VersionProtocol" in str( + warnings[0].message + ) diff --git a/tests/utils.py b/tests/utils.py index 971ff91820..bca565f78c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,91 +1,133 @@ from __future__ import annotations import sys -import time import uuid +from dataclasses import dataclass +from datetime import datetime from pathlib import Path +from typing import TYPE_CHECKING, NamedTuple import pytest -from deprecated import deprecated -from commitizen import cmd, exceptions, git +from commitizen import cli, cmd, exceptions, git +from commitizen.cmd import Command -skip_below_py_3_10 = pytest.mark.skipif( - sys.version_info < (3, 10), - reason="The output meesage of argparse is different between Python 3.10 and lower than Python 3.10", -) +if TYPE_CHECKING: + from unittest.mock import Mock + from freezegun.api import FrozenDateTimeFactory + from pytest_mock import MockerFixture -class FakeCommand: - def __init__(self, out=None, err=None, return_code=0): - self.out = out - self.err = err - self.return_code = return_code + from commitizen.version_schemes import Increment, Prerelease -def create_file_and_commit( - message: str, filename: str | None = None, committer_date: str | None = None -): - if not filename: - filename = str(uuid.uuid4()) +class VersionSchemeTestArgs(NamedTuple): + current_version: str + increment: Increment | None + prerelease: Prerelease | None + prerelease_offset: int + devrelease: int | None - 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) +@dataclass +class UtilFixture: + """ + An helper fixture providing most common operations for tests. -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 + git and cli operations grant control over time. """ - time.sleep(1.1) + + mocker: MockerFixture + monkeypatch: pytest.MonkeyPatch + freezer: FrozenDateTimeFactory + + def __post_init__(self): + self.patch_env() + + def create_file_and_commit( + self, + message: str, + filename: str | None = None, + committer_date: str | None = None, + ) -> None: + """Create a file and commit it.""" + 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) + self.tick() + + def create_branch(self, name: str) -> None: + """Create a new branch.""" + c = cmd.run(f"git branch {name}") + if c.return_code != 0: + raise exceptions.GitCommandError(c.err) + + def switch_branch(self, branch: str) -> None: + """Switch to given branch.""" + c = cmd.run(f"git switch {branch}") + if c.return_code != 0: + raise exceptions.GitCommandError(c.err) + + def merge_branch(self, branch: str) -> None: + """Merge given branch into current branch.""" + c = cmd.run(f"git merge {branch}") + if c.return_code != 0: + raise exceptions.GitCommandError(c.err) + self.tick() + + def get_current_branch(self) -> str: + """Get current git branch name.""" + 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( + self, tag: str, message: str | None = None, annotated: bool | None = None + ) -> None: + """Create a git tag.""" + c = git.tag( + tag, annotated=(annotated is True or message is not None), msg=message + ) + if c.return_code != 0: + raise exceptions.CommitError(c.err) + self.tick() + + def run_cli(self, *args: str) -> None: + """Execute commitizen CLI with given args.""" + self.mocker.patch.object(sys, "argv", ["cz", *args]) + cli.main() + self.tick() + + def patch_env(self) -> None: + """Patch environment variables to sync with FreezeGun time.""" + self.monkeypatch.setenv("DATE", datetime.now().isoformat()) + self.monkeypatch.setenv("GIT_AUTHOR_DATE", datetime.now().isoformat()) + self.monkeypatch.setenv("GIT_COMMITTER_DATE", datetime.now().isoformat()) + + def tick(self) -> None: + """Advance time by 1 second.""" + self.freezer.tick() + self.patch_env() + + def mock_cmd(self, out: str = "", err: str = "", return_code: int = 0) -> Mock: + """Mock cmd.run command.""" + return_value = Command(out, err, b"", b"", return_code) + return self.mocker.patch("commitizen.cmd.run", return_value=return_value) + + +@pytest.fixture +def util( + monkeypatch: pytest.MonkeyPatch, + mocker: MockerFixture, + freezer: FrozenDateTimeFactory, +) -> UtilFixture: + """An helper fixture""" + return UtilFixture(mocker=mocker, monkeypatch=monkeypatch, freezer=freezer) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000..2263aad3be --- /dev/null +++ b/uv.lock @@ -0,0 +1,1895 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <4.0" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] + +[[package]] +name = "argcomplete" +version = "3.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backrefs" +version = "6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" }, + { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, +] + +[[package]] +name = "cachetools" +version = "6.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "commitizen" +version = "4.13.9" +source = { editable = "." } +dependencies = [ + { name = "argcomplete" }, + { name = "charset-normalizer" }, + { name = "colorama" }, + { name = "decli" }, + { name = "deprecated" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "prompt-toolkit" }, + { name = "pyyaml" }, + { name = "questionary" }, + { name = "termcolor" }, + { name = "tomlkit" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] + +[package.dev-dependencies] +base = [ + { name = "poethepoet" }, +] +dev = [ + { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "mkdocs" }, + { name = "mkdocs-git-revision-date-localized-plugin" }, + { name = "mkdocs-material" }, + { name = "mypy" }, + { name = "poethepoet" }, + { name = "pre-commit" }, + { name = "prek" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-freezer" }, + { name = "pytest-gitconfig" }, + { name = "pytest-mock" }, + { name = "pytest-regressions" }, + { name = "pytest-xdist" }, + { name = "rich" }, + { name = "ruff" }, + { name = "tox" }, + { name = "tox-uv" }, + { name = "types-colorama" }, + { name = "types-deprecated" }, + { name = "types-python-dateutil" }, + { name = "types-pyyaml" }, + { name = "types-termcolor" }, +] +documentation = [ + { name = "mkdocs" }, + { name = "mkdocs-git-revision-date-localized-plugin" }, + { name = "mkdocs-material" }, +] +linters = [ + { name = "mypy" }, + { name = "prek" }, + { name = "ruff" }, + { name = "types-colorama" }, + { name = "types-deprecated" }, + { name = "types-python-dateutil" }, + { name = "types-pyyaml" }, + { name = "types-termcolor" }, +] +script = [ + { name = "rich" }, +] +test = [ + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-freezer" }, + { name = "pytest-gitconfig" }, + { name = "pytest-mock" }, + { name = "pytest-regressions" }, + { name = "pytest-xdist" }, +] + +[package.metadata] +requires-dist = [ + { name = "argcomplete", specifier = ">=1.12.1,<3.7" }, + { name = "charset-normalizer", specifier = ">=2.1.0,<4" }, + { name = "colorama", specifier = ">=0.4.1,<1.0" }, + { name = "decli", specifier = ">=0.6.0,<1.0" }, + { name = "deprecated", specifier = ">=1.2.13,<2" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'", specifier = ">=8.0.0,<8.7.0" }, + { name = "jinja2", specifier = ">=2.10.3" }, + { name = "packaging", specifier = ">=19" }, + { name = "prompt-toolkit", specifier = "!=3.0.52" }, + { name = "pyyaml", specifier = ">=3.8" }, + { name = "questionary", specifier = ">=2.0,<3.0" }, + { name = "termcolor", specifier = ">=1.1.0,<4.0.0" }, + { name = "tomlkit", specifier = ">=0.8.0,<1.0.0" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'", specifier = ">=4.0.1,<5.0.0" }, +] + +[package.metadata.requires-dev] +base = [{ name = "poethepoet", specifier = ">=0.34.0" }] +dev = [ + { name = "ipython", specifier = ">=8.0" }, + { name = "mkdocs", specifier = ">=1.4.2" }, + { name = "mkdocs-git-revision-date-localized-plugin", specifier = ">=1.5.0" }, + { name = "mkdocs-material", specifier = ">=9.1.6" }, + { name = "mypy", specifier = ">=1.16.0" }, + { name = "poethepoet", specifier = ">=0.34.0" }, + { name = "pre-commit", specifier = ">=4.5.1" }, + { name = "prek", specifier = ">=0.2.28" }, + { name = "pytest", specifier = ">=9" }, + { name = "pytest-cov", specifier = ">=4" }, + { name = "pytest-freezer", specifier = ">=0.4.6" }, + { name = "pytest-gitconfig", specifier = ">=0.9.0" }, + { name = "pytest-mock", specifier = ">=3.10" }, + { name = "pytest-regressions", specifier = ">=2.4.0" }, + { name = "pytest-xdist", specifier = ">=3.1.0" }, + { name = "rich", specifier = ">=13.7.1" }, + { name = "ruff", specifier = ">=0.11.5" }, + { name = "tox", specifier = ">4" }, + { name = "tox-uv" }, + { name = "types-colorama", specifier = ">=0.4.15.20240311" }, + { name = "types-deprecated", specifier = ">=1.2.9.2" }, + { name = "types-python-dateutil", specifier = ">=2.8.19.13" }, + { name = "types-pyyaml", specifier = ">=5.4.3" }, + { name = "types-termcolor", specifier = ">=0.1.1" }, +] +documentation = [ + { name = "mkdocs", specifier = ">=1.4.2" }, + { name = "mkdocs-git-revision-date-localized-plugin", specifier = ">=1.5.0" }, + { name = "mkdocs-material", specifier = ">=9.1.6" }, +] +linters = [ + { name = "mypy", specifier = ">=1.16.0" }, + { name = "prek", specifier = ">=0.2.28" }, + { name = "ruff", specifier = ">=0.11.5" }, + { name = "types-colorama", specifier = ">=0.4.15.20240311" }, + { name = "types-deprecated", specifier = ">=1.2.9.2" }, + { name = "types-python-dateutil", specifier = ">=2.8.19.13" }, + { name = "types-pyyaml", specifier = ">=5.4.3" }, + { name = "types-termcolor", specifier = ">=0.1.1" }, +] +script = [{ name = "rich", specifier = ">=13.7.1" }] +test = [ + { name = "pre-commit", specifier = ">=4.5.1" }, + { name = "pytest", specifier = ">=9" }, + { name = "pytest-cov", specifier = ">=4" }, + { name = "pytest-freezer", specifier = ">=0.4.6" }, + { name = "pytest-gitconfig", specifier = ">=0.9.0" }, + { name = "pytest-mock", specifier = ">=3.10" }, + { name = "pytest-regressions", specifier = ">=2.4.0" }, + { name = "pytest-xdist", specifier = ">=3.1.0" }, +] + +[[package]] +name = "coverage" +version = "7.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/08/bdd7ccca14096f7eb01412b87ac11e5d16e4cb54b6e328afc9dee8bdaec1/coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070", size = 217979, upload-time = "2025-12-08T13:12:14.505Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/d1302e3416298a28b5663ae1117546a745d9d19fde7e28402b2c5c3e2109/coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98", size = 218496, upload-time = "2025-12-08T13:12:16.237Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/d36c354c8b2a320819afcea6bffe72839efd004b98d1d166b90801d49d57/coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5", size = 245237, upload-time = "2025-12-08T13:12:17.858Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/be5e85631e0eec547873d8b08dd67a5f6b111ecfe89a86e40b89b0c1c61c/coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e", size = 247061, upload-time = "2025-12-08T13:12:19.132Z" }, + { url = "https://files.pythonhosted.org/packages/0f/45/a5e8fa0caf05fbd8fa0402470377bff09cc1f026d21c05c71e01295e55ab/coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33", size = 248928, upload-time = "2025-12-08T13:12:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/f5/42/ffb5069b6fd1b95fae482e02f3fecf380d437dd5a39bae09f16d2e2e7e01/coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791", size = 245931, upload-time = "2025-12-08T13:12:22.243Z" }, + { url = "https://files.pythonhosted.org/packages/95/6e/73e809b882c2858f13e55c0c36e94e09ce07e6165d5644588f9517efe333/coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032", size = 246968, upload-time = "2025-12-08T13:12:23.52Z" }, + { url = "https://files.pythonhosted.org/packages/87/08/64ebd9e64b6adb8b4a4662133d706fbaccecab972e0b3ccc23f64e2678ad/coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9", size = 244972, upload-time = "2025-12-08T13:12:24.781Z" }, + { url = "https://files.pythonhosted.org/packages/12/97/f4d27c6fe0cb375a5eced4aabcaef22de74766fb80a3d5d2015139e54b22/coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f", size = 245241, upload-time = "2025-12-08T13:12:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/0c/94/42f8ae7f633bf4c118bf1038d80472f9dade88961a466f290b81250f7ab7/coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8", size = 245847, upload-time = "2025-12-08T13:12:29.337Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/6369ca22b6b6d933f4f4d27765d313d8914cc4cce84f82a16436b1a233db/coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f", size = 220573, upload-time = "2025-12-08T13:12:30.905Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dc/a6a741e519acceaeccc70a7f4cfe5d030efc4b222595f0677e101af6f1f3/coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303", size = 221509, upload-time = "2025-12-08T13:12:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dc/888bf90d8b1c3d0b4020a40e52b9f80957d75785931ec66c7dfaccc11c7d/coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820", size = 218104, upload-time = "2025-12-08T13:12:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ea/069d51372ad9c380214e86717e40d1a743713a2af191cfba30a0911b0a4a/coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f", size = 218606, upload-time = "2025-12-08T13:12:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/68/09/77b1c3a66c2aa91141b6c4471af98e5b1ed9b9e6d17255da5eb7992299e3/coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96", size = 248999, upload-time = "2025-12-08T13:12:36.02Z" }, + { url = "https://files.pythonhosted.org/packages/0a/32/2e2f96e9d5691eaf1181d9040f850b8b7ce165ea10810fd8e2afa534cef7/coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259", size = 250925, upload-time = "2025-12-08T13:12:37.221Z" }, + { url = "https://files.pythonhosted.org/packages/7b/45/b88ddac1d7978859b9a39a8a50ab323186148f1d64bc068f86fc77706321/coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb", size = 253032, upload-time = "2025-12-08T13:12:38.763Z" }, + { url = "https://files.pythonhosted.org/packages/71/cb/e15513f94c69d4820a34b6bf3d2b1f9f8755fa6021be97c7065442d7d653/coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9", size = 249134, upload-time = "2025-12-08T13:12:40.382Z" }, + { url = "https://files.pythonhosted.org/packages/09/61/d960ff7dc9e902af3310ce632a875aaa7860f36d2bc8fc8b37ee7c1b82a5/coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030", size = 250731, upload-time = "2025-12-08T13:12:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/98/34/c7c72821794afc7c7c2da1db8f00c2c98353078aa7fb6b5ff36aac834b52/coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833", size = 248795, upload-time = "2025-12-08T13:12:43.331Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/e0f07107987a43b2def9aa041c614ddb38064cbf294a71ef8c67d43a0cdd/coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8", size = 248514, upload-time = "2025-12-08T13:12:44.546Z" }, + { url = "https://files.pythonhosted.org/packages/71/c2/c949c5d3b5e9fc6dd79e1b73cdb86a59ef14f3709b1d72bf7668ae12e000/coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753", size = 249424, upload-time = "2025-12-08T13:12:45.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/f1/bbc009abd6537cec0dffb2cc08c17a7f03de74c970e6302db4342a6e05af/coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b", size = 220597, upload-time = "2025-12-08T13:12:47.378Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/d9977f2fb51c10fbaed0718ce3d0a8541185290b981f73b1d27276c12d91/coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe", size = 221536, upload-time = "2025-12-08T13:12:48.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/ad/3fcf43fd96fb43e337a3073dea63ff148dcc5c41ba7a14d4c7d34efb2216/coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7", size = 220206, upload-time = "2025-12-08T13:12:50.365Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f1/2619559f17f31ba00fc40908efd1fbf1d0a5536eb75dc8341e7d660a08de/coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf", size = 218274, upload-time = "2025-12-08T13:12:52.095Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/30d71ae5d6e949ff93b2a79a2c1b4822e00423116c5c6edfaeef37301396/coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f", size = 218638, upload-time = "2025-12-08T13:12:53.418Z" }, + { url = "https://files.pythonhosted.org/packages/79/c2/fce80fc6ded8d77e53207489d6065d0fed75db8951457f9213776615e0f5/coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb", size = 250129, upload-time = "2025-12-08T13:12:54.744Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b6/51b5d1eb6fcbb9a1d5d6984e26cbe09018475c2922d554fd724dd0f056ee/coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621", size = 252885, upload-time = "2025-12-08T13:12:56.401Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/972a5affea41de798691ab15d023d3530f9f56a72e12e243f35031846ff7/coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74", size = 253974, upload-time = "2025-12-08T13:12:57.718Z" }, + { url = "https://files.pythonhosted.org/packages/8a/56/116513aee860b2c7968aa3506b0f59b22a959261d1dbf3aea7b4450a7520/coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57", size = 250538, upload-time = "2025-12-08T13:12:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/d6/75/074476d64248fbadf16dfafbf93fdcede389ec821f74ca858d7c87d2a98c/coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8", size = 251912, upload-time = "2025-12-08T13:13:00.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d2/aa4f8acd1f7c06024705c12609d8698c51b27e4d635d717cd1934c9668e2/coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d", size = 250054, upload-time = "2025-12-08T13:13:01.892Z" }, + { url = "https://files.pythonhosted.org/packages/19/98/8df9e1af6a493b03694a1e8070e024e7d2cdc77adedc225a35e616d505de/coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b", size = 249619, upload-time = "2025-12-08T13:13:03.236Z" }, + { url = "https://files.pythonhosted.org/packages/d8/71/f8679231f3353018ca66ef647fa6fe7b77e6bff7845be54ab84f86233363/coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd", size = 251496, upload-time = "2025-12-08T13:13:04.511Z" }, + { url = "https://files.pythonhosted.org/packages/04/86/9cb406388034eaf3c606c22094edbbb82eea1fa9d20c0e9efadff20d0733/coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef", size = 220808, upload-time = "2025-12-08T13:13:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/1c/59/af483673df6455795daf5f447c2f81a3d2fcfc893a22b8ace983791f6f34/coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae", size = 221616, upload-time = "2025-12-08T13:13:07.95Z" }, + { url = "https://files.pythonhosted.org/packages/64/b0/959d582572b30a6830398c60dd419c1965ca4b5fb38ac6b7093a0d50ca8d/coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080", size = 220261, upload-time = "2025-12-08T13:13:09.581Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" }, + { url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" }, + { url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" }, + { url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" }, + { url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" }, + { url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" }, + { url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" }, + { url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" }, + { url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" }, + { url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" }, + { url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" }, + { url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" }, + { url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" }, + { url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" }, + { url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" }, + { url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" }, + { url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" }, + { url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" }, + { url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" }, + { url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" }, + { url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" }, + { url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" }, + { url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" }, + { url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "decli" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/59/d4ffff1dee2c8f6f2dd8f87010962e60f7b7847504d765c91ede5a466730/decli-0.6.3.tar.gz", hash = "sha256:87f9d39361adf7f16b9ca6e3b614badf7519da13092f2db3c80ca223c53c7656", size = 7564, upload-time = "2025-06-01T15:23:41.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/fa/ec878c28bc7f65b77e7e17af3522c9948a9711b9fa7fc4c5e3140a7e3578/decli-0.6.3-py3-none-any.whl", hash = "sha256:5152347c7bb8e3114ad65db719e5709b28d7f7f45bdb709f70167925e55640f3", size = 7989, upload-time = "2025-06-01T15:23:40.228Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, +] + +[[package]] +name = "freezegun" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/dd/23e2f4e357f8fd3bdff613c1fe4466d21bfb00a6177f238079b17f7b1c84/freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a", size = 35914, upload-time = "2025-08-09T10:39:08.338Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/2e/b41d8a1a917d6581fc27a35d05561037b048e47df50f27f8ac9c7e27a710/freezegun-1.5.5-py3-none-any.whl", hash = "sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2", size = 19266, upload-time = "2025-08-09T10:39:06.636Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.46" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, +] + +[[package]] +name = "identify" +version = "2.6.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "ipython" +version = "8.37.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi", marker = "python_full_version < '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, + { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "stack-data", marker = "python_full_version < '3.11'" }, + { name = "traitlets", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088, upload-time = "2025-05-31T16:39:09.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/d0/274fbf7b0b12643cbbc001ce13e6a5b1607ac4929d1b11c72460152c9fc3/ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2", size = 831864, upload-time = "2025-05-31T16:39:06.38Z" }, +] + +[[package]] +name = "ipython" +version = "9.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.11'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/51/a703c030f4928646d390b4971af4938a1b10c9dfce694f0d99a0bb073cb2/ipython-9.8.0.tar.gz", hash = "sha256:8e4ce129a627eb9dd221c41b1d2cdaed4ef7c9da8c17c63f6f578fe231141f83", size = 4424940, upload-time = "2025-12-03T10:18:24.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/df/8ee1c5dd1e3308b5d5b2f2dfea323bb2f3827da8d654abb6642051199049/ipython-9.8.0-py3-none-any.whl", hash = "sha256:ebe6d1d58d7d988fbf23ff8ff6d8e1622cfdb194daf4b7b73b792c4ec3b85385", size = 621374, upload-time = "2025-12-03T10:18:22.335Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "librt" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/93/e4/b59bdf1197fdf9888452ea4d2048cdad61aef85eb83e99dc52551d7fdc04/librt-0.7.4.tar.gz", hash = "sha256:3871af56c59864d5fd21d1ac001eb2fb3b140d52ba0454720f2e4a19812404ba", size = 145862, upload-time = "2025-12-15T16:52:43.862Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/1e/3e61dff6c07a3b400fe907d3164b92b3b3023ef86eac1ee236869dc276f7/librt-0.7.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dc300cb5a5a01947b1ee8099233156fdccd5001739e5f596ecfbc0dab07b5a3b", size = 54708, upload-time = "2025-12-15T16:51:03.752Z" }, + { url = "https://files.pythonhosted.org/packages/87/98/ab2428b0a80d0fd67decaeea84a5ec920e3dd4d95ecfd074c71f51bd7315/librt-0.7.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee8d3323d921e0f6919918a97f9b5445a7dfe647270b2629ec1008aa676c0bc0", size = 56656, upload-time = "2025-12-15T16:51:05.038Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/de1fad3a16e4fb5b6605bd6cbe6d0e5207cc8eca58993835749a1da0812b/librt-0.7.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:95cb80854a355b284c55f79674f6187cc9574df4dc362524e0cce98c89ee8331", size = 161024, upload-time = "2025-12-15T16:51:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/88/00/ddfcdc1147dd7fb68321d7b064b12f0b9101d85f466a46006f86096fde8d/librt-0.7.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca1caedf8331d8ad6027f93b52d68ed8f8009f5c420c246a46fe9d3be06be0f", size = 169529, upload-time = "2025-12-15T16:51:07.907Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b3/915702c7077df2483b015030d1979404474f490fe9a071e9576f7b26fef6/librt-0.7.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a6f1236151e6fe1da289351b5b5bce49651c91554ecc7b70a947bced6fe212", size = 183270, upload-time = "2025-12-15T16:51:09.164Z" }, + { url = "https://files.pythonhosted.org/packages/45/19/ab2f217e8ec509fca4ea9e2e5022b9f72c1a7b7195f5a5770d299df807ea/librt-0.7.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7766b57aeebaf3f1dac14fdd4a75c9a61f2ed56d8ebeefe4189db1cb9d2a3783", size = 179038, upload-time = "2025-12-15T16:51:10.538Z" }, + { url = "https://files.pythonhosted.org/packages/10/1c/d40851d187662cf50312ebbc0b277c7478dd78dbaaf5ee94056f1d7f2f83/librt-0.7.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1c4c89fb01157dd0a3bfe9e75cd6253b0a1678922befcd664eca0772a4c6c979", size = 173502, upload-time = "2025-12-15T16:51:11.888Z" }, + { url = "https://files.pythonhosted.org/packages/07/52/d5880835c772b22c38db18660420fa6901fd9e9a433b65f0ba9b0f4da764/librt-0.7.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7fa8beef580091c02b4fd26542de046b2abfe0aaefa02e8bcf68acb7618f2b3", size = 193570, upload-time = "2025-12-15T16:51:13.168Z" }, + { url = "https://files.pythonhosted.org/packages/f1/35/22d3c424b82f86ce019c0addadf001d459dfac8036aecc07fadc5c541053/librt-0.7.4-cp310-cp310-win32.whl", hash = "sha256:543c42fa242faae0466fe72d297976f3c710a357a219b1efde3a0539a68a6997", size = 42596, upload-time = "2025-12-15T16:51:14.422Z" }, + { url = "https://files.pythonhosted.org/packages/95/b1/e7c316ac5fe60ac1fdfe515198087205220803c4cf923ee63e1cb8380b17/librt-0.7.4-cp310-cp310-win_amd64.whl", hash = "sha256:25cc40d8eb63f0a7ea4c8f49f524989b9df901969cb860a2bc0e4bad4b8cb8a8", size = 48972, upload-time = "2025-12-15T16:51:15.516Z" }, + { url = "https://files.pythonhosted.org/packages/84/64/44089b12d8b4714a7f0e2f33fb19285ba87702d4be0829f20b36ebeeee07/librt-0.7.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3485b9bb7dfa66167d5500ffdafdc35415b45f0da06c75eb7df131f3357b174a", size = 54709, upload-time = "2025-12-15T16:51:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/26/ef/6fa39fb5f37002f7d25e0da4f24d41b457582beea9369eeb7e9e73db5508/librt-0.7.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:188b4b1a770f7f95ea035d5bbb9d7367248fc9d12321deef78a269ebf46a5729", size = 56663, upload-time = "2025-12-15T16:51:17.856Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e4/cbaca170a13bee2469c90df9e47108610b4422c453aea1aec1779ac36c24/librt-0.7.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1b668b1c840183e4e38ed5a99f62fac44c3a3eef16870f7f17cfdfb8b47550ed", size = 161703, upload-time = "2025-12-15T16:51:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/d0/32/0b2296f9cc7e693ab0d0835e355863512e5eac90450c412777bd699c76ae/librt-0.7.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e8f864b521f6cfedb314d171630f827efee08f5c3462bcbc2244ab8e1768cd6", size = 171027, upload-time = "2025-12-15T16:51:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/d8/33/c70b6d40f7342716e5f1353c8da92d9e32708a18cbfa44897a93ec2bf879/librt-0.7.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df7c9def4fc619a9c2ab402d73a0c5b53899abe090e0100323b13ccb5a3dd82", size = 184700, upload-time = "2025-12-15T16:51:22.272Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c8/555c405155da210e4c4113a879d378f54f850dbc7b794e847750a8fadd43/librt-0.7.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f79bc3595b6ed159a1bf0cdc70ed6ebec393a874565cab7088a219cca14da727", size = 180719, upload-time = "2025-12-15T16:51:23.561Z" }, + { url = "https://files.pythonhosted.org/packages/6b/88/34dc1f1461c5613d1b73f0ecafc5316cc50adcc1b334435985b752ed53e5/librt-0.7.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77772a4b8b5f77d47d883846928c36d730b6e612a6388c74cba33ad9eb149c11", size = 174535, upload-time = "2025-12-15T16:51:25.031Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5a/f3fafe80a221626bcedfa9fe5abbf5f04070989d44782f579b2d5920d6d0/librt-0.7.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:064a286e6ab0b4c900e228ab4fa9cb3811b4b83d3e0cc5cd816b2d0f548cb61c", size = 195236, upload-time = "2025-12-15T16:51:26.328Z" }, + { url = "https://files.pythonhosted.org/packages/d8/77/5c048d471ce17f4c3a6e08419be19add4d291e2f7067b877437d482622ac/librt-0.7.4-cp311-cp311-win32.whl", hash = "sha256:42da201c47c77b6cc91fc17e0e2b330154428d35d6024f3278aa2683e7e2daf2", size = 42930, upload-time = "2025-12-15T16:51:27.853Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3b/514a86305a12c3d9eac03e424b07cd312c7343a9f8a52719aa079590a552/librt-0.7.4-cp311-cp311-win_amd64.whl", hash = "sha256:d31acb5886c16ae1711741f22504195af46edec8315fe69b77e477682a87a83e", size = 49240, upload-time = "2025-12-15T16:51:29.037Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/3b7b1914f565926b780a734fac6e9a4d2c7aefe41f4e89357d73697a9457/librt-0.7.4-cp311-cp311-win_arm64.whl", hash = "sha256:114722f35093da080a333b3834fff04ef43147577ed99dd4db574b03a5f7d170", size = 42613, upload-time = "2025-12-15T16:51:30.194Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e7/b805d868d21f425b7e76a0ea71a2700290f2266a4f3c8357fcf73efc36aa/librt-0.7.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7dd3b5c37e0fb6666c27cf4e2c88ae43da904f2155c4cfc1e5a2fdce3b9fcf92", size = 55688, upload-time = "2025-12-15T16:51:31.571Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/69a2b02e62a14cfd5bfd9f1e9adea294d5bcfeea219c7555730e5d068ee4/librt-0.7.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c5de1928c486201b23ed0cc4ac92e6e07be5cd7f3abc57c88a9cf4f0f32108", size = 57141, upload-time = "2025-12-15T16:51:32.714Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6b/05dba608aae1272b8ea5ff8ef12c47a4a099a04d1e00e28a94687261d403/librt-0.7.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:078ae52ffb3f036396cc4aed558e5b61faedd504a3c1f62b8ae34bf95ae39d94", size = 165322, upload-time = "2025-12-15T16:51:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/8f/bc/199533d3fc04a4cda8d7776ee0d79955ab0c64c79ca079366fbc2617e680/librt-0.7.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce58420e25097b2fc201aef9b9f6d65df1eb8438e51154e1a7feb8847e4a55ab", size = 174216, upload-time = "2025-12-15T16:51:35.384Z" }, + { url = "https://files.pythonhosted.org/packages/62/ec/09239b912a45a8ed117cb4a6616d9ff508f5d3131bd84329bf2f8d6564f1/librt-0.7.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b719c8730c02a606dc0e8413287e8e94ac2d32a51153b300baf1f62347858fba", size = 189005, upload-time = "2025-12-15T16:51:36.687Z" }, + { url = "https://files.pythonhosted.org/packages/46/2e/e188313d54c02f5b0580dd31476bb4b0177514ff8d2be9f58d4a6dc3a7ba/librt-0.7.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3749ef74c170809e6dee68addec9d2458700a8de703de081c888e92a8b015cf9", size = 183960, upload-time = "2025-12-15T16:51:37.977Z" }, + { url = "https://files.pythonhosted.org/packages/eb/84/f1d568d254518463d879161d3737b784137d236075215e56c7c9be191cee/librt-0.7.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b35c63f557653c05b5b1b6559a074dbabe0afee28ee2a05b6c9ba21ad0d16a74", size = 177609, upload-time = "2025-12-15T16:51:40.584Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/060bbc1c002f0d757c33a1afe6bf6a565f947a04841139508fc7cef6c08b/librt-0.7.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1ef704e01cb6ad39ad7af668d51677557ca7e5d377663286f0ee1b6b27c28e5f", size = 199269, upload-time = "2025-12-15T16:51:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7f/708f8f02d8012ee9f366c07ea6a92882f48bd06cc1ff16a35e13d0fbfb08/librt-0.7.4-cp312-cp312-win32.whl", hash = "sha256:c66c2b245926ec15188aead25d395091cb5c9df008d3b3207268cd65557d6286", size = 43186, upload-time = "2025-12-15T16:51:43.149Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a5/4e051b061c8b2509be31b2c7ad4682090502c0a8b6406edcf8c6b4fe1ef7/librt-0.7.4-cp312-cp312-win_amd64.whl", hash = "sha256:71a56f4671f7ff723451f26a6131754d7c1809e04e22ebfbac1db8c9e6767a20", size = 49455, upload-time = "2025-12-15T16:51:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d2/90d84e9f919224a3c1f393af1636d8638f54925fdc6cd5ee47f1548461e5/librt-0.7.4-cp312-cp312-win_arm64.whl", hash = "sha256:419eea245e7ec0fe664eb7e85e7ff97dcdb2513ca4f6b45a8ec4a3346904f95a", size = 42828, upload-time = "2025-12-15T16:51:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/fe/4d/46a53ccfbb39fd0b493fd4496eb76f3ebc15bb3e45d8c2e695a27587edf5/librt-0.7.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d44a1b1ba44cbd2fc3cb77992bef6d6fdb1028849824e1dd5e4d746e1f7f7f0b", size = 55745, upload-time = "2025-12-15T16:51:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2b/3ac7f5212b1828bf4f979cf87f547db948d3e28421d7a430d4db23346ce4/librt-0.7.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c9cab4b3de1f55e6c30a84c8cee20e4d3b2476f4d547256694a1b0163da4fe32", size = 57166, upload-time = "2025-12-15T16:51:48.219Z" }, + { url = "https://files.pythonhosted.org/packages/e8/99/6523509097cbe25f363795f0c0d1c6a3746e30c2994e25b5aefdab119b21/librt-0.7.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2857c875f1edd1feef3c371fbf830a61b632fb4d1e57160bb1e6a3206e6abe67", size = 165833, upload-time = "2025-12-15T16:51:49.443Z" }, + { url = "https://files.pythonhosted.org/packages/fe/35/323611e59f8fe032649b4fb7e77f746f96eb7588fcbb31af26bae9630571/librt-0.7.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b370a77be0a16e1ad0270822c12c21462dc40496e891d3b0caf1617c8cc57e20", size = 174818, upload-time = "2025-12-15T16:51:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/40fb2bb21616c6e06b6a64022802228066e9a31618f493e03f6b9661548a/librt-0.7.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d05acd46b9a52087bfc50c59dfdf96a2c480a601e8898a44821c7fd676598f74", size = 189607, upload-time = "2025-12-15T16:51:52.671Z" }, + { url = "https://files.pythonhosted.org/packages/32/48/1b47c7d5d28b775941e739ed2bfe564b091c49201b9503514d69e4ed96d7/librt-0.7.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:70969229cb23d9c1a80e14225838d56e464dc71fa34c8342c954fc50e7516dee", size = 184585, upload-time = "2025-12-15T16:51:54.027Z" }, + { url = "https://files.pythonhosted.org/packages/75/a6/ee135dfb5d3b54d5d9001dbe483806229c6beac3ee2ba1092582b7efeb1b/librt-0.7.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4450c354b89dbb266730893862dbff06006c9ed5b06b6016d529b2bf644fc681", size = 178249, upload-time = "2025-12-15T16:51:55.248Z" }, + { url = "https://files.pythonhosted.org/packages/04/87/d5b84ec997338be26af982bcd6679be0c1db9a32faadab1cf4bb24f9e992/librt-0.7.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:adefe0d48ad35b90b6f361f6ff5a1bd95af80c17d18619c093c60a20e7a5b60c", size = 199851, upload-time = "2025-12-15T16:51:56.933Z" }, + { url = "https://files.pythonhosted.org/packages/86/63/ba1333bf48306fe398e3392a7427ce527f81b0b79d0d91618c4610ce9d15/librt-0.7.4-cp313-cp313-win32.whl", hash = "sha256:21ea710e96c1e050635700695095962a22ea420d4b3755a25e4909f2172b4ff2", size = 43249, upload-time = "2025-12-15T16:51:58.498Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8a/de2c6df06cdfa9308c080e6b060fe192790b6a48a47320b215e860f0e98c/librt-0.7.4-cp313-cp313-win_amd64.whl", hash = "sha256:772e18696cf5a64afee908662fbcb1f907460ddc851336ee3a848ef7684c8e1e", size = 49417, upload-time = "2025-12-15T16:51:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/31/66/8ee0949efc389691381ed686185e43536c20e7ad880c122dd1f31e65c658/librt-0.7.4-cp313-cp313-win_arm64.whl", hash = "sha256:52e34c6af84e12921748c8354aa6acf1912ca98ba60cdaa6920e34793f1a0788", size = 42824, upload-time = "2025-12-15T16:52:00.784Z" }, + { url = "https://files.pythonhosted.org/packages/74/81/6921e65c8708eb6636bbf383aa77e6c7dad33a598ed3b50c313306a2da9d/librt-0.7.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4f1ee004942eaaed6e06c087d93ebc1c67e9a293e5f6b9b5da558df6bf23dc5d", size = 55191, upload-time = "2025-12-15T16:52:01.97Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d6/3eb864af8a8de8b39cc8dd2e9ded1823979a27795d72c4eea0afa8c26c9f/librt-0.7.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d854c6dc0f689bad7ed452d2a3ecff58029d80612d336a45b62c35e917f42d23", size = 56898, upload-time = "2025-12-15T16:52:03.356Z" }, + { url = "https://files.pythonhosted.org/packages/49/bc/b1d4c0711fdf79646225d576faee8747b8528a6ec1ceb6accfd89ade7102/librt-0.7.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4f7339d9e445280f23d63dea842c0c77379c4a47471c538fc8feedab9d8d063", size = 163725, upload-time = "2025-12-15T16:52:04.572Z" }, + { url = "https://files.pythonhosted.org/packages/2c/08/61c41cd8f0a6a41fc99ea78a2205b88187e45ba9800792410ed62f033584/librt-0.7.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39003fc73f925e684f8521b2dbf34f61a5deb8a20a15dcf53e0d823190ce8848", size = 172469, upload-time = "2025-12-15T16:52:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c7/4ee18b4d57f01444230bc18cf59103aeab8f8c0f45e84e0e540094df1df1/librt-0.7.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb15ee29d95875ad697d449fe6071b67f730f15a6961913a2b0205015ca0843", size = 186804, upload-time = "2025-12-15T16:52:07.192Z" }, + { url = "https://files.pythonhosted.org/packages/a1/af/009e8ba3fbf830c936842da048eda1b34b99329f402e49d88fafff6525d1/librt-0.7.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:02a69369862099e37d00765583052a99d6a68af7e19b887e1b78fee0146b755a", size = 181807, upload-time = "2025-12-15T16:52:08.554Z" }, + { url = "https://files.pythonhosted.org/packages/85/26/51ae25f813656a8b117c27a974f25e8c1e90abcd5a791ac685bf5b489a1b/librt-0.7.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ec72342cc4d62f38b25a94e28b9efefce41839aecdecf5e9627473ed04b7be16", size = 175595, upload-time = "2025-12-15T16:52:10.186Z" }, + { url = "https://files.pythonhosted.org/packages/48/93/36d6c71f830305f88996b15c8e017aa8d1e03e2e947b40b55bbf1a34cf24/librt-0.7.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:776dbb9bfa0fc5ce64234b446995d8d9f04badf64f544ca036bd6cff6f0732ce", size = 196504, upload-time = "2025-12-15T16:52:11.472Z" }, + { url = "https://files.pythonhosted.org/packages/08/11/8299e70862bb9d704735bf132c6be09c17b00fbc7cda0429a9df222fdc1b/librt-0.7.4-cp314-cp314-win32.whl", hash = "sha256:0f8cac84196d0ffcadf8469d9ded4d4e3a8b1c666095c2a291e22bf58e1e8a9f", size = 39738, upload-time = "2025-12-15T16:52:12.962Z" }, + { url = "https://files.pythonhosted.org/packages/54/d5/656b0126e4e0f8e2725cd2d2a1ec40f71f37f6f03f135a26b663c0e1a737/librt-0.7.4-cp314-cp314-win_amd64.whl", hash = "sha256:037f5cb6fe5abe23f1dc058054d50e9699fcc90d0677eee4e4f74a8677636a1a", size = 45976, upload-time = "2025-12-15T16:52:14.441Z" }, + { url = "https://files.pythonhosted.org/packages/60/86/465ff07b75c1067da8fa7f02913c4ead096ef106cfac97a977f763783bfb/librt-0.7.4-cp314-cp314-win_arm64.whl", hash = "sha256:a5deebb53d7a4d7e2e758a96befcd8edaaca0633ae71857995a0f16033289e44", size = 39073, upload-time = "2025-12-15T16:52:15.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a0/24941f85960774a80d4b3c2aec651d7d980466da8101cae89e8b032a3e21/librt-0.7.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b4c25312c7f4e6ab35ab16211bdf819e6e4eddcba3b2ea632fb51c9a2a97e105", size = 57369, upload-time = "2025-12-15T16:52:16.782Z" }, + { url = "https://files.pythonhosted.org/packages/77/a0/ddb259cae86ab415786c1547d0fe1b40f04a7b089f564fd5c0242a3fafb2/librt-0.7.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:618b7459bb392bdf373f2327e477597fff8f9e6a1878fffc1b711c013d1b0da4", size = 59230, upload-time = "2025-12-15T16:52:18.259Z" }, + { url = "https://files.pythonhosted.org/packages/31/11/77823cb530ab8a0c6fac848ac65b745be446f6f301753b8990e8809080c9/librt-0.7.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1437c3f72a30c7047f16fd3e972ea58b90172c3c6ca309645c1c68984f05526a", size = 183869, upload-time = "2025-12-15T16:52:19.457Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ce/157db3614cf3034b3f702ae5ba4fefda4686f11eea4b7b96542324a7a0e7/librt-0.7.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c96cb76f055b33308f6858b9b594618f1b46e147a4d03a4d7f0c449e304b9b95", size = 194606, upload-time = "2025-12-15T16:52:20.795Z" }, + { url = "https://files.pythonhosted.org/packages/30/ef/6ec4c7e3d6490f69a4fd2803516fa5334a848a4173eac26d8ee6507bff6e/librt-0.7.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28f990e6821204f516d09dc39966ef8b84556ffd648d5926c9a3f681e8de8906", size = 206776, upload-time = "2025-12-15T16:52:22.229Z" }, + { url = "https://files.pythonhosted.org/packages/ad/22/750b37bf549f60a4782ab80e9d1e9c44981374ab79a7ea68670159905918/librt-0.7.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc4aebecc79781a1b77d7d4e7d9fe080385a439e198d993b557b60f9117addaf", size = 203205, upload-time = "2025-12-15T16:52:23.603Z" }, + { url = "https://files.pythonhosted.org/packages/7a/87/2e8a0f584412a93df5faad46c5fa0a6825fdb5eba2ce482074b114877f44/librt-0.7.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:022cc673e69283a42621dd453e2407cf1647e77f8bd857d7ad7499901e62376f", size = 196696, upload-time = "2025-12-15T16:52:24.951Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ca/7bf78fa950e43b564b7de52ceeb477fb211a11f5733227efa1591d05a307/librt-0.7.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2b3ca211ae8ea540569e9c513da052699b7b06928dcda61247cb4f318122bdb5", size = 217191, upload-time = "2025-12-15T16:52:26.194Z" }, + { url = "https://files.pythonhosted.org/packages/d6/49/3732b0e8424ae35ad5c3166d9dd5bcdae43ce98775e0867a716ff5868064/librt-0.7.4-cp314-cp314t-win32.whl", hash = "sha256:8a461f6456981d8c8e971ff5a55f2e34f4e60871e665d2f5fde23ee74dea4eeb", size = 40276, upload-time = "2025-12-15T16:52:27.54Z" }, + { url = "https://files.pythonhosted.org/packages/35/d6/d8823e01bd069934525fddb343189c008b39828a429b473fb20d67d5cd36/librt-0.7.4-cp314-cp314t-win_amd64.whl", hash = "sha256:721a7b125a817d60bf4924e1eec2a7867bfcf64cfc333045de1df7a0629e4481", size = 46772, upload-time = "2025-12-15T16:52:28.653Z" }, + { url = "https://files.pythonhosted.org/packages/36/e9/a0aa60f5322814dd084a89614e9e31139702e342f8459ad8af1984a18168/librt-0.7.4-cp314-cp314t-win_arm64.whl", hash = "sha256:76b2ba71265c0102d11458879b4d53ccd0b32b0164d14deb8d2b598a018e502f", size = 39724, upload-time = "2025-12-15T16:52:29.836Z" }, +] + +[[package]] +name = "markdown" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-git-revision-date-localized-plugin" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "gitpython" }, + { name = "mkdocs" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/c5/1d3c4e6ddae6230b89d09105cb79de711655e3ebd6745f7a92efea0f5160/mkdocs_git_revision_date_localized_plugin-1.5.0.tar.gz", hash = "sha256:17345ccfdf69a1905dc96fb1070dce82d03a1eb6b0d48f958081a7589ce3c248", size = 460697, upload-time = "2025-10-31T16:11:34.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/51/fe0e3fdb16f6eed65c9459d12bae6a4e1f0bb4e2228cb037e7907b002678/mkdocs_git_revision_date_localized_plugin-1.5.0-py3-none-any.whl", hash = "sha256:933f9e35a8c135b113f21bb57610d82e9b7bcc71dd34fb06a029053c97e99656", size = 26153, upload-time = "2025-10-31T16:11:32.987Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "parso" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, +] + +[[package]] +name = "pastel" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/f1/4594f5e0fcddb6953e5b8fe00da8c317b8b41b547e2b3ae2da7512943c62/pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d", size = 7555, upload-time = "2020-09-16T19:21:12.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "poethepoet" +version = "0.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pastel" }, + { name = "pyyaml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/14/d1f795f314c4bf3ad6d64216e370bdfda73093ed76e979485778b655a7ac/poethepoet-0.38.0.tar.gz", hash = "sha256:aeeb2f0a2cf0d3afa833976eff3ac7b8f5e472ae64171824900d79d3c68163c7", size = 77339, upload-time = "2025-11-23T13:51:28.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/89/2bf7d43ef4b0d60f446933ae9d3649f95c2c45c47b6736d121b602c28361/poethepoet-0.38.0-py3-none-any.whl", hash = "sha256:214bd9fcb348ff3dfd1466579d67e0c02242451a7044aced1a79641adef9cad0", size = 101938, upload-time = "2025-11-23T13:51:26.518Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "prek" +version = "0.2.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/d7/dda8a6b7819bdb9d1f54fa046911e80974d862bacba75a3539b43a9bb858/prek-0.2.28.tar.gz", hash = "sha256:ac54f58cad26e617a5c5459b705ff1cbaaa41640db03d8d35e39645aca1b82cf", size = 283945, upload-time = "2026-01-13T15:12:20.185Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/8c/2e18867e31d06dfa4974bf31c4e597dc1d2b3671b3c04d85f1f6a32ebc74/prek-0.2.28-py3-none-linux_armv6l.whl", hash = "sha256:1705c0bd035379cb5f1d03c19481821363d72d7923303fe8c84fd8cc7c6c3318", size = 4802811, upload-time = "2026-01-13T15:12:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/26/fa/6c6d0b0d8b2f21301da2bb3441f22232ed5a8cba1b63eeb18244d2192a2e/prek-0.2.28-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:67677c08767c278335b31ebcbf00c1c73e14eadd99a0d8537dfb43c54482673a", size = 4904156, upload-time = "2026-01-13T15:12:21.184Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5a/aa071ef1c2e6c3f58b50d9138676c96dd6de2323a44e1a3e56e18d25c382/prek-0.2.28-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ffac92215058ea6ba954a8e3f978dcd2a5e89922a318fcb7035fb9c0ab4de395", size = 4630803, upload-time = "2026-01-13T15:12:06.158Z" }, + { url = "https://files.pythonhosted.org/packages/77/dc/66498e805a0bb17820de0c3575d75b202c66045a9bfeeff9305d9bedd126/prek-0.2.28-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:413c3da1c9b252b3fd5113f4a04c2dead3c793b0ec56fc55294bb797ba7ca056", size = 4826037, upload-time = "2026-01-13T15:12:12.726Z" }, + { url = "https://files.pythonhosted.org/packages/27/ad/99cccc9283c7b34cd92356fcb301a2b1c25a8b65dc34b86c671b0f8e29d8/prek-0.2.28-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e99639bb68b70704e7b3f7f1a51cb23527c4dbd4b8a01dfccaa70f26f8f6c58b", size = 4723658, upload-time = "2026-01-13T15:12:01.538Z" }, + { url = "https://files.pythonhosted.org/packages/53/13/ce3edc2dda7b65485612e08ab038b8dd1ef7b10a94b0193f527b19a5e246/prek-0.2.28-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33c4a1b2a8a76581476ae327d6d4f1b0af6af42a90d436e21e45c72cb1081b81", size = 5044611, upload-time = "2026-01-13T15:12:26.331Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/6405d7ad7959d9b57d56fec9a1b4b2e00abeb955084dd45d100fb50a8377/prek-0.2.28-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6236dda18152fc56b9f802ce2914fbb2d19f9891595d552228c7894004b3332f", size = 5511371, upload-time = "2026-01-13T15:12:24.453Z" }, + { url = "https://files.pythonhosted.org/packages/92/cc/108c227fae40268ece36b80e5649037f1a816518e9b6d585d128b263df79/prek-0.2.28-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6506709d9a52ee431d48b249b8b5fb93597a9511eb260ee85d868750425057f", size = 5099352, upload-time = "2026-01-13T15:12:18.876Z" }, + { url = "https://files.pythonhosted.org/packages/12/d6/156ad3996d3a078a1bc2c0839b8681634216a494dcb298b8751beb28b327/prek-0.2.28-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:7cc920c12613440c105a767dc19acf048e8a05342ba38b48450d673bea33bd62", size = 4834340, upload-time = "2026-01-13T15:11:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/06/c632d4c4bb9c63d25bcc26149f99c715206a40e414fb6b80e7f800ae2e2d/prek-0.2.28-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:322a1922e6d2fcb2a4c487e01b2613856dc3206269bdc317ad28770704159e63", size = 4844870, upload-time = "2026-01-13T15:12:03.243Z" }, + { url = "https://files.pythonhosted.org/packages/ba/03/763f62d292399ee962e2583e7bc3fd2f8ee2609813c89cc10ec89a39204c/prek-0.2.28-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:a07baefe3562371368135eac3c8b9cdb831bac17b83cb1b6a8f5688050918e6c", size = 4709011, upload-time = "2026-01-13T15:12:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/e1/62/49397d1a5c2aaf5e7a8c0644be901ee97934a8a2cac0052652d01b7c6585/prek-0.2.28-py3-none-musllinux_1_1_i686.whl", hash = "sha256:17e95cab33067365028ffc1d4ab6c80c6c150f88e352d7c64bdc15e0570778f6", size = 4928435, upload-time = "2026-01-13T15:11:58.147Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e8/8ec73b5bb3fb9d5daf77f181cc46c541bd476075c7613f9b4c9c953925cc/prek-0.2.28-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:bc5272e2e8d81438a3cd2785c3b14b6e73dcb8e61b8a2b7ce6e693d57f7181ac", size = 5209880, upload-time = "2026-01-13T15:12:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/f3/52/a784a52bf72619bdfaafdbb6c964bda404b377213e7dc786f9ad6024501c/prek-0.2.28-py3-none-win32.whl", hash = "sha256:74657a1663191fb5f09f15873704c2d2640095ce56277d36860bbd08ba7eea94", size = 4622786, upload-time = "2026-01-13T15:12:22.967Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b8/ec6aafefeb05ef3a0bfcc044d036890f2b732b90ed1426acbf1e33289a44/prek-0.2.28-py3-none-win_amd64.whl", hash = "sha256:c350046d623362db65e2be1455ef1c5a96ea476e235445752fa946a705f1c1c9", size = 5316389, upload-time = "2026-01-13T15:12:17.501Z" }, + { url = "https://files.pythonhosted.org/packages/df/3f/9d4aba92cb9199cad0b056de8292a78dcca1dc1f6a6a34550799f19bde3d/prek-0.2.28-py3-none-win_arm64.whl", hash = "sha256:81db6ba7e5cf1d5ceec7d3b04e01aded32b8db8f1238ad812ac6ebc0bd35f141", size = 4974627, upload-time = "2026-01-13T15:11:56.333Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/2d/9f30cee56d4d6d222430d401e85b0a6a1ae229819362f5786943d1a8c03b/pymdown_extensions-10.19.1.tar.gz", hash = "sha256:4969c691009a389fb1f9712dd8e7bd70dcc418d15a0faf70acb5117d022f7de8", size = 847839, upload-time = "2025-12-14T17:25:24.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/35/b763e8fbcd51968329b9adc52d188fc97859f85f2ee15fe9f379987d99c5/pymdown_extensions-10.19.1-py3-none-any.whl", hash = "sha256:e8698a66055b1dc0dca2a7f2c9d0ea6f5faa7834a9c432e3535ab96c0c4e509b", size = 266693, upload-time = "2025-12-14T17:25:22.999Z" }, +] + +[[package]] +name = "pyproject-api" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/7b/c0e1333b61d41c69e59e5366e727b18c4992688caf0de1be10b3e5265f6b/pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330", size = 22785, upload-time = "2025-10-09T19:12:27.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/cc/cecf97be298bee2b2a37dd360618c819a2a7fd95251d8e480c1f0eb88f3b/pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09", size = 13218, upload-time = "2025-10-09T19:12:24.428Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "pytest-datadir" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/46/db060b291999ca048edd06d6fa9ee95945d088edc38b1172c59eeb46ec45/pytest_datadir-1.8.0.tar.gz", hash = "sha256:7a15faed76cebe87cc91941dd1920a9a38eba56a09c11e9ddf1434d28a0f78eb", size = 11848, upload-time = "2025-07-30T13:52:12.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/7a/33895863aec26ac3bb5068a73583f935680d6ab6af2a9567d409430c3ee1/pytest_datadir-1.8.0-py3-none-any.whl", hash = "sha256:5c677bc097d907ac71ca418109adc3abe34cf0bddfe6cf78aecfbabd96a15cf0", size = 6512, upload-time = "2025-07-30T13:52:11.525Z" }, +] + +[[package]] +name = "pytest-freezer" +version = "0.4.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "freezegun" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/f0/98dcbc5324064360b19850b14c84cea9ca50785d921741dbfc442346e925/pytest_freezer-0.4.9.tar.gz", hash = "sha256:21bf16bc9cc46bf98f94382c4b5c3c389be7056ff0be33029111ae11b3f1c82a", size = 3177, upload-time = "2024-12-12T08:53:08.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/e9/30252bc05bcf67200a17f4f0b4cc7598f0a68df4fa9fa356193aa899f145/pytest_freezer-0.4.9-py3-none-any.whl", hash = "sha256:8b6c50523b7d4aec4590b52bfa5ff766d772ce506e2bf4846c88041ea9ccae59", size = 3192, upload-time = "2024-12-12T08:53:07.641Z" }, +] + +[[package]] +name = "pytest-gitconfig" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/c7/39ad46d239bf49e6c1c8b4f6b99e75c8bee7bfc1e83cd8542d8701190508/pytest_gitconfig-0.9.0.tar.gz", hash = "sha256:5f9e8a29b4a8e55ddd740216ddb0a8a5e97f1c9c7f6bfdac91863473a8c60f9c", size = 10078, upload-time = "2025-12-28T01:52:17.821Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/78/460084e968adb270fa1b15d3b125d3d6da25514293e05c811820a19bfc04/pytest_gitconfig-0.9.0-py3-none-any.whl", hash = "sha256:3d855a9dd5fb3906010dbb1f8161366d4d86b34df8c14021fa012f23de3e5354", size = 7113, upload-time = "2025-12-28T01:52:16.395Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "pytest-regressions" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "pytest-datadir" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/63/cdb0ee15012a538fa07de21ec0a5c8eb113db9f28378f67b538d1c0b6d04/pytest_regressions-2.8.3.tar.gz", hash = "sha256:1ad90708bee02a3d36c78ef0b6f9692a9a30d312dd828680fd6d2a7235fcd221", size = 117168, upload-time = "2025-09-05T12:51:32.319Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/b9/7b2fe8407744cc37a74e29bed833256a305133505ea4979564911a98338b/pytest_regressions-2.8.3-py3-none-any.whl", hash = "sha256:72500dd95bde418c850f290a3108dacb56427067f364f7112cb5b16f6d6cc29c", size = 24894, upload-time = "2025-09-05T12:51:31.1Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "questionary" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, +] + +[[package]] +name = "requests" +version = "2.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "termcolor" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/56/ab275c2b56a5e2342568838f0d5e3e66a32354adcc159b495e374cda43f5/termcolor-3.2.0.tar.gz", hash = "sha256:610e6456feec42c4bcd28934a8c87a06c3fa28b01561d46aa09a9881b8622c58", size = 14423, upload-time = "2025-10-25T19:11:42.586Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/d5/141f53d7c1eb2a80e6d3e9a390228c3222c27705cbe7f048d3623053f3ca/termcolor-3.2.0-py3-none-any.whl", hash = "sha256:a10343879eba4da819353c55cb8049b0933890c2ebf9ad5d3ecd2bb32ea96ea6", size = 7698, upload-time = "2025-10-25T19:11:41.536Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, +] + +[[package]] +name = "tox" +version = "4.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "chardet" }, + { name = "colorama" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pluggy" }, + { name = "pyproject-api" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/bf/0e4dbd42724cbae25959f0e34c95d0c730df03ab03f54d52accd9abfc614/tox-4.32.0.tar.gz", hash = "sha256:1ad476b5f4d3679455b89a992849ffc3367560bbc7e9495ee8a3963542e7c8ff", size = 203330, upload-time = "2025-10-24T18:03:38.132Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/cc/e09c0d663a004945f82beecd4f147053567910479314e8d01ba71e5d5dea/tox-4.32.0-py3-none-any.whl", hash = "sha256:451e81dc02ba8d1ed20efd52ee409641ae4b5d5830e008af10fe8823ef1bd551", size = 175905, upload-time = "2025-10-24T18:03:36.337Z" }, +] + +[[package]] +name = "tox-uv" +version = "1.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tox" }, + { name = "uv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/90/06752775b8cfadba8856190f5beae9f552547e0f287e0246677972107375/tox_uv-1.29.0.tar.gz", hash = "sha256:30fa9e6ad507df49d3c6a2f88894256bcf90f18e240a00764da6ecab1db24895", size = 23427, upload-time = "2025-10-09T20:40:27.384Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/17/221d62937c4130b044bb437caac4181e7e13d5536bbede65264db1f0ac9f/tox_uv-1.29.0-py3-none-any.whl", hash = "sha256:b1d251286edeeb4bc4af1e24c8acfdd9404700143c2199ccdbb4ea195f7de6cc", size = 17254, upload-time = "2025-10-09T20:40:25.885Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "types-colorama" +version = "0.4.15.20250801" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/37/af713e7d73ca44738c68814cbacf7a655aa40ddd2e8513d431ba78ace7b3/types_colorama-0.4.15.20250801.tar.gz", hash = "sha256:02565d13d68963d12237d3f330f5ecd622a3179f7b5b14ee7f16146270c357f5", size = 10437, upload-time = "2025-08-01T03:48:22.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/3a/44ccbbfef6235aeea84c74041dc6dfee6c17ff3ddba782a0250e41687ec7/types_colorama-0.4.15.20250801-py3-none-any.whl", hash = "sha256:b6e89bd3b250fdad13a8b6a465c933f4a5afe485ea2e2f104d739be50b13eea9", size = 10743, upload-time = "2025-08-01T03:48:21.774Z" }, +] + +[[package]] +name = "types-deprecated" +version = "1.3.1.20251101" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/96/0f96107945697e452c56a2e9f575da627e61d67940e5913d9c968e5e8be1/types_deprecated-1.3.1.20251101.tar.gz", hash = "sha256:f002d266b73201f46ec6fc712c1f016067ec6cb44357559cdb50c86b010951a7", size = 8358, upload-time = "2025-11-01T03:04:05.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/50/fb191dba89031e30c9ea07c3133c6fe8e7da038ef4d0b79539cf85fb1a97/types_deprecated-1.3.1.20251101-py3-none-any.whl", hash = "sha256:274edcc2a084d3fe31802d3c1379abd630716d3db34e40577e12ad84d6b73134", size = 9057, upload-time = "2025-11-01T03:04:04.633Z" }, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20251115" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/36/06d01fb52c0d57e9ad0c237654990920fa41195e4b3d640830dabf9eeb2f/types_python_dateutil-2.9.0.20251115.tar.gz", hash = "sha256:8a47f2c3920f52a994056b8786309b43143faa5a64d4cbb2722d6addabdf1a58", size = 16363, upload-time = "2025-11-15T03:00:13.717Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/0b/56961d3ba517ed0df9b3a27bfda6514f3d01b28d499d1bce9068cfe4edd1/types_python_dateutil-2.9.0.20251115-py3-none-any.whl", hash = "sha256:9cf9c1c582019753b8639a081deefd7e044b9fa36bd8217f565c6c4e36ee0624", size = 18251, upload-time = "2025-11-15T03:00:12.317Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250915" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, +] + +[[package]] +name = "types-termcolor" +version = "1.1.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/77b1d73399d1cb77823ad32a36490b6a9851a7bd84f3a54560adab7ae022/types-termcolor-1.1.6.2.tar.gz", hash = "sha256:d8f0f69cf5552cc59ce75aa5172937cec9b320c17453adefe4168b93d16daad6", size = 2594, upload-time = "2023-03-28T01:19:18.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/94/0b01039dcb59173fc1f172a29b455986108f02fad9c5e103ff48afe17f35/types_termcolor-1.1.6.2-py3-none-any.whl", hash = "sha256:44c4c762c54a90d99b5c1033ef008aaa5610056d31d5c66b9288a942682a64d7", size = 2360, upload-time = "2023-03-28T01:19:17.328Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uv" +version = "0.9.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/20/c7598c4a8b0a7d19a1927287876d5fabb4ad5150103c9b751740bea33396/uv-0.9.20.tar.gz", hash = "sha256:a8b45804a84e5dfd01127abea663f7ca508551cd705e3476cc050751e5788b32", size = 3832559, upload-time = "2025-12-29T20:54:08.844Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/2e/ff66dbb7389f03097ff83e4d08c093f32bed96752df58b9992ec5e7aa0ea/uv-0.9.20-py3-none-linux_armv6l.whl", hash = "sha256:025ff1890f7ae2ea93a6c9ba326d25e56ba6d9e4f05c372b82ff5a947d21c400", size = 21294993, upload-time = "2025-12-29T20:54:23.877Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e0/5c14ac09dfebda68df962524cd191c5a4d7e0b02ac5503c34dce6788ffe6/uv-0.9.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:94f716f0e6e609d8c6804faa74797024c43e7aa2ea876778fa16ceb68dc4d80a", size = 20487595, upload-time = "2025-12-29T20:54:26.506Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/089a115a898a8729074fcc2bf00dfbf8a90ef79eadf738b531c89794528e/uv-0.9.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:288fdf29b22b3034285e244c006bcdf5575e35fe14f1f2dd55bbb477d6c22d99", size = 18972327, upload-time = "2025-12-29T20:54:31.292Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9f/3a880df832196a4113ed5b16322bd6d6cc21b80338406f2bdab7cbc31361/uv-0.9.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:44076e26038fe37bd6aad7aaf411b72006ec01f124af3c1227d47a3fafa11b1b", size = 20825991, upload-time = "2025-12-29T20:54:21.359Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c1/5b4bdc9b4637f061955200eda21d2285701b8dd979d745a9f5ac2f789abb/uv-0.9.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b66e83fa1558b322194a297e9477e7e49f31136f95b88830a971ba23d8778fb5", size = 20885060, upload-time = "2025-12-29T20:54:10.973Z" }, + { url = "https://files.pythonhosted.org/packages/4d/11/9c44cea9b30dda6a9268d3496aae0c83afa6096afa83559de33181877ab8/uv-0.9.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ae848f6760fce321a2be6cc98b37a31353178295c9e0ebb64e70be8a3c73b07", size = 21992898, upload-time = "2025-12-29T20:54:49.959Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/6d5eaefec10ef5aceeb0957ebd49afede80c446354ecb463cc8f457144da/uv-0.9.20-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:47cda4b2dcdfb55fb4d77fe3ef138d30e89d69e138e96c30325509462e4c4386", size = 23540946, upload-time = "2025-12-29T20:54:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/df/87/a8ce66e5ee35351a2c50f9cf125407e582e1d790e78cad79b614944f3f4a/uv-0.9.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7551cd1a3a1516583021d43b2daef41ce36c2249aa19de10c0f108264099388", size = 23156794, upload-time = "2025-12-29T20:54:03.745Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/ca9653caba6123f83842ac81de2411316570fda3b7a5916c2b1ef858ab5a/uv-0.9.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143783954e48abaea83d100eb7ca70a614c089d5933b55d59a5638d630b3f7c3", size = 22225527, upload-time = "2025-12-29T20:54:06.663Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a4/f88d4e6806894a37b4018edfd196cd3689194e6ecbcee05fd7ae5ed4cf6f/uv-0.9.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d86724ae303c80d52712b335cfc9f1b5aa785244c18905fd605067303a305854", size = 22205705, upload-time = "2025-12-29T20:54:44.766Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bc/202cfb8d47f83b633502b0b32e00abc019f91255e54aa6672a87495ef06c/uv-0.9.20-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:19e4ce680230f9148c777af4664a6a534821222dd4e8f06844c3d23b79e4778b", size = 20943437, upload-time = "2025-12-29T20:54:34.237Z" }, + { url = "https://files.pythonhosted.org/packages/a3/15/de8ecbf504c96cfb5af7fdf621e5497f0496a403d0883c31ee97702b97db/uv-0.9.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:02383c09eadd2518afb0eebbc651d74b2dd59ad5818d28037cbf7bcef76f8b56", size = 22071434, upload-time = "2025-12-29T20:54:47.395Z" }, + { url = "https://files.pythonhosted.org/packages/30/32/283604c72142e4550f04252d53ab591ed71a178ae044b3debbea1700018c/uv-0.9.20-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:277a7fc7e5229a809e6cc4dacc936dea1fa7260fb5802895ddbba0873fc4b6ec", size = 20838373, upload-time = "2025-12-29T20:54:28.975Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d4/c56a14ae2a693260d227e60358f09f924b7e347bed4aef7a85f4d52e06f5/uv-0.9.20-py3-none-musllinux_1_1_i686.whl", hash = "sha256:c9a073116da198b0533e80828d765bbb30137eef37de68cbc3d21b0f614c843d", size = 21399009, upload-time = "2025-12-29T20:54:13.458Z" }, + { url = "https://files.pythonhosted.org/packages/99/35/e717a53df2d579fd57e9236ef3e13d9127c34e006c6ea1bb4294164d04df/uv-0.9.20-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:56bb00bc0d13f9e4913ef773f31659423faaffe408f6c24b24e25f2c1d5a93cf", size = 22426264, upload-time = "2025-12-29T20:54:37.053Z" }, + { url = "https://files.pythonhosted.org/packages/d4/18/563fc6703e882167b39270db83a57e36834b0d75a51c97a9e5e30a5b2a47/uv-0.9.20-py3-none-win32.whl", hash = "sha256:e620adcbd2e6d714e26928c1674af4581ba9e4b74c38745d6987b4d323d80edc", size = 20034131, upload-time = "2025-12-29T20:54:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/c8/32/3359f7afeebadcaa472535f72ca48dc2d7f234de8570039ee556b470734e/uv-0.9.20-py3-none-win_amd64.whl", hash = "sha256:42d43b6cb7d84a3d4fce8a8a46f0b2c6c8b5d58b3b8062980cbfdb65b559fa8f", size = 22203935, upload-time = "2025-12-29T20:54:18.997Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c4/65fd3981884a51971135547ebb0dddc5c32161374f90e162aca227f4832f/uv-0.9.20-py3-none-win_arm64.whl", hash = "sha256:d63f501a95ef74ea6d2004665eb2ae65429d039150f2c2e6bc12efb6e03fd702", size = 20578093, upload-time = "2025-12-29T20:54:41.944Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.36.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[package]] +name = "wrapt" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/0d/12d8c803ed2ce4e5e7d5b9f5f602721f9dfef82c95959f3ce97fa584bb5c/wrapt-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:64b103acdaa53b7caf409e8d45d39a8442fe6dcfec6ba3f3d141e0cc2b5b4dbd", size = 77481, upload-time = "2025-11-07T00:43:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/3e/4364ebe221ebf2a44d9fc8695a19324692f7dd2795e64bd59090856ebf12/wrapt-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:91bcc576260a274b169c3098e9a3519fb01f2989f6d3d386ef9cbf8653de1374", size = 60692, upload-time = "2025-11-07T00:43:13.697Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ff/ae2a210022b521f86a8ddcdd6058d137c051003812b0388a5e9a03d3fe10/wrapt-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab594f346517010050126fcd822697b25a7031d815bb4fbc238ccbe568216489", size = 61574, upload-time = "2025-11-07T00:43:14.967Z" }, + { url = "https://files.pythonhosted.org/packages/c6/93/5cf92edd99617095592af919cb81d4bff61c5dbbb70d3c92099425a8ec34/wrapt-2.0.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:36982b26f190f4d737f04a492a68accbfc6fa042c3f42326fdfbb6c5b7a20a31", size = 113688, upload-time = "2025-11-07T00:43:18.275Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0a/e38fc0cee1f146c9fb266d8ef96ca39fb14a9eef165383004019aa53f88a/wrapt-2.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23097ed8bc4c93b7bf36fa2113c6c733c976316ce0ee2c816f64ca06102034ef", size = 115698, upload-time = "2025-11-07T00:43:19.407Z" }, + { url = "https://files.pythonhosted.org/packages/b0/85/bef44ea018b3925fb0bcbe9112715f665e4d5309bd945191da814c314fd1/wrapt-2.0.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bacfe6e001749a3b64db47bcf0341da757c95959f592823a93931a422395013", size = 112096, upload-time = "2025-11-07T00:43:16.5Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0b/733a2376e413117e497aa1a5b1b78e8f3a28c0e9537d26569f67d724c7c5/wrapt-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8ec3303e8a81932171f455f792f8df500fc1a09f20069e5c16bd7049ab4e8e38", size = 114878, upload-time = "2025-11-07T00:43:20.81Z" }, + { url = "https://files.pythonhosted.org/packages/da/03/d81dcb21bbf678fcda656495792b059f9d56677d119ca022169a12542bd0/wrapt-2.0.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3f373a4ab5dbc528a94334f9fe444395b23c2f5332adab9ff4ea82f5a9e33bc1", size = 111298, upload-time = "2025-11-07T00:43:22.229Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d5/5e623040e8056e1108b787020d56b9be93dbbf083bf2324d42cde80f3a19/wrapt-2.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f49027b0b9503bf6c8cdc297ca55006b80c2f5dd36cecc72c6835ab6e10e8a25", size = 113361, upload-time = "2025-11-07T00:43:24.301Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f3/de535ccecede6960e28c7b722e5744846258111d6c9f071aa7578ea37ad3/wrapt-2.0.1-cp310-cp310-win32.whl", hash = "sha256:8330b42d769965e96e01fa14034b28a2a7600fbf7e8f0cc90ebb36d492c993e4", size = 58035, upload-time = "2025-11-07T00:43:28.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/15/39d3ca5428a70032c2ec8b1f1c9d24c32e497e7ed81aed887a4998905fcc/wrapt-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:1218573502a8235bb8a7ecaed12736213b22dcde9feab115fa2989d42b5ded45", size = 60383, upload-time = "2025-11-07T00:43:25.804Z" }, + { url = "https://files.pythonhosted.org/packages/43/c2/dfd23754b7f7a4dce07e08f4309c4e10a40046a83e9ae1800f2e6b18d7c1/wrapt-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:eda8e4ecd662d48c28bb86be9e837c13e45c58b8300e43ba3c9b4fa9900302f7", size = 58894, upload-time = "2025-11-07T00:43:27.074Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/553997acf3939079dab022e37b67b1904b5b0cc235503226898ba573b10c/wrapt-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e17283f533a0d24d6e5429a7d11f250a58d28b4ae5186f8f47853e3e70d2590", size = 77480, upload-time = "2025-11-07T00:43:30.573Z" }, + { url = "https://files.pythonhosted.org/packages/2d/50/e5b3d30895d77c52105c6d5cbf94d5b38e2a3dd4a53d22d246670da98f7c/wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85df8d92158cb8f3965aecc27cf821461bb5f40b450b03facc5d9f0d4d6ddec6", size = 60690, upload-time = "2025-11-07T00:43:31.594Z" }, + { url = "https://files.pythonhosted.org/packages/f0/40/660b2898703e5cbbb43db10cdefcc294274458c3ca4c68637c2b99371507/wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1be685ac7700c966b8610ccc63c3187a72e33cab53526a27b2a285a662cd4f7", size = 61578, upload-time = "2025-11-07T00:43:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/5b/36/825b44c8a10556957bc0c1d84c7b29a40e05fcf1873b6c40aa9dbe0bd972/wrapt-2.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df0b6d3b95932809c5b3fecc18fda0f1e07452d05e2662a0b35548985f256e28", size = 114115, upload-time = "2025-11-07T00:43:35.605Z" }, + { url = "https://files.pythonhosted.org/packages/83/73/0a5d14bb1599677304d3c613a55457d34c344e9b60eda8a737c2ead7619e/wrapt-2.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da7384b0e5d4cae05c97cd6f94faaf78cc8b0f791fc63af43436d98c4ab37bb", size = 116157, upload-time = "2025-11-07T00:43:37.058Z" }, + { url = "https://files.pythonhosted.org/packages/01/22/1c158fe763dbf0a119f985d945711d288994fe5514c0646ebe0eb18b016d/wrapt-2.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ec65a78fbd9d6f083a15d7613b2800d5663dbb6bb96003899c834beaa68b242c", size = 112535, upload-time = "2025-11-07T00:43:34.138Z" }, + { url = "https://files.pythonhosted.org/packages/5c/28/4f16861af67d6de4eae9927799b559c20ebdd4fe432e89ea7fe6fcd9d709/wrapt-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7de3cc939be0e1174969f943f3b44e0d79b6f9a82198133a5b7fc6cc92882f16", size = 115404, upload-time = "2025-11-07T00:43:39.214Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8b/7960122e625fad908f189b59c4aae2d50916eb4098b0fb2819c5a177414f/wrapt-2.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fb1a5b72cbd751813adc02ef01ada0b0d05d3dcbc32976ce189a1279d80ad4a2", size = 111802, upload-time = "2025-11-07T00:43:40.476Z" }, + { url = "https://files.pythonhosted.org/packages/3e/73/7881eee5ac31132a713ab19a22c9e5f1f7365c8b1df50abba5d45b781312/wrapt-2.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3fa272ca34332581e00bf7773e993d4f632594eb2d1b0b162a9038df0fd971dd", size = 113837, upload-time = "2025-11-07T00:43:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/45/00/9499a3d14e636d1f7089339f96c4409bbc7544d0889f12264efa25502ae8/wrapt-2.0.1-cp311-cp311-win32.whl", hash = "sha256:fc007fdf480c77301ab1afdbb6ab22a5deee8885f3b1ed7afcb7e5e84a0e27be", size = 58028, upload-time = "2025-11-07T00:43:47.369Z" }, + { url = "https://files.pythonhosted.org/packages/70/5d/8f3d7eea52f22638748f74b102e38fdf88cb57d08ddeb7827c476a20b01b/wrapt-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:47434236c396d04875180171ee1f3815ca1eada05e24a1ee99546320d54d1d1b", size = 60385, upload-time = "2025-11-07T00:43:44.34Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/32195e57a8209003587bbbad44d5922f13e0ced2a493bb46ca882c5b123d/wrapt-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:837e31620e06b16030b1d126ed78e9383815cbac914693f54926d816d35d8edf", size = 58893, upload-time = "2025-11-07T00:43:46.161Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" }, + { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" }, + { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" }, + { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" }, + { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" }, + { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" }, + { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" }, + { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" }, + { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" }, + { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" }, + { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" }, + { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" }, + { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/73/81/d08d83c102709258e7730d3cd25befd114c60e43ef3891d7e6877971c514/wrapt-2.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1", size = 78290, upload-time = "2025-11-07T00:44:34.691Z" }, + { url = "https://files.pythonhosted.org/packages/f6/14/393afba2abb65677f313aa680ff0981e829626fed39b6a7e3ec807487790/wrapt-2.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55", size = 61255, upload-time = "2025-11-07T00:44:35.762Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/a4a1f2fba205a9462e36e708ba37e5ac95f4987a0f1f8fd23f0bf1fc3b0f/wrapt-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0", size = 61797, upload-time = "2025-11-07T00:44:37.22Z" }, + { url = "https://files.pythonhosted.org/packages/12/db/99ba5c37cf1c4fad35349174f1e38bd8d992340afc1ff27f526729b98986/wrapt-2.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509", size = 120470, upload-time = "2025-11-07T00:44:39.425Z" }, + { url = "https://files.pythonhosted.org/packages/30/3f/a1c8d2411eb826d695fc3395a431757331582907a0ec59afce8fe8712473/wrapt-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1", size = 122851, upload-time = "2025-11-07T00:44:40.582Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8d/72c74a63f201768d6a04a8845c7976f86be6f5ff4d74996c272cefc8dafc/wrapt-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970", size = 117433, upload-time = "2025-11-07T00:44:38.313Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5a/df37cf4042cb13b08256f8e27023e2f9b3d471d553376616591bb99bcb31/wrapt-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c", size = 121280, upload-time = "2025-11-07T00:44:41.69Z" }, + { url = "https://files.pythonhosted.org/packages/54/34/40d6bc89349f9931e1186ceb3e5fbd61d307fef814f09fbbac98ada6a0c8/wrapt-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41", size = 116343, upload-time = "2025-11-07T00:44:43.013Z" }, + { url = "https://files.pythonhosted.org/packages/70/66/81c3461adece09d20781dee17c2366fdf0cb8754738b521d221ca056d596/wrapt-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed", size = 119650, upload-time = "2025-11-07T00:44:44.523Z" }, + { url = "https://files.pythonhosted.org/packages/46/3a/d0146db8be8761a9e388cc9cc1c312b36d583950ec91696f19bbbb44af5a/wrapt-2.0.1-cp314-cp314-win32.whl", hash = "sha256:bfb5539005259f8127ea9c885bdc231978c06b7a980e63a8a61c8c4c979719d0", size = 58701, upload-time = "2025-11-07T00:44:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/1a/38/5359da9af7d64554be63e9046164bd4d8ff289a2dd365677d25ba3342c08/wrapt-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:4ae879acc449caa9ed43fc36ba08392b9412ee67941748d31d94e3cedb36628c", size = 60947, upload-time = "2025-11-07T00:44:46.086Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3f/96db0619276a833842bf36343685fa04f987dd6e3037f314531a1e00492b/wrapt-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:8639b843c9efd84675f1e100ed9e99538ebea7297b62c4b45a7042edb84db03e", size = 59359, upload-time = "2025-11-07T00:44:47.164Z" }, + { url = "https://files.pythonhosted.org/packages/71/49/5f5d1e867bf2064bf3933bc6cf36ade23505f3902390e175e392173d36a2/wrapt-2.0.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b", size = 82031, upload-time = "2025-11-07T00:44:49.4Z" }, + { url = "https://files.pythonhosted.org/packages/2b/89/0009a218d88db66ceb83921e5685e820e2c61b59bbbb1324ba65342668bc/wrapt-2.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec", size = 62952, upload-time = "2025-11-07T00:44:50.74Z" }, + { url = "https://files.pythonhosted.org/packages/ae/18/9b968e920dd05d6e44bcc918a046d02afea0fb31b2f1c80ee4020f377cbe/wrapt-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa", size = 63688, upload-time = "2025-11-07T00:44:52.248Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7d/78bdcb75826725885d9ea26c49a03071b10c4c92da93edda612910f150e4/wrapt-2.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815", size = 152706, upload-time = "2025-11-07T00:44:54.613Z" }, + { url = "https://files.pythonhosted.org/packages/dd/77/cac1d46f47d32084a703df0d2d29d47e7eb2a7d19fa5cbca0e529ef57659/wrapt-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa", size = 158866, upload-time = "2025-11-07T00:44:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/8a/11/b521406daa2421508903bf8d5e8b929216ec2af04839db31c0a2c525eee0/wrapt-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef", size = 146148, upload-time = "2025-11-07T00:44:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c0/340b272bed297baa7c9ce0c98ef7017d9c035a17a6a71dce3184b8382da2/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747", size = 155737, upload-time = "2025-11-07T00:44:56.971Z" }, + { url = "https://files.pythonhosted.org/packages/f3/93/bfcb1fb2bdf186e9c2883a4d1ab45ab099c79cbf8f4e70ea453811fa3ea7/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f", size = 144451, upload-time = "2025-11-07T00:44:58.515Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/dca504fb18d971139d232652656180e3bd57120e1193d9a5899c3c0b7cdd/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349", size = 150353, upload-time = "2025-11-07T00:44:59.753Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f6/a1de4bd3653afdf91d250ca5c721ee51195df2b61a4603d4b373aa804d1d/wrapt-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:be9e84e91d6497ba62594158d3d31ec0486c60055c49179edc51ee43d095f79c", size = 60609, upload-time = "2025-11-07T00:45:03.315Z" }, + { url = "https://files.pythonhosted.org/packages/01/3a/07cd60a9d26fe73efead61c7830af975dfdba8537632d410462672e4432b/wrapt-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61c4956171c7434634401db448371277d07032a81cc21c599c22953374781395", size = 64038, upload-time = "2025-11-07T00:45:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/99/8a06b8e17dddbf321325ae4eb12465804120f699cd1b8a355718300c62da/wrapt-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad", size = 60634, upload-time = "2025-11-07T00:45:02.087Z" }, + { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, +]