Skip to content

feat: add cz dump-config command to output effective configuration - #1986

Closed
bearomorphism wants to merge 1 commit into
commitizen-tools:masterfrom
bearomorphism:feat/1331-dump-config-command
Closed

feat: add cz dump-config command to output effective configuration#1986
bearomorphism wants to merge 1 commit into
commitizen-tools:masterfrom
bearomorphism:feat/1331-dump-config-command

Conversation

@bearomorphism

Copy link
Copy Markdown
Collaborator

Description

Adds a new cz dump-config command that prints the effective Commitizen configuration (defaults merged with any project overrides) in TOML, YAML, or JSON format. Users can paste the output directly into a configuration file as a starting template and tweak from there — which is exactly the use-case raised in #1331.

Why

Users working with monorepos or custom commit conventions (like the reporter in #1331) struggle because overriding a single setting (e.g. customize.questions) replaces the entire default. There was no way to see the full effective config without reading source code. cz dump-config solves this by exposing the merged settings.

What changed

Area Change
commitizen/commands/dump_config.py New DumpConfig command class
commitizen/commands/__init__.py Export DumpConfig
commitizen/cli.py Register dump-config subparser with --format argument
tests/commands/test_dump_config_command.py New tests for all three formats
tests/commands/test_common_command.py Add dump-config to help-snapshot parametrize list
tests/commands/test_common_command/ Help snapshot files for py 3.10–3.14
docs/commands/dump_config.md Command documentation
mkdocs.yml Add dump-config to nav

How it works

  1. DumpConfig.__call__() reads config.settings (a Settings TypedDict containing defaults merged with user config).
  2. For TOML (default): None values are filtered out (TOML cannot represent null), then the dict is wrapped in a [tool.commitizen] section and serialised with tomlkit.dumps().
  3. For YAML: the dict (with None values preserved as null) is wrapped in tool.commitizen and serialised with yaml.safe_dump().
  4. For JSON: same wrapping, serialised with json.dumps(indent=2).

All three formats mirror the input structure so the output can be pasted directly into pyproject.toml, .cz.yaml, or .cz.json.

Backward compatibility

Purely additive — new subcommand only. No existing commands, APIs, or config keys changed.

Checklist

Was generative AI tooling used to co-author this PR?

  • Yes (please specify the tool below)

Generated-by: GitHub Copilot CLI following the guidelines

Code Changes

  • Add test cases to all the changes you introduce
  • 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

Documentation Changes

  • Added docs/commands/dump_config.md
  • Updated mkdocs.yml nav

Expected Behavior

Command Expected output
cz dump-config TOML block under [tool.commitizen] with effective settings (no null values)
cz dump-config --format yaml YAML block under tool.commitizen with effective settings
cz dump-config --format json JSON object {"tool": {"commitizen": {...}}}

Steps to Test This Pull Request

# 1. Install the branch
git checkout feat/1331-dump-config-command

# 2. Dump default config as TOML
cz dump-config
# Expected: [tool.commitizen] block with name = "cz_conventional_commits" etc.

# 3. Dump as YAML
cz dump-config --format yaml
# Expected: tool:\n  commitizen:\n    name: cz_conventional_commits ...

# 4. Dump as JSON
cz dump-config --format json
# Expected: {"tool": {"commitizen": {"name": "cz_conventional_commits", ...}}}

# 5. Verify no null values in TOML output
cz dump-config | grep -i null
# Expected: no output (null values are filtered)

# 6. Round-trip test: dump → paste → compare
cz dump-config > /tmp/dumped.toml
# Verify the file is valid TOML: python -c "import tomlkit; tomlkit.loads(open('/tmp/dumped.toml').read())"

Additional Context

Closes #1331.

The maintainer suggested this could be layered on top of cz init (#1331 comment), but a standalone cz dump-config is cleaner and more composable (pipe to a file, diff against a known config, etc.).

Closes commitizen-tools#1331

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codecov

codecov Bot commented May 9, 2026

Copy link
Copy Markdown

❌ 4 Tests Failed:

Tests completed Failed Passed Skipped
1286 4 1282 2
View the top 3 failed test(s) by shortest run time
tests/commands/test_common_command.py::test_command_shows_description_when_use_help_option[py_3.10-dump-config]
Stack Traces | 0.018s run time
capsys = <_pytest.capture.CaptureFixture object at 0x7f539db30340>
file_regression = <pytest_regressions.file_regression.FileRegressionFixture object at 0x7f539db31330>
command = 'dump-config'
util = UtilFixture(mocker=<pytest_mock.plugin.MockerFixture object at 0x7f539db30d60>, monkeypatch=<_pytest.monkeypatch.MonkeyPatch object at 0x7f539db31480>, freezer=<freezegun.api.FrozenDateTimeFactory object at 0x7f539d8c89a0>)

    @pytest.mark.parametrize(
        "command",
        [
            "bump",
            "changelog",
            "check",
            "commit",
            "dump-config",
            "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")
E       AssertionError: FILES DIFFER:
E       .../popen-gw3/test_command_shows_description4/test_command_shows_description_when_use_help_option_py_3_10_dump_config_.txt
E       .../popen-gw3/test_command_shows_description4/test_command_shows_description_when_use_help_option_py_3_10_dump_config_.obtained.txt
E       HTML DIFF: .../popen-gw3/test_command_shows_description4/test_command_shows_description_when_use_help_option_py_3_10_dump_config_.obtained.diff.html
E       --- 
E       +++ 
E       @@ -4,5 +4,5 @@
E        
E        options:
E          -h, --help            show this help message and exit
E       -  --format, -f {toml,yaml,json}
E       +  --format {toml,yaml,json}, -f {toml,yaml,json}
E                                Output format (default: toml).

tests/commands/test_common_command.py:40: AssertionError
tests/test_cli.py::test_invalid_command[py_3.10---invalid-arg]
Stack Traces | 0.027s run time
util = UtilFixture(mocker=<pytest_mock.plugin.MockerFixture object at 0x7f539d955a20>, monkeypatch=<_pytest.monkeypatch.MonkeyPatch object at 0x7f539de38d30>, freezer=<freezegun.api.FrozenDateTimeFactory object at 0x7f539d9570d0>)
capsys = <_pytest.capture.CaptureFixture object at 0x7f539d9547f0>
file_regression = <pytest_regressions.file_regression.FileRegressionFixture object at 0x7f539d955990>
arg = '--invalid-arg'

    @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")
E       AssertionError: FILES DIFFER:
E       .../popen-gw3/test_invalid_command_py_3_10__0/test_invalid_command_py_3_10___invalid_arg_.txt
E       .../popen-gw3/test_invalid_command_py_3_10__0/test_invalid_command_py_3_10___invalid_arg_.obtained.txt
E       HTML DIFF: .../popen-gw3/test_invalid_command_py_3_10__0/test_invalid_command_py_3_10___invalid_arg_.obtained.diff.html
E       --- 
E       +++ 
E       @@ -1,4 +1,4 @@
E        usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE]
E       -          {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}
E       +          {init,commit,c,ls,example,info,schema,dump-config,bump,changelog,ch,check,version}
E                  ...
E       -cz: error: the following arguments are required: {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}
E       +cz: error: the following arguments are required: {init,commit,c,ls,example,info,schema,dump-config,bump,changelog,ch,check,version}

tests/test_cli.py:43: AssertionError
tests/test_cli.py::test_invalid_command[py_3.10-invalidCommand]
Stack Traces | 0.027s run time
util = UtilFixture(mocker=<pytest_mock.plugin.MockerFixture object at 0x7f539db59930>, monkeypatch=<_pytest.monkeypatch.MonkeyPatch object at 0x7f539db59990>, freezer=<freezegun.api.FrozenDateTimeFactory object at 0x7f539db5aa40>)
capsys = <_pytest.capture.CaptureFixture object at 0x7f539db5b6a0>
file_regression = <pytest_regressions.file_regression.FileRegressionFixture object at 0x7f539db5b640>
arg = 'invalidCommand'

    @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")
E       AssertionError: FILES DIFFER:
E       .../popen-gw3/test_invalid_command_py_3_10_i0/test_invalid_command_py_3_10_invalidCommand_.txt
E       .../popen-gw3/test_invalid_command_py_3_10_i0/test_invalid_command_py_3_10_invalidCommand_.obtained.txt
E       HTML DIFF: .../popen-gw3/test_invalid_command_py_3_10_i0/test_invalid_command_py_3_10_invalidCommand_.obtained.diff.html
E       --- 
E       +++ 
E       @@ -1,4 +1,4 @@
E        usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE]
E       -          {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}
E       +          {init,commit,c,ls,example,info,schema,dump-config,bump,changelog,ch,check,version}
E                  ...
E       -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')
E       +cz: error: argument {init,commit,c,ls,example,info,schema,dump-config,bump,changelog,ch,check,version}: invalid choice: 'invalidCommand' (choose from 'init', 'commit', 'c', 'ls', 'example', 'info', 'schema', 'dump-config', 'bump', 'changelog', 'ch', 'check', 'version')

tests/test_cli.py:43: AssertionError
tests/test_cli.py::test_no_argv[py_3.10]
Stack Traces | 0.036s run time
util = UtilFixture(mocker=<pytest_mock.plugin.MockerFixture object at 0x7f539de39630>, monkeypatch=<_pytest.monkeypatch.MonkeyPatch object at 0x7f539de397e0>, freezer=<freezegun.api.FrozenDateTimeFactory object at 0x7f539de38160>)
capsys = <_pytest.capture.CaptureFixture object at 0x7f539de393c0>
file_regression = <pytest_regressions.file_regression.FileRegressionFixture object at 0x7f539de39810>

    @pytest.mark.usefixtures("python_version", "consistent_terminal_output")
    def test_no_argv(util: UtilFixture, capsys, file_regression):
        with pytest.raises(ExpectedExit):
            util.run_cli()
        out, err = capsys.readouterr()
        assert out == ""
>       file_regression.check(err, extension=".txt")
E       AssertionError: FILES DIFFER:
E       .../popen-gw3/test_no_argv_py_3_10_0/test_no_argv_py_3_10_.txt
E       .../popen-gw3/test_no_argv_py_3_10_0/test_no_argv_py_3_10_.obtained.txt
E       HTML DIFF: .../popen-gw3/test_no_argv_py_3_10_0/test_no_argv_py_3_10_.obtained.diff.html
E       --- 
E       +++ 
E       @@ -1,5 +1,5 @@
E        usage: cz [-h] [--config CONFIG] [--debug] [-n NAME] [-nr NO_RAISE]
E       -          {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}
E       +          {init,commit,c,ls,example,info,schema,dump-config,bump,changelog,ch,check,version}
E                  ...
E        
E        Commitizen is a powerful release management tool that helps teams maintain consistent and meaningful commit messages while automating version management.
E       @@ -18,13 +18,14 @@
E                                tools.github.io/commitizen/exit_codes/
E        
E        commands:
E       -  {init,commit,c,ls,example,info,schema,bump,changelog,ch,check,version}
E       +  {init,commit,c,ls,example,info,schema,dump-config,bump,changelog,ch,check,version}
E            init                Initialize commitizen configuration.
E            commit (c)          Create new commit.
E            ls                  Show available Commitizens.
E            example             Show commit example.
E            info                Show information about the cz.
E            schema              Show commit schema.
E       +    dump-config         Output the current commitizen configuration.
E            bump                Bump semantic version based on the git log.
E            changelog (ch)      Generate changelog (note that it will overwrite
E                                existing files).

tests/test_cli.py:27: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@bearomorphism

Copy link
Copy Markdown
Collaborator Author

Closing this PR per maintainer-triage policy: feature-request issues should sit with the maintainers for design / scope review before any implementation lands. The issue's type: feature (or implicit equivalent) label means it's not on a "ready-to-implement" track yet.

The implementation itself is preserved on the branch (feat/1331-dump-config-command) — if a maintainer decides this is the direction they want, the PR can be re-opened in one click, or the work can serve as a starting point for a maintainer-led design.

This PR is being closed so that #1331 reverts to "awaiting maintainer triage / decision" rather than "PR pending review", which is the correct state for a feature request.

Closed via the round-2 triage cleanup in #1965.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1 participant