Repository: callowayproject/bump-my-version (language: python)

Issue:
Please post configuration to implement the semver tutorial
* bump-my-version version: 0.12.0
* Python version: 3.11.5
* Operating System: macOS 13.6.2, debian 12.2 (latest)

### Description

I am not finding sample configuration to support the workflow described at https://callowayproject.github.io/bump-my-version/tutorials/semantic-versioning-example.html, and I've been unable to recreate it. I'll elaborate in another case because my goals differ slightly from what's described there, but seeing how to do what's described there would have been helpful.


Discussion / hints:
This issue is a bit of a blocker for new potential users (like me :smile:)

Is this the relevant config (found in unit tests)?
https://github.com/callowayproject/bump-my-version/blob/master/tests/fixtures/pep440.toml
While overstuffed, this config you've found is a working model of semver pre_labels, once you uncomment line 47.

I didn't end up creating the other ticket I alluded to, because I found a workaround, but this example config highlights my main question about the would-be semver tutorial, which is how in step 5 we can go from `1.0.1-rc2` to `1.0.1`, in a single command which doesn't include any explicit state (such as the undesirable `bump-my-version bump patch --new-version 1.0.1`). 

With this config, the tutorial's syntax of `bump-my-version release` returns `Unknown version part: release`. And running `bump pre_label` when the current version is already an rc returns a stack trace ending with the complaint, `ValueError: The part has already the maximum value among ['final', 'a', 'b', 'rc'] and cannot be bumped.` So one can either `bump patch` to a later version than the release candidate, or specify a version explicitly; I'm not finding a third option.

Fix patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 34665d21..806c017f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -19,7 +19,6 @@ All types of contributions are encouraged and valued. See the [Table of Contents
 - [Your First Code Contribution](#your-first-code-contribution)
 - [Improving The Documentation](#improving-the-documentation)
 - [Styleguides](#styleguides)
-- [Commit Messages](#commit-messages)
 - [Join The Project Team](#join-the-project-team)
 
 
diff --git a/Makefile b/Makefile
index d7d0092b..15eba793 100644
--- a/Makefile
+++ b/Makefile
@@ -27,7 +27,6 @@ release-version: get-version do-release  ## Release a specific version: release-
 
 docs: ## generate Sphinx HTML documentation, including API docs
 	mkdir -p docs
-	rm -rf docsrc/_autosummary
 	ls -A1 docs | xargs -I {} rm -rf docs/{}
 	$(MAKE) -C docsrc clean html
 	cp -a docsrc/_build/html/. docs
diff --git a/README.md b/README.md
index 93ce67c0..aa6b2ae0 100644
--- a/README.md
+++ b/README.md
@@ -129,9 +129,9 @@ Example output:
 `-h, --help`
 Print help and exit
 
-## Using bumpversion in a script
+## Using bump-my-version in a script
 
-If you need to use the version generated by bumpversion in a script, you can make use of the `show` subcommand.
+If you need to use the version generated by bump-my-version in a script, you can make use of the `show` subcommand.
 
 Say, for example, that you are using git-flow to manage your project and want to automatically create a release. When you issue `git flow release start` you need to know the new version before applying the change.
 
diff --git a/bumpversion/cli.py b/bumpversion/cli.py
index bcb08d26..c3a06038 100644
--- a/bumpversion/cli.py
+++ b/bumpversion/cli.py
@@ -1,18 +1,23 @@
 """bump-my-version Command line interface."""
+from pathlib import Path
 from typing import List, Optional
 
+import questionary
 import rich_click as click
 from click.core import Context
+from tomlkit import dumps
 
 from bumpversion import __version__
 from bumpversion.aliases import AliasedGroup
 from bumpversion.bump import do_bump
 from bumpversion.config import get_configuration
+from bumpversion.config.create import create_configuration
 from bumpversion.config.files import find_config_file
 from bumpversion.files import ConfiguredFile, modify_files
 from bumpversion.show import do_show, log_list
-from bumpversion.ui import get_indented_logger, print_warning, setup_logging
+from bumpversion.ui import get_indented_logger, print_info, print_warning, setup_logging
 from bumpversion.utils import get_context, get_overrides
+from bumpversion.visualize import visualize
 
 logger = get_indented_logger(__name__)
 
@@ -516,3 +521,51 @@ def replace(
     ctx = get_context(config, version, next_version)
 
     modify_files(configured_files, version, next_version, ctx, dry_run)
+
+
+@cli.command()
+@click.option(
+    "--prompt/--no-prompt",
+    default=True,
+    help="Ask the user questions about the configuration.",
+)
+@click.option(
+    "--destination",
+    default="stdout",
+    help="Where to write the sample configuration.",
+    type=click.Choice(["stdout", ".bumpversion.toml", "pyproject.toml"]),
+)
+def sample_config(prompt: bool, destination: str) -> None:
+    """Print a sample configuration file."""
+    if prompt:
+        destination = questionary.select(
+            "Destination", choices=["stdout", ".bumpversion.toml", "pyproject.toml"], default=destination
+        ).ask()
+
+    destination_config = create_configuration(destination, prompt)
+
+    if destination == "stdout":
+        print_info(dumps(destination_config))
+    else:
+        Path(destination).write_text(dumps(destination_config))
+
+
+@cli.command()
+@click.argument("version", nargs=1, type=str, required=False, default="")
+@click.option(
+    "--config-file",
+    metavar="FILE",
+    required=False,
+    envvar="BUMPVERSION_CONFIG_FILE",
+    type=click.Path(exists=True),
+    help="Config file to read most of the variables from.",
+)
+@click.option("--ascii", is_flag=True, help="Use ASCII characters only.")
+def show_bump(version: str, config_file: Optional[str], ascii: bool) -> None:
+    """Show the possible versions resulting from the bump subcommand."""
+    found_config_file = find_config_file(config_file)
+    config = get_configuration(found_config_file)
+    if not version:
+        version = config.current_version
+    box_style = "ascii" if ascii else "light"
+    visualize(config=config, version_str=version, box_style=box_style)
diff --git a/bumpversion/config/create.py b/bumpversion/config/create.py
new file mode 100644
index 00000000..edf51024
--- /dev/null
+++ b/bumpversion/config/create.py
@@ -0,0 +1,76 @@
+"""Module for creating a new config file."""
+from pathlib import Path
+from typing import Tuple
+
+import questionary
+from tomlkit import TOMLDocument
+
+
+def create_configuration(destination: str, prompt: bool) -> TOMLDocument:
+    """
+    Create a new configuration as a TOMLDocument.
+
+    Args:
+        destination: `stdout` or a path to a new or existing file.
+        prompt: `True` if the user should be prompted for input.
+
+    Returns:
+        The TOMLDocument structure with the updated configuration.
+    """
+    config, destination_config = get_defaults_from_dest(destination)
+
+    if prompt:
+        allow_dirty_default = "(Y/n)" if config["allow_dirty"] else "(y/N)"
+        answers = questionary.form(
+            current_version=questionary.text("What is the current version?", default=config["current_version"]),
+            commit=questionary.confirm(
+                "Commit changes made when bumping to version control?", default=config["commit"]
+            ),
+            allow_dirty=questionary.confirm(
+                "Allow dirty working directory when bumping?",
+                default=config["allow_dirty"],
+                instruction=(
+                    "If you are also creating or modifying other files (e.g. a CHANGELOG), say Yes. "
+                    f"{allow_dirty_default} "
+                ),
+            ),
+            tag=questionary.confirm("Tag changes made when bumping in version control?", default=config["tag"]),
+            commit_args=questionary.text(
+                "Any extra arguments to pass to the commit command?",
+                default=config["commit_args"] or "",
+                instruction="For example, `--no-verify` is useful if you have a pre-commit hook. ",
+            ),
+        ).ask()
+        config.update(answers)
+
+    for key, val in config.items():
+        destination_config["tool"]["bumpversion"][key] = val if val is not None else ""
+
+    return destination_config
+
+
+def get_defaults_from_dest(destination: str) -> Tuple[dict, TOMLDocument]:
+    """Get the default configuration and the configuration from the destination."""
+    from tomlkit import document, parse
+
+    from bumpversion.config import DEFAULTS
+
+    config = DEFAULTS.copy()
+    if Path(destination).exists():
+        destination_config = parse(Path(destination).read_text())
+    else:
+        destination_config = document()
+
+    destination_config.setdefault("tool", {})
+    destination_config["tool"].setdefault("bumpversion", {})
+    existing_config = destination_config["tool"]["bumpversion"]
+    if existing_config:
+        config.update(existing_config)
+
+    project_config = destination_config.get("project", {}).get("version")
+    config["current_version"] = config["current_version"] or project_config or "0.1.0"
+    del config["scm_info"]
+    del config["parts"]
+    del config["files"]
+
+    return config, destination_config
diff --git a/bumpversion/scm.py b/bumpversion/scm.py
index f5a2272b..fa4496df 100644
--- a/bumpversion/scm.py
+++ b/bumpversion/scm.py
@@ -25,7 +25,7 @@ class SCMInfo:
 
     tool: Optional[Type["SourceCodeManager"]] = None
     commit_sha: Optional[str] = None
-    distance_to_latest_tag: Optional[int] = None
+    distance_to_latest_tag: int = 0
     current_version: Optional[str] = None
     branch_name: Optional[str] = None
     short_branch_name: Optional[str] = None
diff --git a/bumpversion/utils.py b/bumpversion/utils.py
index ea9b68d4..c6d30cc5 100644
--- a/bumpversion/utils.py
+++ b/bumpversion/utils.py
@@ -1,4 +1,5 @@
 """General utilities."""
+import datetime
 import string
 from collections import ChainMap
 from dataclasses import asdict
@@ -6,6 +7,7 @@
 
 if TYPE_CHECKING:  # pragma: no-coverage
     from bumpversion.config import Config
+    from bumpversion.scm import SCMInfo
     from bumpversion.version_part import Version
 
 
@@ -51,19 +53,29 @@ def labels_for_format(serialize_format: str) -> List[str]:
     return [item[1] for item in string.Formatter().parse(serialize_format) if item[1]]
 
 
-def get_context(
-    config: "Config", current_version: Optional["Version"] = None, new_version: Optional["Version"] = None
-) -> ChainMap:
-    """Return the context for rendering messages and tags."""
-    import datetime
+def base_context(scm_info: Optional["SCMInfo"] = None) -> ChainMap:
+    """The default context for rendering messages and tags."""
+    from bumpversion.scm import SCMInfo  # Including this here to avoid circular imports
+
+    scm = asdict(scm_info) if scm_info else asdict(SCMInfo())
 
-    ctx = ChainMap(
-        {"current_version": config.current_version},
-        {"now": datetime.datetime.now(), "utcnow": datetime.datetime.utcnow()},
+    return ChainMap(
+        {
+            "now": datetime.datetime.now(),
+            "utcnow": datetime.datetime.now(datetime.timezone.utc),
+        },
         prefixed_environ(),
-        asdict(config.scm_info),
+        scm,
         {c: c for c in ("#", ";")},
     )
+
+
+def get_context(
+    config: "Config", current_version: Optional["Version"] = None, new_version: Optional["Version"] = None
+) -> ChainMap:
+    """Return the context for rendering messages and tags."""
+    ctx = base_context(config.scm_info)
+    ctx.new_child({"current_version": config.current_version})
     if current_version:
         ctx = ctx.new_child({f"current_{part}": current_version[part].value for part in current_version})
     if new_version:
diff --git a/bumpversion/visualize.py b/bumpversion/visualize.py
new file mode 100644
index 00000000..29e505b0
--- /dev/null
+++ b/bumpversion/visualize.py
@@ -0,0 +1,137 @@
+"""Visualize the bumpversion process."""
+from dataclasses import dataclass
+from typing import List, Optional
+
+from bumpversion.bump import get_next_version
+from bumpversion.config import Config
+from bumpversion.exceptions import BumpVersionError
+from bumpversion.ui import print_info
+from bumpversion.utils import base_context, get_context
+
+BOX_CHARS = {
+    "ascii": ["+", "+", "+", "+", "+", "+", "+", "+", "-", "|", "+"],
+    "light": ["╯", "╮", "╭", "╰", "┤", "┴", "┬", "├", "─", "│", "┼"],
+}
+
+
+@dataclass
+class Border:
+    """A border definition."""
+
+    corner_bottom_right: str
+    corner_top_right: str
+    corner_top_left: str
+    corner_bottom_left: str
+    divider_left: str
+    divider_up: str
+    divider_down: str
+    divider_right: str
+    line: str
+    pipe: str
+    cross: str
+
+
+def lead_string(version_str: str, border: Border, blank: bool = False) -> str:
+    """
+    Return the first part of a string with the bump character or spaces of the correct amount.
+
+    Examples:
+        >>> lead_string("1.0.0", Border(*BOX_CHARS["light"]))
+        '1.0.0 ── bump ─'
+        >>> lead_string("1.0.0", Border(*BOX_CHARS["light"]), blank=True)
+        '               '
+
+    Args:
+        version_str: The string to render as the starting point
+        border: The border definition to draw the lines
+        blank: If `True`, return a blank string the same length as the version bump string
+
+    Returns:
+        The version bump string or a blank string
+    """
+    version_bump = f"{version_str} {border.line * 2} bump {border.line}"
+    return " " * len(version_bump) if blank else version_bump
+
+
+def connection_str(border: Border, has_next: bool = False, has_previous: bool = False) -> str:
+    """
+    Return the correct connection string based on the next and previous.
+
+    Args:
+        border: The border definition to draw the lines
+        has_next: If `True`, there is a next line
+        has_previous: If `True`, there is a previous line
+
+    Returns:
+        A string that connects left-to-right and top-to-bottom based on the next and previous
+    """
+    if has_next and has_previous:
+        return border.divider_right + border.line
+    elif has_next:
+        return border.divider_down + border.line
+    elif has_previous:
+        return border.corner_bottom_left + border.line
+    else:
+        return border.line * 2
+
+
+def labeled_line(label: str, border: Border, fit_length: Optional[int] = None) -> str:
+    """
+    Return the version part string with the correct padding.
+
+    Args:
+        label: The label to render
+        border: The border definition to draw the lines
+        fit_length: The length to fit the label to
+
+    Returns:
+        A labeled line with leading and trailing spaces
+    """
+    if fit_length is None:
+        fit_length = len(label)
+    return f" {label} {border.line * (fit_length - len(label))}{border.line} "
+
+
+def filter_version_parts(config: Config) -> List[str]:
+    """
+    Return the version parts that are in the configuration.
+
+    Args:
+        config: The configuration to check against
+
+    Returns:
+        The version parts that are in the configuration
+    """
+    version_parts = [part for part in config.version_config.order if not part.startswith("$")]
+    default_context = base_context(config.scm_info)
+    return [part for part in version_parts if part not in default_context]
+
+
+def visualize(config: Config, version_str: str, box_style: str = "light") -> None:
+    """Output a visualization of the bump-my-version bump process."""
+    version = config.version_config.parse(version_str)
+    version_parts = filter_version_parts(config)
+    num_parts = len(version_parts)
+
+    box_style = box_style if box_style in BOX_CHARS else "light"
+    border = Border(*BOX_CHARS[box_style])
+
+    version_lead = lead_string(version_str, border)
+    blank_lead = lead_string(version_str, border, blank=True)
+    version_part_length = max(len(part) for part in version_parts)
+
+    for i, part in enumerate(version_parts):
+        line = [version_lead] if i == 0 else [blank_lead]
+
+        try:
+            next_version = get_next_version(version, config, part, None)
+            next_version_str = config.version_config.serialize(next_version, get_context(config))
+        except (BumpVersionError, ValueError) as e:
+            next_version_str = f"invalid: {e}"
+
+        has_next = i < num_parts - 1
+        has_previous = i > 0
+        line.append(connection_str(border, has_next=has_next, has_previous=has_previous))
+        line.append(labeled_line(part, border, version_part_length))
+        line.append(next_version_str)
+        print_info("".join(line))
diff --git a/docsrc/LICENSE b/docsrc/LICENSE
new file mode 100644
index 00000000..39479bb5
--- /dev/null
+++ b/docsrc/LICENSE
@@ -0,0 +1,2 @@
+```{include} ../LICENSE
+```
diff --git a/docsrc/conf.py b/docsrc/conf.py
index 855dd6cb..b60d6e95 100644
--- a/docsrc/conf.py
+++ b/docsrc/conf.py
@@ -22,7 +22,6 @@
 extensions = [
     "myst_parser",
     "autodoc2",
-    # "sphinx.ext.autodoc",
     "sphinx.ext.viewcode",
     "sphinx.ext.autosummary",
     "sphinx.ext.intersphinx",
@@ -42,16 +41,6 @@
 autodoc2_output_dir = "reference"
 autodoc2_index_template = None
 
-autodoc_default_flags = [
-    # Make sure that any autodoc declarations show the right members
-    "members",
-    "undoc-members",
-    "private-members",
-]
-autodoc_class_signature = "separated"
-autodoc_member_order = "bysource"
-autodoc_typehints = "description"
-
 autosummary_generate = True
 
 napoleon_attr_annotations = True
@@ -72,6 +61,7 @@
     "tasklist",
     "fieldlist",
 ]
+myst_heading_anchors = 2
 intersphinx_mapping = {
     "python": ("https://docs.python.org/3", None),
 }
diff --git a/docsrc/index.md b/docsrc/index.md
index 676e119c..981fe308 100644
--- a/docsrc/index.md
+++ b/docsrc/index.md
@@ -11,8 +11,8 @@ tutorials/index
 howtos/index
 reference/index
 explanation/index
-contributing
-changelog
+CONTRIBUTING
+CHANGELOG
 ```
 
 ## Indices and tables
diff --git a/docsrc/reference/bumpversion/bumpversion.aliases.md b/docsrc/reference/bumpversion/bumpversion.aliases.md
index 97cec39b..579c985b 100644
--- a/docsrc/reference/bumpversion/bumpversion.aliases.md
+++ b/docsrc/reference/bumpversion/bumpversion.aliases.md
@@ -23,7 +23,7 @@
 
 ### API
 
-`````{py:class} AliasedGroup(name: typing.Optional[str] = None, commands: typing.Optional[typing.Union[typing.Dict[str, click.core.Command], typing.Sequence[click.core.Command]]] = None, **attrs: typing.Any)
+`````{py:class} AliasedGroup(*args: typing.Any, **kwargs: typing.Any)
 :canonical: bumpversion.aliases.AliasedGroup
 
 Bases: {py:obj}`rich_click.rich_group.RichGroup`
diff --git a/docsrc/reference/bumpversion/bumpversion.bump.md b/docsrc/reference/bumpversion/bumpversion.bump.md
index b42e31c7..8e765bb1 100644
--- a/docsrc/reference/bumpversion/bumpversion.bump.md
+++ b/docsrc/reference/bumpversion/bumpversion.bump.md
@@ -46,7 +46,7 @@
 ````{py:data} logger
 :canonical: bumpversion.bump.logger
 :value: >
-   None
+   'get_indented_logger(...)'
 
 ```{autodoc2-docstring} bumpversion.bump.logger
 ```
diff --git a/docsrc/reference/bumpversion/bumpversion.cli.md b/docsrc/reference/bumpversion/bumpversion.cli.md
index 5562c134..11244357 100644
--- a/docsrc/reference/bumpversion/bumpversion.cli.md
+++ b/docsrc/reference/bumpversion/bumpversion.cli.md
@@ -31,6 +31,14 @@
   - ```{autodoc2-docstring} bumpversion.cli.replace
     :summary:
     ```
+* - {py:obj}`sample_config <bumpversion.cli.sample_config>`
+  - ```{autodoc2-docstring} bumpversion.cli.sample_config
+    :summary:
+    ```
+* - {py:obj}`show_bump <bumpversion.cli.show_bump>`
+  - ```{autodoc2-docstring} bumpversion.cli.show_bump
+    :summary:
+    ```
 ````
 
 ### Data
@@ -50,7 +58,7 @@
 ````{py:data} logger
 :canonical: bumpversion.cli.logger
 :value: >
-   None
+   'get_indented_logger(...)'
 
 ```{autodoc2-docstring} bumpversion.cli.logger
 ```
@@ -64,7 +72,7 @@
 ```
 ````
 
-````{py:function} bump(args: list, config_file: typing.Optional[str], verbose: int, allow_dirty: typing.Optional[bool], current_version: typing.Optional[str], new_version: typing.Optional[str], parse: typing.Optional[str], serialize: typing.Optional[typing.List[str]], search: typing.Optional[str], replace: typing.Optional[str], no_regex: bool, no_configured_files: bool, ignore_missing_version: bool, dry_run: bool, commit: typing.Optional[bool], tag: typing.Optional[bool], sign_tags: typing.Optional[bool], tag_name: typing.Optional[str], tag_message: typing.Optional[str], message: typing.Optional[str], commit_args: typing.Optional[str], show_list: bool) -> None
+````{py:function} bump(args: list, config_file: typing.Optional[str], verbose: int, allow_dirty: typing.Optional[bool], current_version: typing.Optional[str], new_version: typing.Optional[str], parse: typing.Optional[str], serialize: typing.Optional[typing.List[str]], search: typing.Optional[str], replace: typing.Optional[str], regex: typing.Optional[bool], no_configured_files: bool, ignore_missing_version: bool, dry_run: bool, commit: typing.Optional[bool], tag: typing.Optional[bool], sign_tags: typing.Optional[bool], tag_name: typing.Optional[str], tag_message: typing.Optional[str], message: typing.Optional[str], commit_args: typing.Optional[str], show_list: bool) -> None
 :canonical: bumpversion.cli.bump
 
 ```{autodoc2-docstring} bumpversion.cli.bump
@@ -78,9 +86,23 @@
 ```
 ````
 
-````{py:function} replace(files: list, config_file: typing.Optional[str], verbose: int, allow_dirty: typing.Optional[bool], current_version: typing.Optional[str], new_version: typing.Optional[str], parse: typing.Optional[str], serialize: typing.Optional[typing.List[str]], search: typing.Optional[str], replace: typing.Optional[str], no_regex: bool, no_configured_files: bool, ignore_missing_version: bool, dry_run: bool) -> None
+````{py:function} replace(files: list, config_file: typing.Optional[str], verbose: int, allow_dirty: typing.Optional[bool], current_version: typing.Optional[str], new_version: typing.Optional[str], parse: typing.Optional[str], serialize: typing.Optional[typing.List[str]], search: typing.Optional[str], replace: typing.Optional[str], regex: bool, no_configured_files: bool, ignore_missing_version: bool, dry_run: bool) -> None
 :canonical: bumpversion.cli.replace
 
 ```{autodoc2-docstring} bumpversion.cli.replace
 ```
 ````
+
+````{py:function} sample_config(prompt: bool, destination: str) -> None
+:canonical: bumpversion.cli.sample_config
+
+```{autodoc2-docstring} bumpversion.cli.sample_config
+```
+````
+
+````{py:function} show_bump(version: str, config_file: typing.Optional[str], ascii: bool) -> None
+:canonical: bumpversion.cli.show_bump
+
+```{autodoc2-docstring} bumpversion.cli.show_bump
+```
+````
diff --git a/docsrc/reference/bumpversion/bumpversion.config.create.md b/docsrc/reference/bumpversion/bumpversion.config.create.md
new file mode 100644
index 00000000..2a59dae8
--- /dev/null
+++ b/docsrc/reference/bumpversion/bumpversion.config.create.md
@@ -0,0 +1,42 @@
+# {py:mod}`bumpversion.config.create`
+
+```{py:module} bumpversion.config.create
+```
+
+```{autodoc2-docstring} bumpversion.config.create
+:allowtitles:
+```
+
+## Module Contents
+
+### Functions
+
+````{list-table}
+:class: autosummary longtable
+:align: left
+
+* - {py:obj}`create_configuration <bumpversion.config.create.create_configuration>`
+  - ```{autodoc2-docstring} bumpversion.config.create.create_configuration
+    :summary:
+    ```
+* - {py:obj}`get_defaults_from_dest <bumpversion.config.create.get_defaults_from_dest>`
+  - ```{autodoc2-docstring} bumpversion.config.create.get_defaults_from_dest
+    :summary:
+    ```
+````
+
+### API
+
+````{py:function} create_configuration(destination: str, prompt: bool) -> tomlkit.TOMLDocument
+:canonical: bumpversion.config.create.create_configuration
+
+```{autodoc2-docstring} bumpversion.config.create.create_configuration
+```
+````
+
+````{py:function} get_defaults_from_dest(destination: str) -> typing.Tuple[dict, tomlkit.TOMLDocument]
+:canonical: bumpversion.config.create.get_defaults_from_dest
+
+```{autodoc2-docstring} bumpversion.config.create.get_defaults_from_dest
+```
+````
diff --git a/docsrc/reference/bumpversion/bumpversion.config.files.md b/docsrc/reference/bumpversion/bumpversion.config.files.md
new file mode 100644
index 00000000..9beb1c4e
--- /dev/null
+++ b/docsrc/reference/bumpversion/bumpversion.config.files.md
@@ -0,0 +1,100 @@
+# {py:mod}`bumpversion.config.files`
+
+```{py:module} bumpversion.config.files
+```
+
+```{autodoc2-docstring} bumpversion.config.files
+:allowtitles:
+```
+
+## Module Contents
+
+### Functions
+
+````{list-table}
+:class: autosummary longtable
+:align: left
+
+* - {py:obj}`find_config_file <bumpversion.config.files.find_config_file>`
+  - ```{autodoc2-docstring} bumpversion.config.files.find_config_file
+    :summary:
+    ```
+* - {py:obj}`read_config_file <bumpversion.config.files.read_config_file>`
+  - ```{autodoc2-docstring} bumpversion.config.files.read_config_file
+    :summary:
+    ```
+* - {py:obj}`read_toml_file <bumpversion.config.files.read_toml_file>`
+  - ```{autodoc2-docstring} bumpversion.config.files.read_toml_file
+    :summary:
+    ```
+* - {py:obj}`update_config_file <bumpversion.config.files.update_config_file>`
+  - ```{autodoc2-docstring} bumpversion.config.files.update_config_file
+    :summary:
+    ```
+````
+
+### Data
+
+````{list-table}
+:class: autosummary longtable
+:align: left
+
+* - {py:obj}`logger <bumpversion.config.files.logger>`
+  - ```{autodoc2-docstring} bumpversion.config.files.logger
+    :summary:
+    ```
+* - {py:obj}`CONFIG_FILE_SEARCH_ORDER <bumpversion.config.files.CONFIG_FILE_SEARCH_ORDER>`
+  - ```{autodoc2-docstring} bumpversion.config.files.CONFIG_FILE_SEARCH_ORDER
+    :summary:
+    ```
+````
+
+### API
+
+````{py:data} logger
+:canonical: bumpversion.config.files.logger
+:value: >
+   'get_indented_logger(...)'
+
+```{autodoc2-docstring} bumpversion.config.files.logger
+```
+
+````
+
+````{py:data} CONFIG_FILE_SEARCH_ORDER
+:canonical: bumpversion.config.files.CONFIG_FILE_SEARCH_ORDER
+:value: >
+   ('.bumpversion.cfg', '.bumpversion.toml', 'setup.cfg', 'pyproject.toml')
+
+```{autodoc2-docstring} bumpversion.config.files.CONFIG_FILE_SEARCH_ORDER
+```
+
+````
+
+````{py:function} find_config_file(explicit_file: typing.Union[str, pathlib.Path, None] = None) -> typing.Union[pathlib.Path, None]
+:canonical: bumpversion.config.files.find_config_file
+
+```{autodoc2-docstring} bumpversion.config.files.find_config_file
+```
+````
+
+````{py:function} read_config_file(config_file: typing.Union[str, pathlib.Path, None] = None) -> typing.Dict[str, typing.Any]
+:canonical: bumpversion.config.files.read_config_file
+
+```{autodoc2-docstring} bumpversion.config.files.read_config_file
+```
+````
+
+````{py:function} read_toml_file(file_path: pathlib.Path) -> typing.Dict[str, typing.Any]
+:canonical: bumpversion.config.files.read_toml_file
+
+```{autodoc2-docstring} bumpversion.config.files.read_toml_file
+```
+````
+
+````{py:function} update_config_file(config_file: typing.Union[str, pathlib.Path], config: bumpversion.config.models.Config, current_version: bumpversion.version_part.Version, new_version: bumpversion.version_part.Version, context: typing.MutableMapping, dry_run: bool = False) -> None
+:canonical: bumpversion.config.files.update_config_file
+
+```{autodoc2-docstring} bumpversion.config.files.update_config_file
+```
+````
diff --git a/docsrc/reference/bumpversion/bumpversion.config.files_legacy.md b/docsrc/reference/bumpversion/bumpversion.config.files_legacy.md
new file mode 100644
index 00000000..1b856036
--- /dev/null
+++ b/docsrc/reference/bumpversion/bumpversion.config.files_legacy.md
@@ -0,0 +1,64 @@
+# {py:mod}`bumpversion.config.files_legacy`
+
+```{py:module} bumpversion.config.files_legacy
+```
+
+```{autodoc2-docstring} bumpversion.config.files_legacy
+:allowtitles:
+```
+
+## Module Contents
+
+### Functions
+
+````{list-table}
+:class: autosummary longtable
+:align: left
+
+* - {py:obj}`read_ini_file <bumpversion.config.files_legacy.read_ini_file>`
+  - ```{autodoc2-docstring} bumpversion.config.files_legacy.read_ini_file
+    :summary:
+    ```
+* - {py:obj}`update_ini_config_file <bumpversion.config.files_legacy.update_ini_config_file>`
+  - ```{autodoc2-docstring} bumpversion.config.files_legacy.update_ini_config_file
+    :summary:
+    ```
+````
+
+### Data
+
+````{list-table}
+:class: autosummary longtable
+:align: left
+
+* - {py:obj}`logger <bumpversion.config.files_legacy.logger>`
+  - ```{autodoc2-docstring} bumpversion.config.files_legacy.logger
+    :summary:
+    ```
+````
+
+### API
+
+````{py:data} logger
+:canonical: bumpversion.config.files_legacy.logger
+:value: >
+   'get_indented_logger(...)'
+
+```{autodoc2-docstring} bumpversion.config.files_legacy.logger
+```
+
+````
+
+````{py:function} read_ini_file(file_path: pathlib.Path) -> typing.Dict[str, typing.Any]
+:canonical: bumpversion.config.files_legacy.read_ini_file
+
+```{autodoc2-docstring} bumpversion.config.files_legacy.read_ini_file
+```
+````
+
+````{py:function} update_ini_config_file(config_file: typing.Union[str, pathlib.Path], current_version: str, new_version: str, dry_run: bool = False) -> None
+:canonical: bumpversion.config.files_legacy.update_ini_config_file
+
+```{autodoc2-docstring} bumpversion.config.files_legacy.update_ini_config_file
+```
+````
diff --git a/docsrc/reference/bumpversion/bumpversion.config.md b/docsrc/reference/bumpversion/bumpversion.config.md
index aff2f26e..3c4b3b50 100644
--- a/docsrc/reference/bumpversion/bumpversion.config.md
+++ b/docsrc/reference/bumpversion/bumpversion.config.md
@@ -7,27 +7,20 @@
 :allowtitles:
 ```
 
-## Module Contents
+## Submodules
 
-### Classes
+```{toctree}
+:titlesonly:
+:maxdepth: 1
 
-````{list-table}
-:class: autosummary longtable
-:align: left
+bumpversion.config.files
+bumpversion.config.models
+bumpversion.config.create
+bumpversion.config.files_legacy
+bumpversion.config.utils
+```
 
-* - {py:obj}`VersionPartConfig <bumpversion.config.VersionPartConfig>`
-  - ```{autodoc2-docstring} bumpversion.config.VersionPartConfig
-    :summary:
-    ```
-* - {py:obj}`FileConfig <bumpversion.config.FileConfig>`
-  - ```{autodoc2-docstring} bumpversion.config.FileConfig
-    :summary:
-    ```
-* - {py:obj}`Config <bumpversion.config.Config>`
-  - ```{autodoc2-docstring} bumpversion.config.Config
-    :summary:
-    ```
-````
+## Package Contents
 
 ### Functions
 
@@ -35,42 +28,14 @@
 :class: autosummary longtable
 :align: left
 
-* - {py:obj}`get_all_file_configs <bumpversion.config.get_all_file_configs>`
-  - ```{autodoc2-docstring} bumpversion.config.get_all_file_configs
-    :summary:
-    ```
 * - {py:obj}`get_configuration <bumpversion.config.get_configuration>`
   - ```{autodoc2-docstring} bumpversion.config.get_configuration
     :summary:
     ```
-* - {py:obj}`get_all_part_configs <bumpversion.config.get_all_part_configs>`
-  - ```{autodoc2-docstring} bumpversion.config.get_all_part_configs
-    :summary:
-    ```
 * - {py:obj}`check_current_version <bumpversion.config.check_current_version>`
   - ```{autodoc2-docstring} bumpversion.config.check_current_version
     :summary:
     ```
-* - {py:obj}`find_config_file <bumpversion.config.find_config_file>`
-  - ```{autodoc2-docstring} bumpversion.config.find_config_file
-    :summary:
-    ```
-* - {py:obj}`read_config_file <bumpversion.config.read_config_file>`
-  - ```{autodoc2-docstring} bumpversion.config.read_config_file
-    :summary:
-    ```
-* - {py:obj}`read_ini_file <bumpversion.config.read_ini_file>`
-  - ```{autodoc2-docstring} bumpversion.config.read_ini_file
-    :summary:
-    ```
-* - {py:obj}`read_toml_file <bumpversion.config.read_toml_file>`
-  - ```{autodoc2-docstring} bumpversion.config.read_toml_file
-    :summary:
-    ```
-* - {py:obj}`update_config_file <bumpversion.config.update_config_file>`
-  - ```{autodoc2-docstring} bumpversion.config.update_config_file
-    :summary:
-    ```
 ````
 
 ### Data
@@ -87,10 +52,6 @@
   - ```{autodoc2-docstring} bumpversion.config.DEFAULTS
     :summary:
     ```
-* - {py:obj}`CONFIG_FILE_SEARCH_ORDER <bumpversion.config.CONFIG_FILE_SEARCH_ORDER>`
-  - ```{autodoc2-docstring} bumpversion.config.CONFIG_FILE_SEARCH_ORDER
-    :summary:
-    ```
 ````
 
 ### API
@@ -98,408 +59,13 @@
 ````{py:data} logger
 :canonical: bumpversion.config.logger
 :value: >
-   None
+   'get_indented_logger(...)'
 
 ```{autodoc2-docstring} bumpversion.config.logger
 ```
 
 ````
 
-`````{py:class} VersionPartConfig
-:canonical: bumpversion.config.VersionPartConfig
-
-Bases: {py:obj}`pydantic.BaseModel`
-
-```{autodoc2-docstring} bumpversion.config.VersionPartConfig
-```
-
-````{py:attribute} values
-:canonical: bumpversion.config.VersionPartConfig.values
-:type: typing.Optional[list]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.VersionPartConfig.values
-```
-
-````
-
-````{py:attribute} optional_value
-:canonical: bumpversion.config.VersionPartConfig.optional_value
-:type: typing.Optional[str]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.VersionPartConfig.optional_value
-```
-
-````
-
-````{py:attribute} first_value
-:canonical: bumpversion.config.VersionPartConfig.first_value
-:type: typing.Optional[str]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.VersionPartConfig.first_value
-```
-
-````
-
-````{py:attribute} independent
-:canonical: bumpversion.config.VersionPartConfig.independent
-:type: bool
-:value: >
-   False
-
-```{autodoc2-docstring} bumpversion.config.VersionPartConfig.independent
-```
-
-````
-
-`````
-
-`````{py:class} FileConfig
-:canonical: bumpversion.config.FileConfig
-
-Bases: {py:obj}`pydantic.BaseModel`
-
-```{autodoc2-docstring} bumpversion.config.FileConfig
-```
-
-````{py:attribute} filename
-:canonical: bumpversion.config.FileConfig.filename
-:type: typing.Optional[str]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.FileConfig.filename
-```
-
-````
-
-````{py:attribute} glob
-:canonical: bumpversion.config.FileConfig.glob
-:type: typing.Optional[str]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.FileConfig.glob
-```
-
-````
-
-````{py:attribute} parse
-:canonical: bumpversion.config.FileConfig.parse
-:type: typing.Optional[str]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.FileConfig.parse
-```
-
-````
-
-````{py:attribute} serialize
-:canonical: bumpversion.config.FileConfig.serialize
-:type: typing.Optional[typing.List[str]]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.FileConfig.serialize
-```
-
-````
-
-````{py:attribute} search
-:canonical: bumpversion.config.FileConfig.search
-:type: typing.Optional[str]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.FileConfig.search
-```
-
-````
-
-````{py:attribute} replace
-:canonical: bumpversion.config.FileConfig.replace
-:type: typing.Optional[str]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.FileConfig.replace
-```
-
-````
-
-````{py:attribute} no_regex
-:canonical: bumpversion.config.FileConfig.no_regex
-:type: typing.Optional[bool]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.FileConfig.no_regex
-```
-
-````
-
-````{py:attribute} ignore_missing_version
-:canonical: bumpversion.config.FileConfig.ignore_missing_version
-:type: typing.Optional[bool]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.FileConfig.ignore_missing_version
-```
-
-````
-
-`````
-
-``````{py:class} Config
-:canonical: bumpversion.config.Config
-
-Bases: {py:obj}`pydantic.BaseSettings`
-
-```{autodoc2-docstring} bumpversion.config.Config
-```
-
-````{py:attribute} current_version
-:canonical: bumpversion.config.Config.current_version
-:type: typing.Optional[str]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.current_version
-```
-
-````
-
-````{py:attribute} parse
-:canonical: bumpversion.config.Config.parse
-:type: str
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.parse
-```
-
-````
-
-````{py:attribute} serialize
-:canonical: bumpversion.config.Config.serialize
-:type: typing.List[str]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.serialize
-```
-
-````
-
-````{py:attribute} search
-:canonical: bumpversion.config.Config.search
-:type: str
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.search
-```
-
-````
-
-````{py:attribute} replace
-:canonical: bumpversion.config.Config.replace
-:type: str
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.replace
-```
-
-````
-
-````{py:attribute} no_regex
-:canonical: bumpversion.config.Config.no_regex
-:type: bool
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.no_regex
-```
-
-````
-
-````{py:attribute} ignore_missing_version
-:canonical: bumpversion.config.Config.ignore_missing_version
-:type: bool
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.ignore_missing_version
-```
-
-````
-
-````{py:attribute} tag
-:canonical: bumpversion.config.Config.tag
-:type: bool
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.tag
-```
-
-````
-
-````{py:attribute} sign_tags
-:canonical: bumpversion.config.Config.sign_tags
-:type: bool
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.sign_tags
-```
-
-````
-
-````{py:attribute} tag_name
-:canonical: bumpversion.config.Config.tag_name
-:type: str
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.tag_name
-```
-
-````
-
-````{py:attribute} tag_message
-:canonical: bumpversion.config.Config.tag_message
-:type: typing.Optional[str]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.tag_message
-```
-
-````
-
-````{py:attribute} allow_dirty
-:canonical: bumpversion.config.Config.allow_dirty
-:type: bool
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.allow_dirty
-```
-
-````
-
-````{py:attribute} commit
-:canonical: bumpversion.config.Config.commit
-:type: bool
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.commit
-```
-
-````
-
-````{py:attribute} message
-:canonical: bumpversion.config.Config.message
-:type: str
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.message
-```
-
-````
-
-````{py:attribute} commit_args
-:canonical: bumpversion.config.Config.commit_args
-:type: typing.Optional[str]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.commit_args
-```
-
-````
-
-````{py:attribute} scm_info
-:canonical: bumpversion.config.Config.scm_info
-:type: typing.Optional[bumpversion.scm.SCMInfo]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.scm_info
-```
-
-````
-
-````{py:attribute} parts
-:canonical: bumpversion.config.Config.parts
-:type: typing.Dict[str, bumpversion.config.VersionPartConfig]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.parts
-```
-
-````
-
-````{py:attribute} files
-:canonical: bumpversion.config.Config.files
-:type: typing.List[bumpversion.config.FileConfig]
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.config.Config.files
-```
-
-````
-
-`````{py:class} Config
-:canonical: bumpversion.config.Config.Config
-
-```{autodoc2-docstring} bumpversion.config.Config.Config
-```
-
-````{py:attribute} env_prefix
-:canonical: bumpversion.config.Config.Config.env_prefix
-:value: >
-   'bumpversion_'
-
-```{autodoc2-docstring} bumpversion.config.Config.Config.env_prefix
-```
-
-````
-
-`````
-
-````{py:method} add_files(filename: typing.Union[str, typing.List[str]]) -> None
-:canonical: bumpversion.config.Config.add_files
-
-```{autodoc2-docstring} bumpversion.config.Config.add_files
-```
-
-````
-
-````{py:property} version_config
-:canonical: bumpversion.config.Config.version_config
-:type: bumpversion.version_part.VersionConfig
-
-```{autodoc2-docstring} bumpversion.config.Config.version_config
-```
-
-````
-
-``````
-
 ````{py:data} DEFAULTS
 :canonical: bumpversion.config.DEFAULTS
 :value: >
@@ -510,75 +76,16 @@ Bases: {py:obj}`pydantic.BaseSettings`
 
 ````
 
-````{py:data} CONFIG_FILE_SEARCH_ORDER
-:canonical: bumpversion.config.CONFIG_FILE_SEARCH_ORDER
-:value: >
-   ()
-
-```{autodoc2-docstring} bumpversion.config.CONFIG_FILE_SEARCH_ORDER
-```
-
-````
-
-````{py:function} get_all_file_configs(config_dict: dict) -> typing.List[bumpversion.config.FileConfig]
-:canonical: bumpversion.config.get_all_file_configs
-
-```{autodoc2-docstring} bumpversion.config.get_all_file_configs
-```
-````
-
-````{py:function} get_configuration(config_file: typing.Union[str, pathlib.Path, None] = None, **overrides) -> bumpversion.config.Config
+````{py:function} get_configuration(config_file: typing.Union[str, pathlib.Path, None] = None, **overrides) -> bumpversion.config.models.Config
 :canonical: bumpversion.config.get_configuration
 
 ```{autodoc2-docstring} bumpversion.config.get_configuration
 ```
 ````
 
-````{py:function} get_all_part_configs(config_dict: dict) -> typing.Dict[str, bumpversion.config.VersionPartConfig]
-:canonical: bumpversion.config.get_all_part_configs
-
-```{autodoc2-docstring} bumpversion.config.get_all_part_configs
-```
-````
-
-````{py:function} check_current_version(config: bumpversion.config.Config) -> str
+````{py:function} check_current_version(config: bumpversion.config.models.Config) -> str
 :canonical: bumpversion.config.check_current_version
 
 ```{autodoc2-docstring} bumpversion.config.check_current_version
 ```
 ````
-
-````{py:function} find_config_file(explicit_file: typing.Union[str, pathlib.Path, None] = None) -> typing.Union[pathlib.Path, None]
-:canonical: bumpversion.config.find_config_file
-
-```{autodoc2-docstring} bumpversion.config.find_config_file
-```
-````
-
-````{py:function} read_config_file(config_file: typing.Union[str, pathlib.Path, None] = None) -> typing.Dict[str, typing.Any]
-:canonical: bumpversion.config.read_config_file
-
-```{autodoc2-docstring} bumpversion.config.read_config_file
-```
-````
-
-````{py:function} read_ini_file(file_path: pathlib.Path) -> typing.Dict[str, typing.Any]
-:canonical: bumpversion.config.read_ini_file
-
-```{autodoc2-docstring} bumpversion.config.read_ini_file
-```
-````
-
-````{py:function} read_toml_file(file_path: pathlib.Path) -> typing.Dict[str, typing.Any]
-:canonical: bumpversion.config.read_toml_file
-
-```{autodoc2-docstring} bumpversion.config.read_toml_file
-```
-````
-
-````{py:function} update_config_file(config_file: typing.Union[str, pathlib.Path, None], current_version: str, new_version: str, dry_run: bool = False) -> None
-:canonical: bumpversion.config.update_config_file
-
-```{autodoc2-docstring} bumpversion.config.update_config_file
-```
-````
diff --git a/docsrc/reference/bumpversion/bumpversion.config.models.md b/docsrc/reference/bumpversion/bumpversion.config.models.md
new file mode 100644
index 00000000..b670f76f
--- /dev/null
+++ b/docsrc/reference/bumpversion/bumpversion.config.models.md
@@ -0,0 +1,545 @@
+# {py:mod}`bumpversion.config.models`
+
+```{py:module} bumpversion.config.models
+```
+
+```{autodoc2-docstring} bumpversion.config.models
+:allowtitles:
+```
+
+## Module Contents
+
+### Classes
+
+````{list-table}
+:class: autosummary longtable
+:align: left
+
+* - {py:obj}`VersionPartConfig <bumpversion.config.models.VersionPartConfig>`
+  - ```{autodoc2-docstring} bumpversion.config.models.VersionPartConfig
+    :summary:
+    ```
+* - {py:obj}`FileChange <bumpversion.config.models.FileChange>`
+  - ```{autodoc2-docstring} bumpversion.config.models.FileChange
+    :summary:
+    ```
+* - {py:obj}`Config <bumpversion.config.models.Config>`
+  - ```{autodoc2-docstring} bumpversion.config.models.Config
+    :summary:
+    ```
+````
+
+### Data
+
+````{list-table}
+:class: autosummary longtable
+:align: left
+
+* - {py:obj}`logger <bumpversion.config.models.logger>`
+  - ```{autodoc2-docstring} bumpversion.config.models.logger
+    :summary:
+    ```
+````
+
+### API
+
+````{py:data} logger
+:canonical: bumpversion.config.models.logger
+:value: >
+   'get_indented_logger(...)'
+
+```{autodoc2-docstring} bumpversion.config.models.logger
+```
+
+````
+
+`````{py:class} VersionPartConfig(**data: typing.Any)
+:canonical: bumpversion.config.models.VersionPartConfig
+
+Bases: {py:obj}`pydantic.BaseModel`
+
+```{autodoc2-docstring} bumpversion.config.models.VersionPartConfig
+```
+
+```{rubric} Initialization
+```
+
+```{autodoc2-docstring} bumpversion.config.models.VersionPartConfig.__init__
+```
+
+````{py:attribute} values
+:canonical: bumpversion.config.models.VersionPartConfig.values
+:type: typing.Optional[list]
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.VersionPartConfig.values
+```
+
+````
+
+````{py:attribute} optional_value
+:canonical: bumpversion.config.models.VersionPartConfig.optional_value
+:type: typing.Optional[str]
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.VersionPartConfig.optional_value
+```
+
+````
+
+````{py:attribute} first_value
+:canonical: bumpversion.config.models.VersionPartConfig.first_value
+:type: typing.Union[str, int, None]
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.VersionPartConfig.first_value
+```
+
+````
+
+````{py:attribute} independent
+:canonical: bumpversion.config.models.VersionPartConfig.independent
+:type: bool
+:value: >
+   False
+
+```{autodoc2-docstring} bumpversion.config.models.VersionPartConfig.independent
+```
+
+````
+
+`````
+
+`````{py:class} FileChange(**data: typing.Any)
+:canonical: bumpversion.config.models.FileChange
+
+Bases: {py:obj}`pydantic.BaseModel`
+
+```{autodoc2-docstring} bumpversion.config.models.FileChange
+```
+
+```{rubric} Initialization
+```
+
+```{autodoc2-docstring} bumpversion.config.models.FileChange.__init__
+```
+
+````{py:attribute} parse
+:canonical: bumpversion.config.models.FileChange.parse
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.FileChange.parse
+```
+
+````
+
+````{py:attribute} serialize
+:canonical: bumpversion.config.models.FileChange.serialize
+:type: tuple
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.FileChange.serialize
+```
+
+````
+
+````{py:attribute} search
+:canonical: bumpversion.config.models.FileChange.search
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.FileChange.search
+```
+
+````
+
+````{py:attribute} replace
+:canonical: bumpversion.config.models.FileChange.replace
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.FileChange.replace
+```
+
+````
+
+````{py:attribute} regex
+:canonical: bumpversion.config.models.FileChange.regex
+:type: bool
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.FileChange.regex
+```
+
+````
+
+````{py:attribute} ignore_missing_version
+:canonical: bumpversion.config.models.FileChange.ignore_missing_version
+:type: bool
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.FileChange.ignore_missing_version
+```
+
+````
+
+````{py:attribute} filename
+:canonical: bumpversion.config.models.FileChange.filename
+:type: typing.Optional[str]
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.FileChange.filename
+```
+
+````
+
+````{py:attribute} glob
+:canonical: bumpversion.config.models.FileChange.glob
+:type: typing.Optional[str]
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.FileChange.glob
+```
+
+````
+
+````{py:attribute} key_path
+:canonical: bumpversion.config.models.FileChange.key_path
+:type: typing.Optional[str]
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.FileChange.key_path
+```
+
+````
+
+````{py:method} __hash__()
+:canonical: bumpversion.config.models.FileChange.__hash__
+
+```{autodoc2-docstring} bumpversion.config.models.FileChange.__hash__
+```
+
+````
+
+````{py:method} get_search_pattern(context: typing.MutableMapping) -> typing.Tuple[re.Pattern, str]
+:canonical: bumpversion.config.models.FileChange.get_search_pattern
+
+```{autodoc2-docstring} bumpversion.config.models.FileChange.get_search_pattern
+```
+
+````
+
+`````
+
+`````{py:class} Config(_case_sensitive: bool | None = None, _env_prefix: str | None = None, _env_file: pydantic_settings.sources.DotenvType | None = ENV_FILE_SENTINEL, _env_file_encoding: str | None = None, _env_nested_delimiter: str | None = None, _secrets_dir: str | pathlib.Path | None = None, **values: typing.Any)
+:canonical: bumpversion.config.models.Config
+
+Bases: {py:obj}`pydantic_settings.BaseSettings`
+
+```{autodoc2-docstring} bumpversion.config.models.Config
+```
+
+```{rubric} Initialization
+```
+
+```{autodoc2-docstring} bumpversion.config.models.Config.__init__
+```
+
+````{py:attribute} current_version
+:canonical: bumpversion.config.models.Config.current_version
+:type: typing.Optional[str]
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config.current_version
+```
+
+````
+
+````{py:attribute} parse
+:canonical: bumpversion.config.models.Config.parse
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config.parse
+```
+
+````
+
+````{py:attribute} serialize
+:canonical: bumpversion.config.models.Config.serialize
+:type: tuple
+:value: >
+   'Field(...)'
+
+```{autodoc2-docstring} bumpversion.config.models.Config.serialize
+```
+
+````
+
+````{py:attribute} search
+:canonical: bumpversion.config.models.Config.search
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config.search
+```
+
+````
+
+````{py:attribute} replace
+:canonical: bumpversion.config.models.Config.replace
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config.replace
+```
+
+````
+
+````{py:attribute} regex
+:canonical: bumpversion.config.models.Config.regex
+:type: bool
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config.regex
+```
+
+````
+
+````{py:attribute} ignore_missing_version
+:canonical: bumpversion.config.models.Config.ignore_missing_version
+:type: bool
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config.ignore_missing_version
+```
+
+````
+
+````{py:attribute} tag
+:canonical: bumpversion.config.models.Config.tag
+:type: bool
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config.tag
+```
+
+````
+
+````{py:attribute} sign_tags
+:canonical: bumpversion.config.models.Config.sign_tags
+:type: bool
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config.sign_tags
+```
+
+````
+
+````{py:attribute} tag_name
+:canonical: bumpversion.config.models.Config.tag_name
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config.tag_name
+```
+
+````
+
+````{py:attribute} tag_message
+:canonical: bumpversion.config.models.Config.tag_message
+:type: typing.Optional[str]
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config.tag_message
+```
+
+````
+
+````{py:attribute} allow_dirty
+:canonical: bumpversion.config.models.Config.allow_dirty
+:type: bool
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config.allow_dirty
+```
+
+````
+
+````{py:attribute} commit
+:canonical: bumpversion.config.models.Config.commit
+:type: bool
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config.commit
+```
+
+````
+
+````{py:attribute} message
+:canonical: bumpversion.config.models.Config.message
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config.message
+```
+
+````
+
+````{py:attribute} commit_args
+:canonical: bumpversion.config.models.Config.commit_args
+:type: typing.Optional[str]
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config.commit_args
+```
+
+````
+
+````{py:attribute} scm_info
+:canonical: bumpversion.config.models.Config.scm_info
+:type: typing.Optional[bumpversion.scm.SCMInfo]
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config.scm_info
+```
+
+````
+
+````{py:attribute} parts
+:canonical: bumpversion.config.models.Config.parts
+:type: typing.Dict[str, bumpversion.config.models.VersionPartConfig]
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config.parts
+```
+
+````
+
+````{py:attribute} files
+:canonical: bumpversion.config.models.Config.files
+:type: typing.List[bumpversion.config.models.FileChange]
+:value: >
+   'Field(...)'
+
+```{autodoc2-docstring} bumpversion.config.models.Config.files
+```
+
+````
+
+````{py:attribute} included_paths
+:canonical: bumpversion.config.models.Config.included_paths
+:type: typing.List[str]
+:value: >
+   'Field(...)'
+
+```{autodoc2-docstring} bumpversion.config.models.Config.included_paths
+```
+
+````
+
+````{py:attribute} excluded_paths
+:canonical: bumpversion.config.models.Config.excluded_paths
+:type: typing.List[str]
+:value: >
+   'Field(...)'
+
+```{autodoc2-docstring} bumpversion.config.models.Config.excluded_paths
+```
+
+````
+
+````{py:attribute} model_config
+:canonical: bumpversion.config.models.Config.model_config
+:value: >
+   'SettingsConfigDict(...)'
+
+```{autodoc2-docstring} bumpversion.config.models.Config.model_config
+```
+
+````
+
+````{py:attribute} _resolved_filemap
+:canonical: bumpversion.config.models.Config._resolved_filemap
+:type: typing.Optional[typing.Dict[str, typing.List[bumpversion.config.models.FileChange]]]
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.config.models.Config._resolved_filemap
+```
+
+````
+
+````{py:method} add_files(filename: typing.Union[str, typing.List[str]]) -> None
+:canonical: bumpversion.config.models.Config.add_files
+
+```{autodoc2-docstring} bumpversion.config.models.Config.add_files
+```
+
+````
+
+````{py:property} resolved_filemap
+:canonical: bumpversion.config.models.Config.resolved_filemap
+:type: typing.Dict[str, typing.List[bumpversion.config.models.FileChange]]
+
+```{autodoc2-docstring} bumpversion.config.models.Config.resolved_filemap
+```
+
+````
+
+````{py:method} _resolve_filemap() -> typing.Dict[str, typing.List[bumpversion.config.models.FileChange]]
+:canonical: bumpversion.config.models.Config._resolve_filemap
+
+```{autodoc2-docstring} bumpversion.config.models.Config._resolve_filemap
+```
+
+````
+
+````{py:property} files_to_modify
+:canonical: bumpversion.config.models.Config.files_to_modify
+:type: typing.List[bumpversion.config.models.FileChange]
+
+```{autodoc2-docstring} bumpversion.config.models.Config.files_to_modify
+```
+
+````
+
+````{py:property} version_config
+:canonical: bumpversion.config.models.Config.version_config
+:type: bumpversion.version_part.VersionConfig
+
+```{autodoc2-docstring} bumpversion.config.models.Config.version_config
+```
+
+````
+
+`````
diff --git a/docsrc/reference/bumpversion/bumpversion.config.utils.md b/docsrc/reference/bumpversion/bumpversion.config.utils.md
new file mode 100644
index 00000000..9d7e45e9
--- /dev/null
+++ b/docsrc/reference/bumpversion/bumpversion.config.utils.md
@@ -0,0 +1,53 @@
+# {py:mod}`bumpversion.config.utils`
+
+```{py:module} bumpversion.config.utils
+```
+
+```{autodoc2-docstring} bumpversion.config.utils
+:allowtitles:
+```
+
+## Module Contents
+
+### Functions
+
+````{list-table}
+:class: autosummary longtable
+:align: left
+
+* - {py:obj}`get_all_file_configs <bumpversion.config.utils.get_all_file_configs>`
+  - ```{autodoc2-docstring} bumpversion.config.utils.get_all_file_configs
+    :summary:
+    ```
+* - {py:obj}`get_all_part_configs <bumpversion.config.utils.get_all_part_configs>`
+  - ```{autodoc2-docstring} bumpversion.config.utils.get_all_part_configs
+    :summary:
+    ```
+* - {py:obj}`resolve_glob_files <bumpversion.config.utils.resolve_glob_files>`
+  - ```{autodoc2-docstring} bumpversion.config.utils.resolve_glob_files
+    :summary:
+    ```
+````
+
+### API
+
+````{py:function} get_all_file_configs(config_dict: dict) -> typing.List[bumpversion.config.models.FileChange]
+:canonical: bumpversion.config.utils.get_all_file_configs
+
+```{autodoc2-docstring} bumpversion.config.utils.get_all_file_configs
+```
+````
+
+````{py:function} get_all_part_configs(config_dict: dict) -> typing.Dict[str, bumpversion.config.models.VersionPartConfig]
+:canonical: bumpversion.config.utils.get_all_part_configs
+
+```{autodoc2-docstring} bumpversion.config.utils.get_all_part_configs
+```
+````
+
+````{py:function} resolve_glob_files(file_cfg: bumpversion.config.models.FileChange) -> typing.List[bumpversion.config.models.FileChange]
+:canonical: bumpversion.config.utils.resolve_glob_files
+
+```{autodoc2-docstring} bumpversion.config.utils.resolve_glob_files
+```
+````
diff --git a/docsrc/reference/bumpversion/bumpversion.files.md b/docsrc/reference/bumpversion/bumpversion.files.md
index 1a098f05..2257aeeb 100644
--- a/docsrc/reference/bumpversion/bumpversion.files.md
+++ b/docsrc/reference/bumpversion/bumpversion.files.md
@@ -19,6 +19,14 @@
   - ```{autodoc2-docstring} bumpversion.files.ConfiguredFile
     :summary:
     ```
+* - {py:obj}`FileUpdater <bumpversion.files.FileUpdater>`
+  - ```{autodoc2-docstring} bumpversion.files.FileUpdater
+    :summary:
+    ```
+* - {py:obj}`DataFileUpdater <bumpversion.files.DataFileUpdater>`
+  - ```{autodoc2-docstring} bumpversion.files.DataFileUpdater
+    :summary:
+    ```
 ````
 
 ### Functions
@@ -27,20 +35,20 @@
 :class: autosummary longtable
 :align: left
 
-* - {py:obj}`resolve_file_config <bumpversion.files.resolve_file_config>`
-  - ```{autodoc2-docstring} bumpversion.files.resolve_file_config
+* - {py:obj}`contains_pattern <bumpversion.files.contains_pattern>`
+  - ```{autodoc2-docstring} bumpversion.files.contains_pattern
     :summary:
     ```
-* - {py:obj}`modify_files <bumpversion.files.modify_files>`
-  - ```{autodoc2-docstring} bumpversion.files.modify_files
+* - {py:obj}`log_changes <bumpversion.files.log_changes>`
+  - ```{autodoc2-docstring} bumpversion.files.log_changes
     :summary:
     ```
-* - {py:obj}`get_glob_files <bumpversion.files.get_glob_files>`
-  - ```{autodoc2-docstring} bumpversion.files.get_glob_files
+* - {py:obj}`resolve_file_config <bumpversion.files.resolve_file_config>`
+  - ```{autodoc2-docstring} bumpversion.files.resolve_file_config
     :summary:
     ```
-* - {py:obj}`_check_files_contain_version <bumpversion.files._check_files_contain_version>`
-  - ```{autodoc2-docstring} bumpversion.files._check_files_contain_version
+* - {py:obj}`modify_files <bumpversion.files.modify_files>`
+  - ```{autodoc2-docstring} bumpversion.files.modify_files
     :summary:
     ```
 ````
@@ -62,14 +70,28 @@
 ````{py:data} logger
 :canonical: bumpversion.files.logger
 :value: >
-   None
+   'get_indented_logger(...)'
 
 ```{autodoc2-docstring} bumpversion.files.logger
 ```
 
 ````
 
-`````{py:class} ConfiguredFile(file_cfg: bumpversion.config.FileConfig, version_config: bumpversion.version_part.VersionConfig, search: typing.Optional[str] = None, replace: typing.Optional[str] = None)
+````{py:function} contains_pattern(search: re.Pattern, contents: str) -> bool
+:canonical: bumpversion.files.contains_pattern
+
+```{autodoc2-docstring} bumpversion.files.contains_pattern
+```
+````
+
+````{py:function} log_changes(file_path: str, file_content_before: str, file_content_after: str, dry_run: bool = False) -> None
+:canonical: bumpversion.files.log_changes
+
+```{autodoc2-docstring} bumpversion.files.log_changes
+```
+````
+
+`````{py:class} ConfiguredFile(file_change: bumpversion.config.models.FileChange, version_config: bumpversion.version_part.VersionConfig, search: typing.Optional[str] = None, replace: typing.Optional[str] = None)
 :canonical: bumpversion.files.ConfiguredFile
 
 ```{autodoc2-docstring} bumpversion.files.ConfiguredFile
@@ -97,34 +119,18 @@
 
 ````
 
-````{py:method} contains_version(version: bumpversion.version_part.Version, context: typing.MutableMapping) -> bool
-:canonical: bumpversion.files.ConfiguredFile.contains_version
-
-```{autodoc2-docstring} bumpversion.files.ConfiguredFile.contains_version
-```
-
-````
-
-````{py:method} contains(search: re.Pattern) -> bool
-:canonical: bumpversion.files.ConfiguredFile.contains
+````{py:method} _contains_change_pattern(search_expression: re.Pattern, raw_search_expression: str, version: bumpversion.version_part.Version, context: typing.MutableMapping) -> bool
+:canonical: bumpversion.files.ConfiguredFile._contains_change_pattern
 
-```{autodoc2-docstring} bumpversion.files.ConfiguredFile.contains
+```{autodoc2-docstring} bumpversion.files.ConfiguredFile._contains_change_pattern
 ```
 
 ````
 
-````{py:method} replace_version(current_version: bumpversion.version_part.Version, new_version: bumpversion.version_part.Version, context: typing.MutableMapping, dry_run: bool = False) -> None
-:canonical: bumpversion.files.ConfiguredFile.replace_version
+````{py:method} make_file_change(current_version: bumpversion.version_part.Version, new_version: bumpversion.version_part.Version, context: typing.MutableMapping, dry_run: bool = False) -> None
+:canonical: bumpversion.files.ConfiguredFile.make_file_change
 
-```{autodoc2-docstring} bumpversion.files.ConfiguredFile.replace_version
-```
-
-````
-
-````{py:method} get_search_pattern(context: typing.MutableMapping) -> re.Pattern
-:canonical: bumpversion.files.ConfiguredFile.get_search_pattern
-
-```{autodoc2-docstring} bumpversion.files.ConfiguredFile.get_search_pattern
+```{autodoc2-docstring} bumpversion.files.ConfiguredFile.make_file_change
 ```
 
 ````
@@ -141,7 +147,7 @@
 
 `````
 
-````{py:function} resolve_file_config(files: typing.List[bumpversion.config.FileConfig], version_config: bumpversion.version_part.VersionConfig, search: typing.Optional[str] = None, replace: typing.Optional[str] = None) -> typing.List[bumpversion.files.ConfiguredFile]
+````{py:function} resolve_file_config(files: typing.List[bumpversion.config.models.FileChange], version_config: bumpversion.version_part.VersionConfig, search: typing.Optional[str] = None, replace: typing.Optional[str] = None) -> typing.List[bumpversion.files.ConfiguredFile]
 :canonical: bumpversion.files.resolve_file_config
 
 ```{autodoc2-docstring} bumpversion.files.resolve_file_config
@@ -155,16 +161,54 @@
 ```
 ````
 
-````{py:function} get_glob_files(file_cfg: bumpversion.config.FileConfig, version_config: bumpversion.version_part.VersionConfig, search: typing.Optional[str] = None, replace: typing.Optional[str] = None) -> typing.List[bumpversion.files.ConfiguredFile]
-:canonical: bumpversion.files.get_glob_files
+`````{py:class} FileUpdater(file_change: bumpversion.config.models.FileChange, version_config: bumpversion.version_part.VersionConfig, search: typing.Optional[str] = None, replace: typing.Optional[str] = None)
+:canonical: bumpversion.files.FileUpdater
+
+```{autodoc2-docstring} bumpversion.files.FileUpdater
+```
+
+```{rubric} Initialization
+```
+
+```{autodoc2-docstring} bumpversion.files.FileUpdater.__init__
+```
+
+````{py:method} update_file(current_version: bumpversion.version_part.Version, new_version: bumpversion.version_part.Version, context: typing.MutableMapping, dry_run: bool = False) -> None
+:canonical: bumpversion.files.FileUpdater.update_file
+
+```{autodoc2-docstring} bumpversion.files.FileUpdater.update_file
+```
+
+````
+
+`````
+
+`````{py:class} DataFileUpdater(file_change: bumpversion.config.models.FileChange, version_part_configs: typing.Dict[str, bumpversion.config.models.VersionPartConfig])
+:canonical: bumpversion.files.DataFileUpdater
+
+```{autodoc2-docstring} bumpversion.files.DataFileUpdater
+```
+
+```{rubric} Initialization
+```
 
-```{autodoc2-docstring} bumpversion.files.get_glob_files
+```{autodoc2-docstring} bumpversion.files.DataFileUpdater.__init__
 ```
+
+````{py:method} update_file(current_version: bumpversion.version_part.Version, new_version: bumpversion.version_part.Version, context: typing.MutableMapping, dry_run: bool = False) -> None
+:canonical: bumpversion.files.DataFileUpdater.update_file
+
+```{autodoc2-docstring} bumpversion.files.DataFileUpdater.update_file
+```
+
 ````
 
-````{py:function} _check_files_contain_version(files: typing.List[bumpversion.files.ConfiguredFile], current_version: bumpversion.version_part.Version, context: typing.MutableMapping) -> None
-:canonical: bumpversion.files._check_files_contain_version
+````{py:method} _update_toml_file(search_for: re.Pattern, raw_search_pattern: str, replace_with: str, dry_run: bool = False) -> None
+:canonical: bumpversion.files.DataFileUpdater._update_toml_file
 
-```{autodoc2-docstring} bumpversion.files._check_files_contain_version
+```{autodoc2-docstring} bumpversion.files.DataFileUpdater._update_toml_file
 ```
+
 ````
+
+`````
diff --git a/docsrc/reference/bumpversion/bumpversion.functions.md b/docsrc/reference/bumpversion/bumpversion.functions.md
index bb0b42fa..04e75998 100644
--- a/docsrc/reference/bumpversion/bumpversion.functions.md
+++ b/docsrc/reference/bumpversion/bumpversion.functions.md
@@ -81,7 +81,7 @@
 
 `````
 
-`````{py:class} NumericFunction(optional_value: typing.Optional[str] = None, first_value: typing.Optional[str] = None)
+`````{py:class} NumericFunction(optional_value: typing.Union[str, int, None] = None, first_value: typing.Union[str, int, None] = None)
 :canonical: bumpversion.functions.NumericFunction
 
 Bases: {py:obj}`bumpversion.functions.PartFunction`
@@ -98,7 +98,7 @@ Bases: {py:obj}`bumpversion.functions.PartFunction`
 ````{py:attribute} FIRST_NUMERIC
 :canonical: bumpversion.functions.NumericFunction.FIRST_NUMERIC
 :value: >
-   None
+   'compile(...)'
 
 ```{autodoc2-docstring} bumpversion.functions.NumericFunction.FIRST_NUMERIC
 ```
diff --git a/docsrc/reference/bumpversion/bumpversion.indented_logger.md b/docsrc/reference/bumpversion/bumpversion.indented_logger.md
new file mode 100644
index 00000000..40e0b412
--- /dev/null
+++ b/docsrc/reference/bumpversion/bumpversion.indented_logger.md
@@ -0,0 +1,112 @@
+# {py:mod}`bumpversion.indented_logger`
+
+```{py:module} bumpversion.indented_logger
+```
+
+```{autodoc2-docstring} bumpversion.indented_logger
+:allowtitles:
+```
+
+## Module Contents
+
+### Classes
+
+````{list-table}
+:class: autosummary longtable
+:align: left
+
+* - {py:obj}`IndentedLoggerAdapter <bumpversion.indented_logger.IndentedLoggerAdapter>`
+  - ```{autodoc2-docstring} bumpversion.indented_logger.IndentedLoggerAdapter
+    :summary:
+    ```
+````
+
+### Data
+
+````{list-table}
+:class: autosummary longtable
+:align: left
+
+* - {py:obj}`CURRENT_INDENT <bumpversion.indented_logger.CURRENT_INDENT>`
+  - ```{autodoc2-docstring} bumpversion.indented_logger.CURRENT_INDENT
+    :summary:
+    ```
+````
+
+### API
+
+````{py:data} CURRENT_INDENT
+:canonical: bumpversion.indented_logger.CURRENT_INDENT
+:value: >
+   'ContextVar(...)'
+
+```{autodoc2-docstring} bumpversion.indented_logger.CURRENT_INDENT
+```
+
+````
+
+`````{py:class} IndentedLoggerAdapter(logger: logging.Logger, extra: typing.Optional[dict] = None, depth: int = 2, indent_char: str = ' ', reset: bool = False)
+:canonical: bumpversion.indented_logger.IndentedLoggerAdapter
+
+Bases: {py:obj}`logging.LoggerAdapter`
+
+```{autodoc2-docstring} bumpversion.indented_logger.IndentedLoggerAdapter
+```
+
+```{rubric} Initialization
+```
+
+```{autodoc2-docstring} bumpversion.indented_logger.IndentedLoggerAdapter.__init__
+```
+
+````{py:property} current_indent
+:canonical: bumpversion.indented_logger.IndentedLoggerAdapter.current_indent
+:type: int
+
+```{autodoc2-docstring} bumpversion.indented_logger.IndentedLoggerAdapter.current_indent
+```
+
+````
+
+````{py:method} indent(amount: int = 1) -> None
+:canonical: bumpversion.indented_logger.IndentedLoggerAdapter.indent
+
+```{autodoc2-docstring} bumpversion.indented_logger.IndentedLoggerAdapter.indent
+```
+
+````
+
+````{py:method} dedent(amount: int = 1) -> None
+:canonical: bumpversion.indented_logger.IndentedLoggerAdapter.dedent
+
+```{autodoc2-docstring} bumpversion.indented_logger.IndentedLoggerAdapter.dedent
+```
+
+````
+
+````{py:method} reset() -> None
+:canonical: bumpversion.indented_logger.IndentedLoggerAdapter.reset
+
+```{autodoc2-docstring} bumpversion.indented_logger.IndentedLoggerAdapter.reset
+```
+
+````
+
+````{py:property} indent_str
+:canonical: bumpversion.indented_logger.IndentedLoggerAdapter.indent_str
+:type: str
+
+```{autodoc2-docstring} bumpversion.indented_logger.IndentedLoggerAdapter.indent_str
+```
+
+````
+
+````{py:method} process(msg: str, kwargs: typing.Optional[typing.MutableMapping[str, typing.Any]]) -> typing.Tuple[str, typing.MutableMapping[str, typing.Any]]
+:canonical: bumpversion.indented_logger.IndentedLoggerAdapter.process
+
+```{autodoc2-docstring} bumpversion.indented_logger.IndentedLoggerAdapter.process
+```
+
+````
+
+`````
diff --git a/docsrc/reference/bumpversion/bumpversion.logging.md b/docsrc/reference/bumpversion/bumpversion.logging.md
deleted file mode 100644
index 1f9646e6..00000000
--- a/docsrc/reference/bumpversion/bumpversion.logging.md
+++ /dev/null
@@ -1,67 +0,0 @@
-# {py:mod}`bumpversion.logging`
-
-```{py:module} bumpversion.logging
-```
-
-```{autodoc2-docstring} bumpversion.logging
-:allowtitles:
-```
-
-## Module Contents
-
-### Functions
-
-````{list-table}
-:class: autosummary longtable
-:align: left
-
-* - {py:obj}`setup_logging <bumpversion.logging.setup_logging>`
-  - ```{autodoc2-docstring} bumpversion.logging.setup_logging
-    :summary:
-    ```
-````
-
-### Data
-
-````{list-table}
-:class: autosummary longtable
-:align: left
-
-* - {py:obj}`logger <bumpversion.logging.logger>`
-  - ```{autodoc2-docstring} bumpversion.logging.logger
-    :summary:
-    ```
-* - {py:obj}`VERBOSITY <bumpversion.logging.VERBOSITY>`
-  - ```{autodoc2-docstring} bumpversion.logging.VERBOSITY
-    :summary:
-    ```
-````
-
-### API
-
-````{py:data} logger
-:canonical: bumpversion.logging.logger
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.logging.logger
-```
-
-````
-
-````{py:data} VERBOSITY
-:canonical: bumpversion.logging.VERBOSITY
-:value: >
-   None
-
-```{autodoc2-docstring} bumpversion.logging.VERBOSITY
-```
-
-````
-
-````{py:function} setup_logging(verbose: int = 0) -> None
-:canonical: bumpversion.logging.setup_logging
-
-```{autodoc2-docstring} bumpversion.logging.setup_logging
-```
-````
diff --git a/docsrc/reference/bumpversion/bumpversion.md b/docsrc/reference/bumpversion/bumpversion.md
index d33bb566..faadab79 100644
--- a/docsrc/reference/bumpversion/bumpversion.md
+++ b/docsrc/reference/bumpversion/bumpversion.md
@@ -7,6 +7,15 @@
 :allowtitles:
 ```
 
+## Subpackages
+
+```{toctree}
+:titlesonly:
+:maxdepth: 3
+
+bumpversion.config
+```
+
 ## Submodules
 
 ```{toctree}
@@ -15,12 +24,12 @@
 
 bumpversion.functions
 bumpversion.version_part
-bumpversion.logging
 bumpversion.files
 bumpversion.show
-bumpversion.config
+bumpversion.indented_logger
 bumpversion.ui
 bumpversion.autocast
+bumpversion.visualize
 bumpversion.aliases
 bumpversion.cli
 bumpversion.utils
diff --git a/docsrc/reference/bumpversion/bumpversion.scm.md b/docsrc/reference/bumpversion/bumpversion.scm.md
index 9d8d0492..1d6c8599 100644
--- a/docsrc/reference/bumpversion/bumpversion.scm.md
+++ b/docsrc/reference/bumpversion/bumpversion.scm.md
@@ -62,7 +62,7 @@
 ````{py:data} logger
 :canonical: bumpversion.scm.logger
 :value: >
-   None
+   'get_indented_logger(...)'
 
 ```{autodoc2-docstring} bumpversion.scm.logger
 ```
@@ -231,7 +231,7 @@
 
 ````
 
-````{py:method} latest_tag_info(tag_pattern: str) -> bumpversion.scm.SCMInfo
+````{py:method} latest_tag_info(tag_name: str, parse_pattern: str) -> bumpversion.scm.SCMInfo
 :canonical: bumpversion.scm.SourceCodeManager.latest_tag_info
 :abstractmethod:
 :classmethod:
@@ -270,6 +270,15 @@
 
 ````
 
+````{py:method} get_version_from_tag(tag: str, tag_name: str, parse_pattern: str) -> typing.Optional[str]
+:canonical: bumpversion.scm.SourceCodeManager.get_version_from_tag
+:classmethod:
+
+```{autodoc2-docstring} bumpversion.scm.SourceCodeManager.get_version_from_tag
+```
+
+````
+
 ````{py:method} commit_to_scm(files: typing.List[typing.Union[str, pathlib.Path]], config: bumpversion.config.Config, context: typing.MutableMapping, extra_args: typing.Optional[typing.List[str]] = None, dry_run: bool = False) -> None
 :canonical: bumpversion.scm.SourceCodeManager.commit_to_scm
 :classmethod:
@@ -350,7 +359,7 @@ Bases: {py:obj}`bumpversion.scm.SourceCodeManager`
 
 ````
 
-````{py:method} latest_tag_info(tag_pattern: str) -> bumpversion.scm.SCMInfo
+````{py:method} latest_tag_info(tag_name: str, parse_pattern: str) -> bumpversion.scm.SCMInfo
 :canonical: bumpversion.scm.Git.latest_tag_info
 :classmethod:
 
@@ -420,7 +429,7 @@ Bases: {py:obj}`bumpversion.scm.SourceCodeManager`
 
 ````
 
-````{py:method} latest_tag_info(tag_pattern: str) -> bumpversion.scm.SCMInfo
+````{py:method} latest_tag_info(tag_name: str, parse_pattern: str) -> bumpversion.scm.SCMInfo
 :canonical: bumpversion.scm.Mercurial.latest_tag_info
 :classmethod:
 
@@ -458,7 +467,7 @@ Bases: {py:obj}`bumpversion.scm.SourceCodeManager`
 
 `````
 
-````{py:function} get_scm_info(tag_pattern: str) -> bumpversion.scm.SCMInfo
+````{py:function} get_scm_info(tag_name: str, parse_pattern: str) -> bumpversion.scm.SCMInfo
 :canonical: bumpversion.scm.get_scm_info
 
 ```{autodoc2-docstring} bumpversion.scm.get_scm_info
diff --git a/docsrc/reference/bumpversion/bumpversion.ui.md b/docsrc/reference/bumpversion/bumpversion.ui.md
index 7a57880d..85ed5321 100644
--- a/docsrc/reference/bumpversion/bumpversion.ui.md
+++ b/docsrc/reference/bumpversion/bumpversion.ui.md
@@ -15,6 +15,14 @@
 :class: autosummary longtable
 :align: left
 
+* - {py:obj}`get_indented_logger <bumpversion.ui.get_indented_logger>`
+  - ```{autodoc2-docstring} bumpversion.ui.get_indented_logger
+    :summary:
+    ```
+* - {py:obj}`setup_logging <bumpversion.ui.setup_logging>`
+  - ```{autodoc2-docstring} bumpversion.ui.setup_logging
+    :summary:
+    ```
 * - {py:obj}`print_info <bumpversion.ui.print_info>`
   - ```{autodoc2-docstring} bumpversion.ui.print_info
     :summary:
@@ -29,8 +37,58 @@
     ```
 ````
 
+### Data
+
+````{list-table}
+:class: autosummary longtable
+:align: left
+
+* - {py:obj}`logger <bumpversion.ui.logger>`
+  - ```{autodoc2-docstring} bumpversion.ui.logger
+    :summary:
+    ```
+* - {py:obj}`VERBOSITY <bumpversion.ui.VERBOSITY>`
+  - ```{autodoc2-docstring} bumpversion.ui.VERBOSITY
+    :summary:
+    ```
+````
+
 ### API
 
+````{py:data} logger
+:canonical: bumpversion.ui.logger
+:value: >
+   'getLogger(...)'
+
+```{autodoc2-docstring} bumpversion.ui.logger
+```
+
+````
+
+````{py:data} VERBOSITY
+:canonical: bumpversion.ui.VERBOSITY
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.ui.VERBOSITY
+```
+
+````
+
+````{py:function} get_indented_logger(name: str) -> bumpversion.indented_logger.IndentedLoggerAdapter
+:canonical: bumpversion.ui.get_indented_logger
+
+```{autodoc2-docstring} bumpversion.ui.get_indented_logger
+```
+````
+
+````{py:function} setup_logging(verbose: int = 0) -> None
+:canonical: bumpversion.ui.setup_logging
+
+```{autodoc2-docstring} bumpversion.ui.setup_logging
+```
+````
+
 ````{py:function} print_info(msg: str) -> None
 :canonical: bumpversion.ui.print_info
 
diff --git a/docsrc/reference/bumpversion/bumpversion.utils.md b/docsrc/reference/bumpversion/bumpversion.utils.md
index 947afaa7..56aa7389 100644
--- a/docsrc/reference/bumpversion/bumpversion.utils.md
+++ b/docsrc/reference/bumpversion/bumpversion.utils.md
@@ -15,6 +15,14 @@
 :class: autosummary longtable
 :align: left
 
+* - {py:obj}`extract_regex_flags <bumpversion.utils.extract_regex_flags>`
+  - ```{autodoc2-docstring} bumpversion.utils.extract_regex_flags
+    :summary:
+    ```
+* - {py:obj}`recursive_sort_dict <bumpversion.utils.recursive_sort_dict>`
+  - ```{autodoc2-docstring} bumpversion.utils.recursive_sort_dict
+    :summary:
+    ```
 * - {py:obj}`key_val_string <bumpversion.utils.key_val_string>`
   - ```{autodoc2-docstring} bumpversion.utils.key_val_string
     :summary:
@@ -35,10 +43,32 @@
   - ```{autodoc2-docstring} bumpversion.utils.get_overrides
     :summary:
     ```
+* - {py:obj}`get_nested_value <bumpversion.utils.get_nested_value>`
+  - ```{autodoc2-docstring} bumpversion.utils.get_nested_value
+    :summary:
+    ```
+* - {py:obj}`set_nested_value <bumpversion.utils.set_nested_value>`
+  - ```{autodoc2-docstring} bumpversion.utils.set_nested_value
+    :summary:
+    ```
 ````
 
 ### API
 
+````{py:function} extract_regex_flags(regex_pattern: str) -> typing.Tuple[str, str]
+:canonical: bumpversion.utils.extract_regex_flags
+
+```{autodoc2-docstring} bumpversion.utils.extract_regex_flags
+```
+````
+
+````{py:function} recursive_sort_dict(input_value: typing.Any) -> typing.Any
+:canonical: bumpversion.utils.recursive_sort_dict
+
+```{autodoc2-docstring} bumpversion.utils.recursive_sort_dict
+```
+````
+
 ````{py:function} key_val_string(d: dict) -> str
 :canonical: bumpversion.utils.key_val_string
 
@@ -73,3 +103,17 @@
 ```{autodoc2-docstring} bumpversion.utils.get_overrides
 ```
 ````
+
+````{py:function} get_nested_value(d: dict, path: str) -> typing.Any
+:canonical: bumpversion.utils.get_nested_value
+
+```{autodoc2-docstring} bumpversion.utils.get_nested_value
+```
+````
+
+````{py:function} set_nested_value(d: dict, value: typing.Any, path: str) -> None
+:canonical: bumpversion.utils.set_nested_value
+
+```{autodoc2-docstring} bumpversion.utils.set_nested_value
+```
+````
diff --git a/docsrc/reference/bumpversion/bumpversion.version_part.md b/docsrc/reference/bumpversion/bumpversion.version_part.md
index 055e4749..4bb79466 100644
--- a/docsrc/reference/bumpversion/bumpversion.version_part.md
+++ b/docsrc/reference/bumpversion/bumpversion.version_part.md
@@ -46,14 +46,14 @@
 ````{py:data} logger
 :canonical: bumpversion.version_part.logger
 :value: >
-   None
+   'get_indented_logger(...)'
 
 ```{autodoc2-docstring} bumpversion.version_part.logger
 ```
 
 ````
 
-`````{py:class} VersionPart(config: bumpversion.config.VersionPartConfig, value: typing.Optional[str] = None)
+`````{py:class} VersionPart(config: bumpversion.config.models.VersionPartConfig, value: typing.Union[str, int, None] = None)
 :canonical: bumpversion.version_part.VersionPart
 
 ```{autodoc2-docstring} bumpversion.version_part.VersionPart
@@ -189,7 +189,7 @@
 
 `````
 
-`````{py:class} VersionConfig(parse: str, serialize: typing.List[str], search: str, replace: str, part_configs: typing.Optional[typing.Dict[str, bumpversion.config.VersionPartConfig]] = None)
+`````{py:class} VersionConfig(parse: str, serialize: typing.Tuple[str], search: str, replace: str, part_configs: typing.Optional[typing.Dict[str, bumpversion.config.models.VersionPartConfig]] = None)
 :canonical: bumpversion.version_part.VersionConfig
 
 ```{autodoc2-docstring} bumpversion.version_part.VersionConfig
@@ -201,6 +201,16 @@
 ```{autodoc2-docstring} bumpversion.version_part.VersionConfig.__init__
 ```
 
+````{py:method} __repr__() -> str
+:canonical: bumpversion.version_part.VersionConfig.__repr__
+
+````
+
+````{py:method} __eq__(other: typing.Any) -> bool
+:canonical: bumpversion.version_part.VersionConfig.__eq__
+
+````
+
 ````{py:property} order
 :canonical: bumpversion.version_part.VersionConfig.order
 :type: typing.List[str]
diff --git a/docsrc/reference/bumpversion/bumpversion.visualize.md b/docsrc/reference/bumpversion/bumpversion.visualize.md
new file mode 100644
index 00000000..e5b4bd71
--- /dev/null
+++ b/docsrc/reference/bumpversion/bumpversion.visualize.md
@@ -0,0 +1,227 @@
+# {py:mod}`bumpversion.visualize`
+
+```{py:module} bumpversion.visualize
+```
+
+```{autodoc2-docstring} bumpversion.visualize
+:allowtitles:
+```
+
+## Module Contents
+
+### Classes
+
+````{list-table}
+:class: autosummary longtable
+:align: left
+
+* - {py:obj}`Border <bumpversion.visualize.Border>`
+  - ```{autodoc2-docstring} bumpversion.visualize.Border
+    :summary:
+    ```
+````
+
+### Functions
+
+````{list-table}
+:class: autosummary longtable
+:align: left
+
+* - {py:obj}`lead_string <bumpversion.visualize.lead_string>`
+  - ```{autodoc2-docstring} bumpversion.visualize.lead_string
+    :summary:
+    ```
+* - {py:obj}`connection_str <bumpversion.visualize.connection_str>`
+  - ```{autodoc2-docstring} bumpversion.visualize.connection_str
+    :summary:
+    ```
+* - {py:obj}`labeled_line <bumpversion.visualize.labeled_line>`
+  - ```{autodoc2-docstring} bumpversion.visualize.labeled_line
+    :summary:
+    ```
+* - {py:obj}`visualize <bumpversion.visualize.visualize>`
+  - ```{autodoc2-docstring} bumpversion.visualize.visualize
+    :summary:
+    ```
+````
+
+### Data
+
+````{list-table}
+:class: autosummary longtable
+:align: left
+
+* - {py:obj}`BOX_CHARS <bumpversion.visualize.BOX_CHARS>`
+  - ```{autodoc2-docstring} bumpversion.visualize.BOX_CHARS
+    :summary:
+    ```
+````
+
+### API
+
+````{py:data} BOX_CHARS
+:canonical: bumpversion.visualize.BOX_CHARS
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.visualize.BOX_CHARS
+```
+
+````
+
+`````{py:class} Border
+:canonical: bumpversion.visualize.Border
+
+```{autodoc2-docstring} bumpversion.visualize.Border
+```
+
+````{py:attribute} corner_bottom_right
+:canonical: bumpversion.visualize.Border.corner_bottom_right
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.visualize.Border.corner_bottom_right
+```
+
+````
+
+````{py:attribute} corner_top_right
+:canonical: bumpversion.visualize.Border.corner_top_right
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.visualize.Border.corner_top_right
+```
+
+````
+
+````{py:attribute} corner_top_left
+:canonical: bumpversion.visualize.Border.corner_top_left
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.visualize.Border.corner_top_left
+```
+
+````
+
+````{py:attribute} corner_bottom_left
+:canonical: bumpversion.visualize.Border.corner_bottom_left
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.visualize.Border.corner_bottom_left
+```
+
+````
+
+````{py:attribute} divider_left
+:canonical: bumpversion.visualize.Border.divider_left
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.visualize.Border.divider_left
+```
+
+````
+
+````{py:attribute} divider_up
+:canonical: bumpversion.visualize.Border.divider_up
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.visualize.Border.divider_up
+```
+
+````
+
+````{py:attribute} divider_down
+:canonical: bumpversion.visualize.Border.divider_down
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.visualize.Border.divider_down
+```
+
+````
+
+````{py:attribute} divider_right
+:canonical: bumpversion.visualize.Border.divider_right
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.visualize.Border.divider_right
+```
+
+````
+
+````{py:attribute} line
+:canonical: bumpversion.visualize.Border.line
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.visualize.Border.line
+```
+
+````
+
+````{py:attribute} pipe
+:canonical: bumpversion.visualize.Border.pipe
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.visualize.Border.pipe
+```
+
+````
+
+````{py:attribute} cross
+:canonical: bumpversion.visualize.Border.cross
+:type: str
+:value: >
+   None
+
+```{autodoc2-docstring} bumpversion.visualize.Border.cross
+```
+
+````
+
+`````
+
+````{py:function} lead_string(version_str: str, border: bumpversion.visualize.Border, blank: bool = False) -> str
+:canonical: bumpversion.visualize.lead_string
+
+```{autodoc2-docstring} bumpversion.visualize.lead_string
+```
+````
+
+````{py:function} connection_str(border: bumpversion.visualize.Border, has_next: bool = False, has_previous: bool = False) -> str
+:canonical: bumpversion.visualize.connection_str
+
+```{autodoc2-docstring} bumpversion.visualize.connection_str
+```
+````
+
+````{py:function} labeled_line(label: str, border: bumpversion.visualize.Border, fit_length: typing.Optional[int] = None) -> str
+:canonical: bumpversion.visualize.labeled_line
+
+```{autodoc2-docstring} bumpversion.visualize.labeled_line
+```
+````
+
+````{py:function} visualize(config: bumpversion.config.Config, version_str: str, box_style: str = 'light') -> None
+:canonical: bumpversion.visualize.visualize
+
+```{autodoc2-docstring} bumpversion.visualize.visualize
+```
+````
diff --git a/docsrc/reference/bumpversion/bumpversion.yaml_dump.md b/docsrc/reference/bumpversion/bumpversion.yaml_dump.md
index d89eafc1..68b4eb8f 100644
--- a/docsrc/reference/bumpversion/bumpversion.yaml_dump.md
+++ b/docsrc/reference/bumpversion/bumpversion.yaml_dump.md
@@ -51,8 +51,8 @@
   - ```{autodoc2-docstring} bumpversion.yaml_dump.format_dict
     :summary:
     ```
-* - {py:obj}`format_list <bumpversion.yaml_dump.format_list>`
-  - ```{autodoc2-docstring} bumpversion.yaml_dump.format_list
+* - {py:obj}`format_sequence <bumpversion.yaml_dump.format_sequence>`
+  - ```{autodoc2-docstring} bumpversion.yaml_dump.format_sequence
     :summary:
     ```
 * - {py:obj}`format_none <bumpversion.yaml_dump.format_none>`
@@ -128,7 +128,7 @@ Bases: {py:obj}`collections.UserDict`
 ````{py:data} YAML_DUMPERS
 :canonical: bumpversion.yaml_dump.YAML_DUMPERS
 :value: >
-   None
+   'YAMLDumpers(...)'
 
 ```{autodoc2-docstring} bumpversion.yaml_dump.YAML_DUMPERS
 ```
@@ -187,10 +187,10 @@ Bases: {py:obj}`collections.UserDict`
 ```
 ````
 
-````{py:function} format_list(val: list) -> str
-:canonical: bumpversion.yaml_dump.format_list
+````{py:function} format_sequence(val: typing.Union[list, tuple]) -> str
+:canonical: bumpversion.yaml_dump.format_sequence
 
-```{autodoc2-docstring} bumpversion.yaml_dump.format_list
+```{autodoc2-docstring} bumpversion.yaml_dump.format_sequence
 ```
 ````
 
diff --git a/docsrc/tutorials/semantic-versioning-example.md b/docsrc/tutorials/semantic-versioning-example.md
index 783bc9dc..d69d91fc 100644
--- a/docsrc/tutorials/semantic-versioning-example.md
+++ b/docsrc/tutorials/semantic-versioning-example.md
@@ -1,44 +1,201 @@
 # Semantic versioning example
 
-bumpversion flow:
+## Create a default configuration
+
+The default configuration uses a simplified version of [semantic versioning](https://semver.org/).
+
+```{code-block} console
+:caption: Generating a default configuration
+
+$ bump-my-version sample-config --no-prompt --destination .bumpversion.toml
+$ cat .bumpversion.toml
+[tool.bumpversion]
+current_version = "0.1.0"
+parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
+serialize = ["{major}.{minor}.{patch}"]
+search = "{current_version}"
+replace = "{new_version}"
+regex = false
+ignore_missing_version = false
+tag = false
+sign_tags = false
+tag_name = "v{new_version}"
+tag_message = "Bump version: {current_version} → {new_version}"
+allow_dirty = false
+commit = false
+message = "Bump version: {current_version} → {new_version}"
+commit_args = ""
+```
+
+### Visualizing the versioning path
+
+You can see the potential versioning paths with the `show-bump` subcommand.
+
+```{code-block} console
+:caption: Showing the potential versioning path
+
+$ bump-my-version show-bump
+0.1.0 ── bump ─┬─ major ─ 1.0.0
+               ├─ minor ─ 0.2.0
+               ╰─ patch ─ 0.1.1
+$ bump-my-version show-bump 1.2.3
+1.2.3 ── bump ─┬─ major ─ 2.0.0
+               ├─ minor ─ 1.3.0
+               ╰─ patch ─ 1.2.4
+```
+
+The default configuration only allows bumping the major, minor, or patch version. What if you wanted to support pre-release versions?
+
+## Supporting pre-release versions
+
+Alter the `parse` configuration to support pre-release versions. This `parse` option uses an extended (or verbose) regular expression to extract the version parts from the current version. 
+
+```{code-block} toml
+:caption: New parse configuration
+
+parse = """(?x)
+    (?P<major>0|[1-9]\\d*)\\.
+    (?P<minor>0|[1-9]\\d*)\\.
+    (?P<patch>0|[1-9]\\d*)
+    (?:
+        -                             # dash seperator for pre-release section
+        (?P<pre_l>[a-zA-Z-]+)         # pre-release label
+        (?P<pre_n>0|[1-9]\\d*)        # pre-release version number
+    )?                                # pre-release section is optional
+"""
+```
+
+Alter the `serialize` configuration to support pre-release versions.
 
-    1.0.0 => 1.0.1-dev1 => 1.0.1-dev2 = > 1.0.1-rc1 => 1.0.1-rc2 => 1.0.1
-             patch         build          release      build        release
-
-## Details
-
-Start with an initial release, say `1.0.0`.
-
-1. Create a new release, starting with a development build.
-
-        $ bumpversion patch
-        => 1.0.1-dev1
-
-2. Every time you build, bump `build`.
-
-        $ bumpversion build
-        => 1.0.1-dev2
-
-3. Go to release candidate by bumping `release`.
-
-        $ bumpversion release
-        => 1.0.1-rc1
-
-4. With every new build, bump `build`.
-
-        $ bumpversion build
-        => 1.0.1-rc2
-
-4. Finally, bump `release` to generate a final release for the current
-   `major` / `minor` / `patch` version.
-
-        $ bumpversion release
-        => 1.0.1
-
-
-## Notes
-
-*  Once the final release has been reached, it is not possible to bump
-   the `release` before bumping `patch` again. Trying to bump the release
-   while in final release state will issue
-   `ValueError: The part has already the maximum value among ['dev', 'rc', 'ga'] and cannot be bumped`.
+```{code-block} toml
+:caption: New serialize configuration
+
+serialize = [
+    "{major}.{minor}.{patch}-{pre_l}{pre_n}",
+    "{major}.{minor}.{patch}",
+]
+```
+
+Add a new configuration section for the `pre_l` part.
+
+```{code-block} toml
+:caption: New pre_l configuration
+
+[tool.bumpversion.parts.pre_l]
+values = ["dev", "rc", "final"]
+optional_value = "final"
+```
+
+### Visualizing the new versioning path
+
+Now when you run `bump-my-version show-bump`, you can see the new pre-release versioning path.
+
+```{code-block} console
+:caption: Showing the new versioning path
+
+$ bump-my-version show-bump
+0.1.0 ── bump ─┬─ major ─ 1.0.0-dev0
+               ├─ minor ─ 0.2.0-dev0
+               ├─ patch ─ 0.1.1-dev0
+               ├─ pre_l ─ invalid: The part has already the maximum value among ['dev', 'rc', 'final'] and cannot be bumped.
+               ╰─ pre_n ─ 0.1.0-final1
+```
+
+The `pre_l` is not bump-able because it is already at the maximum value. The `pre_n` is bump-able because it is not at the maximum value.
+
+If we run `bump-my-version show-bump 1.0.0-dev0`, we can see the new versioning path for a `dev` starting version.
+
+```{code-block} console
+:caption: Showing the new versioning path for a dev version
+
+$ bump-my-version show-bump 1.0.0-dev0
+1.0.0-dev0 ── bump ─┬─ major ─ 2.0.0-dev0
+                    ├─ minor ─ 1.1.0-dev0
+                    ├─ patch ─ 1.0.1-dev0
+                    ├─ pre_l ─ 1.0.0-rc0
+                    ╰─ pre_n ─ 1.0.0-dev1
+```
+
+Finally, we can see the new versioning path for a `rc` starting version.
+
+```{code-block} console
+:caption: Showing the new versioning path for an rc version
+
+$ bump-my-version show-bump 1.0.0-rc0 
+1.0.0-rc0 ── bump ─┬─ major ─ 2.0.0-dev0
+                   ├─ minor ─ 1.1.0-dev0
+                   ├─ patch ─ 1.0.1-dev0
+                   ├─ pre_l ─ 1.0.0
+                   ╰─ pre_n ─ 1.0.0-rc1
+```
+
+The full development and release path is:
+
+- `1.0.0`
+- `bump patch` → `1.0.1-dev0`
+- `bump pre_n` → `1.0.1-dev1`
+- `bump pre_l` → `1.0.1-rc0`
+- `bump pre_n` → `1.0.1-rc1`
+- `bump pre_l` → `1.0.1`
+
+1. You must decide on the next version before you start developing.
+2. Development versions increase using `bump-my-version bump pre_n`.
+3. Switch from development to release candidate using `bump-my-version bump pre_l`.
+4. Release candidates increase using `bump-my-version bump pre_n`.
+5. Switch from the release candidate to the final release using `bump-my-version bump pre_l`.
+
+## Automating the pre-release numbering
+
+The `pre_n` or pre-release number is a number that increases with each pre-release. You can automate this my changing the serialization configuration.
+
+```{code-block} toml
+:caption: Serialize configuration with pre_n automation
+
+serialize = [
+    "{major}.{minor}.{patch}-{pre_l}{distance_to_latest_tag}",
+    "{major}.{minor}.{patch}",
+]
+```
+
+The `distance_to_latest_tag` is a special value that is replaced with the number of commits since the last tag. This is a good value to use for the `pre_n` because it will always increase with each commit.
+
+### Visualizing the pre_n versioning path
+
+Now when you run `bump-my-version show-bump`, you can see the new pre-release versioning path.
+
+```{code-block} console
+:caption: Showing the new versioning path with pre_n automation
+
+$ bump-my-version show-bump
+0.1.0 ── bump ─┬─ major ─ 1.0.0-dev0
+               ├─ minor ─ 0.2.0-dev0
+               ├─ patch ─ 0.1.1-dev0
+               ╰─ pre_l ─ invalid: The part has already the maximum value among ['dev', 'rc', 'final'] and cannot be bumped.
+$ bump-my-version show-bump 1.0.0-dev0
+1.0.0-dev0 ── bump ─┬─ major ─ 2.0.0-dev0
+                    ├─ minor ─ 1.1.0-dev0
+                    ├─ patch ─ 1.0.1-dev0
+                    ╰─ pre_l ─ 1.0.0-rc0
+$ bump-my-version show-bump 1.0.0-rc0 
+1.0.0-rc0 ── bump ─┬─ major ─ 2.0.0-dev0
+                   ├─ minor ─ 1.1.0-dev0
+                   ├─ patch ─ 1.0.1-dev0
+                   ╰─ pre_l ─ 1.0.0
+ ```
+
+The `pre_n` path is now missing because it is automated.
+
+The full development and release path now is:
+
+- `1.0.0`
+- `bump patch` → `1.0.1-dev0`
+- each commit will increase → `1.0.1-dev1`
+- `bump pre_l` → `1.0.1-rc0`
+- each commit will increase → `1.0.1-rc1`
+- `bump pre_l` → `1.0.1`
+
+1. You must decide on the next version before you start developing.
+2. Development versions increase automatically with each commit.
+3. Switch from development to release candidate using `bump-my-version bump pre_l`.
+4. Release candidate versions increase automatically with each commit.
+5. Switch from the release candidate to the final release using `bump-my-version bump pre_l`.
diff --git a/docsrc/usage.md b/docsrc/usage.md
index a810fdf6..e6a70819 100644
--- a/docsrc/usage.md
+++ b/docsrc/usage.md
@@ -111,9 +111,9 @@ The `replace` sub-command works identically to the `bump` sub-command except for
 If you do not include the `--new-version` option, the `new_version` context variable will be `None`.
 ```
 
-One way of providing the `--new-version` option is to use the `bumpversion show` subcommand with an environment variable:
+One way of providing the `--new-version` option is to use the `bump-my-version show` subcommand with an environment variable:
 
 ```console
-$ export BUMPVERSION_NEW_VERSION=$(bumpversion show new_version --increment <versionpart>)
-$ bumpversion replace
+$ export BUMPVERSION_NEW_VERSION=$(bump-my-version show new_version --increment <versionpart>)
+$ bump-my-version replace
 ```
diff --git a/pyproject.toml b/pyproject.toml
index adec0f60..6f709a27 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -35,6 +35,7 @@ dependencies = [
     "click",
     "pydantic>=2.0.0",
     "pydantic-settings",
+    "questionary",
     "rich-click",
     "rich",
     "tomlkit",


Test patch:
diff --git a/tests/fixtures/basic_cfg_expected.txt b/tests/fixtures/basic_cfg_expected.txt
index b1d24f9d..2ec6970b 100644
--- a/tests/fixtures/basic_cfg_expected.txt
+++ b/tests/fixtures/basic_cfg_expected.txt
@@ -59,7 +59,7 @@
               'commit_sha': None,
               'current_version': None,
               'dirty': None,
-              'distance_to_latest_tag': None,
+              'distance_to_latest_tag': 0,
               'short_branch_name': None,
               'tool': None},
  'search': '{current_version}',
diff --git a/tests/fixtures/basic_cfg_expected.yaml b/tests/fixtures/basic_cfg_expected.yaml
index 28e53dbe..39f140d4 100644
--- a/tests/fixtures/basic_cfg_expected.yaml
+++ b/tests/fixtures/basic_cfg_expected.yaml
@@ -73,7 +73,7 @@ scm_info:
   commit_sha: null
   current_version: null
   dirty: null
-  distance_to_latest_tag: null
+  distance_to_latest_tag: 0
   short_branch_name: null
   tool: null
 search: "{current_version}"
diff --git a/tests/fixtures/basic_cfg_expected_full.json b/tests/fixtures/basic_cfg_expected_full.json
index b7baefe3..bb5cac7b 100644
--- a/tests/fixtures/basic_cfg_expected_full.json
+++ b/tests/fixtures/basic_cfg_expected_full.json
@@ -88,7 +88,7 @@
     "commit_sha": null,
     "current_version": null,
     "dirty": null,
-    "distance_to_latest_tag": null,
+    "distance_to_latest_tag": 0,
     "short_branch_name": null,
     "tool": null
   },
diff --git a/tests/test_cli.py b/tests/test_cli.py
deleted file mode 100644
index ba5b4654..00000000
--- a/tests/test_cli.py
+++ /dev/null
@@ -1,644 +0,0 @@
-"""Tests for `bumpversion` package."""
-import shutil
-import subprocess
-import traceback
-from datetime import datetime
-from pathlib import Path
-
-import pytest
-from click.testing import CliRunner, Result
-from pytest import param
-
-from bumpversion import __version__, cli
-from tests.conftest import inside_dir
-
-
-def test_version_displays_library_version():
-    """
-    Arrange/Act: Run the `version` subcommand.
-
-    Assert: The output matches the library version.
-    """
-    runner: CliRunner = CliRunner()
-    result: Result = runner.invoke(cli.cli, ["--version"])
-    assert __version__ in result.output.strip(), "Version number should match library version."
-
-
-def test_bump_no_configured_files(mocker, tmp_path):
-    """
-    Arrange/Act: Run the `bump` subcommand with --no-configured-files.
-
-    Assert: There is no configured files specified to modify
-    """
-    mocked_do_bump = mocker.patch("bumpversion.cli.do_bump")
-    runner: CliRunner = CliRunner()
-    with inside_dir(tmp_path):
-        result: Result = runner.invoke(
-            cli.cli, ["bump", "--current-version", "1.0.0", "--no-configured-files", "patch"]
-        )
-
-    if result.exit_code != 0:
-        print(result.output)
-
-    assert result.exit_code == 0
-
-    call_args = mocked_do_bump.call_args[0]
-    assert len(call_args[2].files) == 0
-
-
-def test_bump_nested_regex(tmp_path: Path, fixtures_path: Path, caplog):
-    """
-    Arrange/Act: Run the `bump` subcommand with --no-configured-files.
-
-    Assert: There is no configured files specified to modify
-    """
-    cff_path = tmp_path / "citation.cff"
-    cff_path.write_text("cff-version: 1.2.0\ndate-released: 2023-09-19\n")
-    content = fixtures_path.joinpath("regex_test_config.toml").read_text()
-    config_path = tmp_path / ".bumpversion.toml"
-    config_path.write_text(content)
-
-    runner: CliRunner = CliRunner()
-    with inside_dir(tmp_path):
-        result: Result = runner.invoke(cli.cli, ["bump", "-vv", "patch"])
-
-    if result.exit_code != 0:
-        print(result.output)
-        print(caplog.text)
-
-    assert result.exit_code == 0
-
-    now = datetime.now().isoformat()[:10]
-    assert cff_path.read_text() == f"cff-version: 1.2.0\ndate-released: {now}\n"
-
-
-def test_bump_legacy(mocker, tmp_path):
-    """
-    Arrange/Act: Run the `bump` subcommand with --no-configured-files.
-
-    Assert: There is no configured files specified to modify
-    """
-    mocked_do_bump = mocker.patch("bumpversion.cli.do_bump")
-    runner: CliRunner = CliRunner()
-    with inside_dir(tmp_path):
-        result: Result = runner.invoke(cli.cli, ["--current-version", "1.0.0", "--no-configured-files", "patch"])
-
-    if result.exit_code != 0:
-        print(result.output)
-
-    assert result.exit_code == 0
-
-    call_args = mocked_do_bump.call_args[0]
-    assert len(call_args[2].files) == 0
-
-
-def test_no_configured_files_still_file_args_work(mocker, tmp_path):
-    mocked_do_bump = mocker.patch("bumpversion.cli.do_bump")
-    runner: CliRunner = CliRunner()
-    with inside_dir(tmp_path):
-        result: Result = runner.invoke(
-            cli.cli, ["bump", "--current-version", "1.0.0", "--no-configured-files", "patch", "do-this-file.txt"]
-        )
-
-    if result.exit_code != 0:
-        print(result.output)
-
-    assert result.exit_code == 0
-
-    call_args = mocked_do_bump.call_args[0]
-    assert len(call_args[2].files) == 1
-    assert call_args[2].files[0].filename == "do-this-file.txt"
-
-
-def test_missing_explicit_config_file(tmp_path: Path):
-    """The command-line processor should raise an exception if the config file is missing."""
-    with inside_dir(tmp_path):
-        runner: CliRunner = CliRunner()
-        with inside_dir(tmp_path):
-            result: Result = runner.invoke(cli.cli, ["bump", "--config-file", "missing-file.cfg"])
-        assert result.exit_code != 0
-        assert "'missing-file.cfg' does not exist." in result.output
-
-
-def test_cli_options_override_config(tmp_path: Path, fixtures_path: Path, mocker):
-    """The command-line processor should override the config file."""
-    # Arrange
-    config_path = tmp_path / "this_config.toml"
-    fixture_toml = fixtures_path / "basic_cfg.toml"
-    shutil.copy(fixture_toml, config_path)
-    runner: CliRunner = CliRunner()
-    mocked_do_bump = mocker.patch("bumpversion.cli.do_bump")
-
-    # Act
-    with inside_dir(tmp_path):
-        result: Result = runner.invoke(
-            cli.cli,
-            [
-                "bump",
-                "--config-file",
-                str(config_path),
-                "--current-version",
-                "1.1.0",
-                "--new-version",
-                "1.2.0",
-                "--allow-dirty",
-                "--parse",
-                r"XXX(?P<spam>\d+);(?P<blob>\d+);(?P<slurp>\d+)",
-                "--serialize",
-                "XXX{spam};{blob};{slurp}",
-                "--search",
-                "my-search",
-                "--replace",
-                "my-replace",
-                "--no-commit",
-                "--no-tag",
-                "slurp",
-                "do-this-file.txt",
-            ],
-        )
-
-    # Assert
-    assert result.exit_code == 0
-    assert mocked_do_bump.call_count == 1
-    assert mocked_do_bump.call_args[0][0] == "slurp"
-    assert mocked_do_bump.call_args[0][1] == "1.2.0"
-    the_config = mocked_do_bump.call_args[0][2]
-    assert the_config.current_version == "1.1.0"
-    assert the_config.allow_dirty
-    assert the_config.parse == r"XXX(?P<spam>\d+);(?P<blob>\d+);(?P<slurp>\d+)"
-    assert the_config.serialize == ("XXX{spam};{blob};{slurp}",)
-    assert the_config.search == "my-search"
-    assert the_config.replace == "my-replace"
-    assert the_config.commit is False
-    assert the_config.tag is False
-    assert len(the_config.files) == 4
-    assert {f.filename for f in the_config.files} == {
-        "setup.py",
-        "bumpversion/__init__.py",
-        "CHANGELOG.md",
-        "do-this-file.txt",
-    }
-    assert mocked_do_bump.call_args[0][3] == config_path
-
-
-@pytest.mark.parametrize(
-    ["repo", "scm_command"],
-    [
-        param("git_repo", "git", id="git"),
-        param("hg_repo", "hg", id="hg"),
-    ],
-)
-def test_dirty_work_dir_raises_error(repo: str, scm_command: str, request):
-    repo_path: Path = request.getfixturevalue(repo)
-    with inside_dir(repo_path):
-        # Arrange
-        repo_path.joinpath("dirty2").write_text("i'm dirty! 1.1.1")
-        subprocess.run([scm_command, "add", "dirty2"], check=True)
-        runner: CliRunner = CliRunner()
-
-        # Act
-        result: Result = runner.invoke(
-            cli.cli, ["bump", "patch", "--current-version", "1.1.1", "--no-allow-dirty", "dirty2"]
-        )
-
-    # Assert
-    assert result.exit_code != 0
-    assert "working directory is not clean" in result.output
-
-
-def test_listing_with_version_part(tmp_path: Path, fixtures_path: Path):
-    """The --list option should list the configuration with new_version."""
-    # Arrange
-    config_path = tmp_path / "pyproject.toml"
-    toml_path = fixtures_path / "basic_cfg.toml"
-    shutil.copy(toml_path, config_path)
-    runner: CliRunner = CliRunner()
-
-    with inside_dir(tmp_path):
-        result: Result = runner.invoke(cli.cli, ["bump", "--list", "patch"])
-
-    if result.exit_code != 0:
-        print(result.output)
-        print(result.exception)
-
-    assert result.exit_code == 0
-    assert set(result.output.splitlines(keepends=False)) == {
-        "WARNING:",
-        "",
-        "DEPRECATED: The --list option is deprecated and will be removed in a future version.",
-        "new_version=1.0.1-dev",
-        "current_version=1.0.0",
-        "excluded_paths=[]",
-        "parse=(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",
-        "serialize=('{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}')",
-        "search={current_version}",
-        "replace={new_version}",
-        "regex=False",
-        "ignore_missing_version=False",
-        "included_paths=[]",
-        "tag=True",
-        "sign_tags=False",
-        "tag_name=v{new_version}",
-        "tag_message=Bump version: {current_version} → {new_version}",
-        "allow_dirty=False",
-        "commit=True",
-        "message=Bump version: {current_version} → {new_version}",
-        "commit_args=None",
-        (
-            "files=[{'parse': "
-            "'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
-            "'serialize': ('{major}.{minor}.{patch}-{release}', "
-            "'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': "
-            "'{new_version}', 'regex': False, 'ignore_missing_version': False, "
-            "'filename': 'setup.py', 'glob': None, 'key_path': None}, {'parse': "
-            "'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
-            "'serialize': ('{major}.{minor}.{patch}-{release}', "
-            "'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': "
-            "'{new_version}', 'regex': False, 'ignore_missing_version': False, "
-            "'filename': 'bumpversion/__init__.py', 'glob': None, 'key_path': None}, "
-            "{'parse': "
-            "'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
-            "'serialize': ('{major}.{minor}.{patch}-{release}', "
-            "'{major}.{minor}.{patch}'), 'search': '**unreleased**', 'replace': "
-            "'**unreleased**\\n**v{new_version}**', 'regex': False, "
-            "'ignore_missing_version': False, 'filename': 'CHANGELOG.md', 'glob': None, "
-            "'key_path': None}]"
-        ),
-    }
-
-
-def test_listing_without_version_part(tmp_path: Path, fixtures_path: Path):
-    """The --list option should list the configuration without new_version."""
-    # Arrange
-    config_path = tmp_path / "pyproject.toml"
-    toml_path = fixtures_path / "basic_cfg.toml"
-    shutil.copy(toml_path, config_path)
-    runner: CliRunner = CliRunner()
-
-    with inside_dir(tmp_path):
-        result: Result = runner.invoke(cli.cli, ["bump", "--list"])
-
-    if result.exit_code != 0:
-        print(result.output)
-        print(result.exception)
-
-    assert result.exit_code == 0
-    assert set(result.output.splitlines(keepends=False)) == {
-        "WARNING:",
-        "",
-        "DEPRECATED: The --list option is deprecated and will be removed in a future version.",
-        "current_version=1.0.0",
-        "excluded_paths=[]",
-        "parse=(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",
-        "serialize=('{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}')",
-        "search={current_version}",
-        "replace={new_version}",
-        "regex=False",
-        "ignore_missing_version=False",
-        "included_paths=[]",
-        "tag=True",
-        "sign_tags=False",
-        "tag_name=v{new_version}",
-        "tag_message=Bump version: {current_version} → {new_version}",
-        "allow_dirty=False",
-        "commit=True",
-        "message=Bump version: {current_version} → {new_version}",
-        "commit_args=None",
-        (
-            "files=[{'parse': "
-            "'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
-            "'serialize': ('{major}.{minor}.{patch}-{release}', "
-            "'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': "
-            "'{new_version}', 'regex': False, 'ignore_missing_version': False, "
-            "'filename': 'setup.py', 'glob': None, 'key_path': None}, {'parse': "
-            "'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
-            "'serialize': ('{major}.{minor}.{patch}-{release}', "
-            "'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': "
-            "'{new_version}', 'regex': False, 'ignore_missing_version': False, "
-            "'filename': 'bumpversion/__init__.py', 'glob': None, 'key_path': None}, "
-            "{'parse': "
-            "'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
-            "'serialize': ('{major}.{minor}.{patch}-{release}', "
-            "'{major}.{minor}.{patch}'), 'search': '**unreleased**', 'replace': "
-            "'**unreleased**\\n**v{new_version}**', 'regex': False, "
-            "'ignore_missing_version': False, 'filename': 'CHANGELOG.md', 'glob': None, "
-            "'key_path': None}]"
-        ),
-    }
-
-
-def test_non_scm_operations_if_scm_not_installed(tmp_path: Path, monkeypatch):
-    """Everything works except SCM commands if the SCM is unusable."""
-    # Arrange
-    monkeypatch.setenv("PATH", "")
-
-    with inside_dir(tmp_path):
-        version_path = tmp_path / "VERSION"
-        version_path.write_text("31.0.3")
-
-        runner: CliRunner = CliRunner()
-
-        # Act
-        runner.invoke(cli.cli, ["bump", "major", "--current-version", "31.0.3", "VERSION"])
-
-    # Assert
-    assert version_path.read_text() == "32.0.0"
-
-
-@pytest.mark.parametrize(
-    ["version_part"],
-    [
-        param("charlie", id="bad_version_part"),
-        param("", id="missing_version_part"),
-    ],
-)
-def test_detects_bad_or_missing_version_part(version_part: str, tmp_path: Path, monkeypatch):
-    """It properly detects bad or missing version part."""
-    # Arrange
-    monkeypatch.setenv("PATH", "")
-
-    with inside_dir(tmp_path):
-        version_path = tmp_path / "VERSION"
-        version_path.write_text("31.0.3")
-
-        runner: CliRunner = CliRunner()
-        args = ["bump", "--current-version", "31.0.3"]
-        if version_part:
-            args.append(version_part)
-        args.append("VERSION")
-        # Act
-        result = runner.invoke(cli.cli, args)
-
-    # Assert
-    assert result.exception is not None
-    assert "Unknown version part:" in result.stdout
-
-
-def test_show(tmp_path: Path, fixtures_path: Path):
-    """The show subcommand should list the parts of the configuration."""
-    # Arrange
-    config_path = tmp_path / "pyproject.toml"
-    toml_path = fixtures_path / "basic_cfg.toml"
-    shutil.copy(toml_path, config_path)
-    runner: CliRunner = CliRunner()
-
-    with inside_dir(tmp_path):
-        result: Result = runner.invoke(cli.cli, ["show", "current_version"])
-
-    if result.exit_code != 0:
-        print(result.output)
-        print(result.exception)
-
-    assert result.exit_code == 0
-    assert result.output.splitlines(keepends=False) == ["1.0.0"]
-
-
-def test_show_and_increment(tmp_path: Path, fixtures_path: Path):
-    """The show subcommand should incrment the version and display it."""
-    # Arrange
-    config_path = tmp_path / "pyproject.toml"
-    toml_path = fixtures_path / "basic_cfg.toml"
-    shutil.copy(toml_path, config_path)
-    runner: CliRunner = CliRunner()
-
-    with inside_dir(tmp_path):
-        result: Result = runner.invoke(cli.cli, ["show", "new_version", "--increment", "minor"])
-
-    if result.exit_code != 0:
-        print(result.output)
-        print(result.exception)
-
-    assert result.exit_code == 0
-    assert result.output.splitlines(keepends=False) == ["1.1.0-dev"]
-
-
-def test_show_no_args(tmp_path: Path, fixtures_path: Path):
-    """The show subcommand should list the entire configuration."""
-    # Arrange
-    config_path = tmp_path / "pyproject.toml"
-    toml_path = fixtures_path / "basic_cfg.toml"
-    expected_output = fixtures_path.joinpath("basic_cfg_expected.yaml").read_text()
-
-    shutil.copy(toml_path, config_path)
-    runner: CliRunner = CliRunner()
-
-    with inside_dir(tmp_path):
-        result: Result = runner.invoke(cli.cli, ["show", "--format", "yaml"])
-
-    if result.exit_code != 0:
-        print(result.output)
-        print(result.exception)
-
-    assert result.exit_code == 0
-    assert result.output.strip() == expected_output.strip()
-
-
-def test_replace(mocker, tmp_path, fixtures_path):
-    """The replace subcommand should replace the version in the file."""
-    # Arrange
-    toml_path = fixtures_path / "basic_cfg.toml"
-    config_path = tmp_path / "pyproject.toml"
-    shutil.copy(toml_path, config_path)
-
-    mocked_modify_files = mocker.patch("bumpversion.cli.modify_files")
-    runner: CliRunner = CliRunner()
-    with inside_dir(tmp_path):
-        result: Result = runner.invoke(cli.cli, ["replace", "--new-version", "1.1.0"])
-
-    if result.exit_code != 0:
-        print(result.output)
-
-    assert result.exit_code == 0
-
-    call_args = mocked_modify_files.call_args[0]
-    configured_files = call_args[0]
-    assert len(configured_files) == 3
-    actual_filenames = {f.file_change.filename for f in configured_files}
-    assert actual_filenames == {"setup.py", "CHANGELOG.md", "bumpversion/__init__.py"}
-
-
-def test_replace_no_newversion(mocker, tmp_path, fixtures_path):
-    """The replace subcommand should set new_version to None in the context."""
-    # Arrange
-    toml_path = fixtures_path / "basic_cfg.toml"
-    config_path = tmp_path / "pyproject.toml"
-    shutil.copy(toml_path, config_path)
-
-    mocked_modify_files = mocker.patch("bumpversion.cli.modify_files")
-    runner: CliRunner = CliRunner()
-    with inside_dir(tmp_path):
-        result: Result = runner.invoke(cli.cli, ["replace"])
-
-    if result.exit_code != 0:
-        print(result.output)
-
-    assert result.exit_code == 0
-
-    call_args = mocked_modify_files.call_args[0]
-    assert call_args[2] is None
-
-
-def test_replace_specific_files(mocker, git_repo, fixtures_path):
-    """The replace subcommand should set the files to only the specified files."""
-    # Arrange
-    toml_path = fixtures_path / "basic_cfg.toml"
-    config_path = git_repo / "pyproject.toml"
-    shutil.copy(toml_path, config_path)
-
-    mocked_modify_files = mocker.patch("bumpversion.cli.modify_files")
-    runner: CliRunner = CliRunner()
-    with inside_dir(git_repo):
-        result: Result = runner.invoke(cli.cli, ["replace", "--no-configured-files", "VERSION"])
-
-    if result.exit_code != 0:
-        print(result.output)
-
-    assert result.exit_code == 0
-
-    call_args = mocked_modify_files.call_args[0]
-    configured_files = call_args[0]
-    assert len(configured_files) == 1
-    assert configured_files[0].file_change.filename == "VERSION"
-
-
-TEST_REPLACE_CONFIG = {
-    "tool": {
-        "bumpversion": {
-            "allow_dirty": True,
-            "commit": False,
-            "current_version": "2.17.7",
-            "files": [],
-            "message": "Bump version: {current_version} → {new_version}",
-            "parse": "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)",
-            "parts": {
-                "major": {},
-                "minor": {},
-                "patch": {},
-            },
-            "replace": "{new_version}",
-            "search": "{current_version}",
-            "serialize": ["{major}.{minor}.{patch}"],
-            "sign_tags": False,
-            "tag": False,
-            "tag_message": "Bump version: {current_version} → {new_version}",
-            "tag_name": "v{new_version}",
-        }
-    }
-}
-
-
-def test_replace_search_with_plain_string(tmp_path, fixtures_path):
-    """Replace should not worry if the search or replace values have version info."""
-    from tomlkit import dumps
-
-    # Arrange
-    config_path = tmp_path / "pyproject.toml"
-    config_path.write_text(dumps(TEST_REPLACE_CONFIG))
-    doc_path = tmp_path / "docs.yaml"
-    doc_path.write_text("url: https://github.com/sampleuser/workflows/main/.github/update_mailmap.py")
-
-    runner: CliRunner = CliRunner()
-    with inside_dir(tmp_path):
-        result: Result = runner.invoke(
-            cli.cli,
-            [
-                "replace",
-                "--no-configured-files",
-                "--search",
-                "/workflows/main/",
-                "--replace",
-                "/workflows/v{current_version}/",
-                "./docs.yaml",
-            ],
-        )
-
-    if result.exit_code != 0:
-        print("Here is the output:")
-        print(result.output)
-        print(traceback.print_exception(result.exc_info[1]))
-
-    assert result.exit_code == 0
-
-
-def test_replace_with_empty_string(tmp_path, fixtures_path):
-    """Replace should not worry if the replace value has version info."""
-    from tomlkit import dumps
-
-    # Arrange
-    config_path = tmp_path / "pyproject.toml"
-    config_path.write_text(dumps(TEST_REPLACE_CONFIG))
-    doc_path = tmp_path / "docs.yaml"
-    doc_path.write_text("We should censor profanity\n\n")
-
-    runner: CliRunner = CliRunner()
-    with inside_dir(tmp_path):
-        result: Result = runner.invoke(
-            cli.cli,
-            [
-                "replace",
-                "--verbose",
-                "--no-configured-files",
-                "--allow-dirty",
-                "--search",
-                "profanity",
-                "--replace",
-                "",
-                "./docs.yaml",
-            ],
-        )
-
-    if result.exit_code != 0:
-        print("Here is the output:")
-        print(result.output)
-        print(traceback.print_exception(result.exc_info[1]))
-    assert result.exit_code == 0
-    assert doc_path.read_text() == "We should censor \n\n"
-
-
-def test_valid_regex_not_ignoring_regex(tmp_path: Path, caplog) -> None:
-    """A search string not meant to be a regex (but is) is still found and replaced correctly."""
-    # Arrange
-    search = "(unreleased)"
-    replace = "(2023-01-01)"
-
-    version_path = tmp_path / "VERSION"
-    version_path.write_text("# Changelog\n\n## [0.0.1 (unreleased)](https://cool.url)\n\n- Test unreleased package.\n")
-    config_file = tmp_path / ".bumpversion.toml"
-    config_file.write_text(
-        "[tool.bumpversion]\n"
-        'current_version = "0.0.1"\n'
-        "allow_dirty = true\n\n"
-        "[[tool.bumpversion.files]]\n"
-        'filename = "VERSION"\n'
-        "regex = false\n"
-        f'search = "{search}"\n'
-        f'replace = "{replace}"\n'
-    )
-
-    # Act
-    runner: CliRunner = CliRunner()
-    with inside_dir(tmp_path):
-        result: Result = runner.invoke(
-            cli.cli,
-            [
-                "replace",
-                "--verbose",
-                "--no-regex",
-                "--no-configured-files",
-                "--search",
-                search,
-                "--replace",
-                replace,
-                "VERSION",
-            ],
-        )
-
-    # Assert
-    if result.exit_code != 0:
-        print(result.output)
-
-    assert result.exit_code == 0
-    assert (
-        version_path.read_text()
-        == "# Changelog\n\n## [0.0.1 (2023-01-01)](https://cool.url)\n\n- Test unreleased package.\n"
-    )
diff --git a/tests/test_cli/__init__.py b/tests/test_cli/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test_cli/test_bump.py b/tests/test_cli/test_bump.py
new file mode 100644
index 00000000..f7a3ff89
--- /dev/null
+++ b/tests/test_cli/test_bump.py
@@ -0,0 +1,349 @@
+"""Tests the bump CLI subcommand."""
+import shutil
+import subprocess
+from datetime import datetime
+from pathlib import Path
+
+import pytest
+from pytest import param
+
+from click.testing import CliRunner, Result
+
+from bumpversion import cli
+from tests.conftest import inside_dir
+
+
+class TestNoConfiguredFilesOption:
+    """Tests around the behavior of the --no-configured-files option."""
+
+    def test_no_files_marked_to_modify(self, mocker, tmp_path: Path):
+        """
+        No files are sent to the `do_bump` function if the --no-configured-files option is used.
+        """
+        mocked_do_bump = mocker.patch("bumpversion.cli.do_bump")
+        runner: CliRunner = CliRunner()
+        with inside_dir(tmp_path):
+            result: Result = runner.invoke(
+                cli.cli, ["bump", "--current-version", "1.0.0", "--no-configured-files", "patch"]
+            )
+
+        if result.exit_code != 0:
+            print(result.output)
+
+        assert result.exit_code == 0
+
+        call_args = mocked_do_bump.call_args[0]
+        passed_config = call_args[2]
+        assert len(passed_config.files) == 0
+
+    def test_file_args_marked_to_modify(self, mocker, tmp_path: Path):
+        """File paths marked in the command-line arguments are sent to the `do_bump` function."""
+        mocked_do_bump = mocker.patch("bumpversion.cli.do_bump")
+        runner: CliRunner = CliRunner()
+        with inside_dir(tmp_path):
+            result: Result = runner.invoke(
+                cli.cli,
+                ["bump", "--current-version", "1.0.0", "--no-configured-files", "patch", "do-this-file.txt"],
+            )
+
+        if result.exit_code != 0:
+            print(result.output)
+
+        assert result.exit_code == 0
+
+        call_args = mocked_do_bump.call_args[0]
+        passed_config = call_args[2]
+        assert len(passed_config.files) == 1
+        assert passed_config.files[0].filename == "do-this-file.txt"
+
+
+class TestListOption:
+    """Test the deprecated --list option."""
+
+    def test_with_version_part_includes_new_version(self, tmp_path: Path, fixtures_path: Path):
+        """The --list option should list the configuration with new_version."""
+        # Arrange
+        config_path = tmp_path / "pyproject.toml"
+        toml_path = fixtures_path / "basic_cfg.toml"
+        shutil.copy(toml_path, config_path)
+        runner: CliRunner = CliRunner()
+
+        with inside_dir(tmp_path):
+            result: Result = runner.invoke(cli.cli, ["bump", "--list", "patch"])
+
+        if result.exit_code != 0:
+            print(result.output)
+            print(result.exception)
+
+        assert result.exit_code == 0
+        assert set(result.output.splitlines(keepends=False)) == {
+            "WARNING:",
+            "",
+            "DEPRECATED: The --list option is deprecated and will be removed in a future version.",
+            "new_version=1.0.1-dev",
+            "current_version=1.0.0",
+            "excluded_paths=[]",
+            "parse=(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",
+            "serialize=('{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}')",
+            "search={current_version}",
+            "replace={new_version}",
+            "regex=False",
+            "ignore_missing_version=False",
+            "included_paths=[]",
+            "tag=True",
+            "sign_tags=False",
+            "tag_name=v{new_version}",
+            "tag_message=Bump version: {current_version} → {new_version}",
+            "allow_dirty=False",
+            "commit=True",
+            "message=Bump version: {current_version} → {new_version}",
+            "commit_args=None",
+            (
+                "files=[{'parse': "
+                "'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
+                "'serialize': ('{major}.{minor}.{patch}-{release}', "
+                "'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': "
+                "'{new_version}', 'regex': False, 'ignore_missing_version': False, "
+                "'filename': 'setup.py', 'glob': None, 'key_path': None}, {'parse': "
+                "'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
+                "'serialize': ('{major}.{minor}.{patch}-{release}', "
+                "'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': "
+                "'{new_version}', 'regex': False, 'ignore_missing_version': False, "
+                "'filename': 'bumpversion/__init__.py', 'glob': None, 'key_path': None}, "
+                "{'parse': "
+                "'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
+                "'serialize': ('{major}.{minor}.{patch}-{release}', "
+                "'{major}.{minor}.{patch}'), 'search': '**unreleased**', 'replace': "
+                "'**unreleased**\\n**v{new_version}**', 'regex': False, "
+                "'ignore_missing_version': False, 'filename': 'CHANGELOG.md', 'glob': None, "
+                "'key_path': None}]"
+            ),
+        }
+
+    def test_without_version_part_excludes_new_version(self, tmp_path: Path, fixtures_path: Path):
+        """The --list option should list the configuration without new_version."""
+        # Arrange
+        config_path = tmp_path / "pyproject.toml"
+        toml_path = fixtures_path / "basic_cfg.toml"
+        shutil.copy(toml_path, config_path)
+        runner: CliRunner = CliRunner()
+
+        with inside_dir(tmp_path):
+            result: Result = runner.invoke(cli.cli, ["bump", "--list"])
+
+        if result.exit_code != 0:
+            print(result.output)
+            print(result.exception)
+
+        assert result.exit_code == 0
+        assert set(result.output.splitlines(keepends=False)) == {
+            "WARNING:",
+            "",
+            "DEPRECATED: The --list option is deprecated and will be removed in a future version.",
+            "current_version=1.0.0",
+            "excluded_paths=[]",
+            "parse=(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",
+            "serialize=('{major}.{minor}.{patch}-{release}', '{major}.{minor}.{patch}')",
+            "search={current_version}",
+            "replace={new_version}",
+            "regex=False",
+            "ignore_missing_version=False",
+            "included_paths=[]",
+            "tag=True",
+            "sign_tags=False",
+            "tag_name=v{new_version}",
+            "tag_message=Bump version: {current_version} → {new_version}",
+            "allow_dirty=False",
+            "commit=True",
+            "message=Bump version: {current_version} → {new_version}",
+            "commit_args=None",
+            (
+                "files=[{'parse': "
+                "'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
+                "'serialize': ('{major}.{minor}.{patch}-{release}', "
+                "'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': "
+                "'{new_version}', 'regex': False, 'ignore_missing_version': False, "
+                "'filename': 'setup.py', 'glob': None, 'key_path': None}, {'parse': "
+                "'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
+                "'serialize': ('{major}.{minor}.{patch}-{release}', "
+                "'{major}.{minor}.{patch}'), 'search': '{current_version}', 'replace': "
+                "'{new_version}', 'regex': False, 'ignore_missing_version': False, "
+                "'filename': 'bumpversion/__init__.py', 'glob': None, 'key_path': None}, "
+                "{'parse': "
+                "'(?P<major>\\\\d+)\\\\.(?P<minor>\\\\d+)\\\\.(?P<patch>\\\\d+)(\\\\-(?P<release>[a-z]+))?', "
+                "'serialize': ('{major}.{minor}.{patch}-{release}', "
+                "'{major}.{minor}.{patch}'), 'search': '**unreleased**', 'replace': "
+                "'**unreleased**\\n**v{new_version}**', 'regex': False, "
+                "'ignore_missing_version': False, 'filename': 'CHANGELOG.md', 'glob': None, "
+                "'key_path': None}]"
+            ),
+        }
+
+
+def test_bump_nested_regex(tmp_path: Path, fixtures_path: Path, caplog):
+    """
+    Arrange/Act: Run the `bump` subcommand with --no-configured-files.
+
+    Assert: There is no configured files specified to modify
+    """
+    cff_path = tmp_path / "citation.cff"
+    cff_path.write_text("cff-version: 1.2.0\ndate-released: 2023-09-19\n")
+    content = fixtures_path.joinpath("regex_test_config.toml").read_text()
+    config_path = tmp_path / ".bumpversion.toml"
+    config_path.write_text(content)
+
+    runner: CliRunner = CliRunner()
+    with inside_dir(tmp_path):
+        result: Result = runner.invoke(cli.cli, ["bump", "-vv", "patch"])
+
+    if result.exit_code != 0:
+        print(result.output)
+        print(caplog.text)
+
+    assert result.exit_code == 0
+
+    now = datetime.now().isoformat()[:10]
+    assert cff_path.read_text() == f"cff-version: 1.2.0\ndate-released: {now}\n"
+
+
+def test_missing_explicit_config_file(tmp_path: Path):
+    """The command-line processor should raise an exception if the config file is missing."""
+    with inside_dir(tmp_path):
+        runner: CliRunner = CliRunner()
+        with inside_dir(tmp_path):
+            result: Result = runner.invoke(cli.cli, ["bump", "--config-file", "missing-file.cfg"])
+        assert result.exit_code != 0
+        assert "'missing-file.cfg' does not exist." in result.output
+
+
+def test_cli_options_override_config(tmp_path: Path, fixtures_path: Path, mocker):
+    """The command-line processor should override the config file."""
+    # Arrange
+    config_path = tmp_path / "this_config.toml"
+    fixture_toml = fixtures_path / "basic_cfg.toml"
+    shutil.copy(fixture_toml, config_path)
+    runner: CliRunner = CliRunner()
+    mocked_do_bump = mocker.patch("bumpversion.cli.do_bump")
+
+    # Act
+    with inside_dir(tmp_path):
+        result: Result = runner.invoke(
+            cli.cli,
+            [
+                "bump",
+                "--config-file",
+                str(config_path),
+                "--current-version",
+                "1.1.0",
+                "--new-version",
+                "1.2.0",
+                "--allow-dirty",
+                "--parse",
+                r"XXX(?P<spam>\d+);(?P<blob>\d+);(?P<slurp>\d+)",
+                "--serialize",
+                "XXX{spam};{blob};{slurp}",
+                "--search",
+                "my-search",
+                "--replace",
+                "my-replace",
+                "--no-commit",
+                "--no-tag",
+                "slurp",
+                "do-this-file.txt",
+            ],
+        )
+
+    # Assert
+    assert result.exit_code == 0
+    assert mocked_do_bump.call_count == 1
+    assert mocked_do_bump.call_args[0][0] == "slurp"
+    assert mocked_do_bump.call_args[0][1] == "1.2.0"
+    the_config = mocked_do_bump.call_args[0][2]
+    assert the_config.current_version == "1.1.0"
+    assert the_config.allow_dirty
+    assert the_config.parse == r"XXX(?P<spam>\d+);(?P<blob>\d+);(?P<slurp>\d+)"
+    assert the_config.serialize == ("XXX{spam};{blob};{slurp}",)
+    assert the_config.search == "my-search"
+    assert the_config.replace == "my-replace"
+    assert the_config.commit is False
+    assert the_config.tag is False
+    assert len(the_config.files) == 4
+    assert {f.filename for f in the_config.files} == {
+        "setup.py",
+        "bumpversion/__init__.py",
+        "CHANGELOG.md",
+        "do-this-file.txt",
+    }
+    assert mocked_do_bump.call_args[0][3] == config_path
+
+
+@pytest.mark.parametrize(
+    ["repo", "scm_command"],
+    [
+        param("git_repo", "git", id="git"),
+        param("hg_repo", "hg", id="hg"),
+    ],
+)
+def test_dirty_work_dir_raises_error(repo: str, scm_command: str, request):
+    repo_path: Path = request.getfixturevalue(repo)
+    with inside_dir(repo_path):
+        # Arrange
+        repo_path.joinpath("dirty2").write_text("i'm dirty! 1.1.1")
+        subprocess.run([scm_command, "add", "dirty2"], check=True)
+        runner: CliRunner = CliRunner()
+
+        # Act
+        result: Result = runner.invoke(
+            cli.cli, ["bump", "patch", "--current-version", "1.1.1", "--no-allow-dirty", "dirty2"]
+        )
+
+    # Assert
+    assert result.exit_code != 0
+    assert "working directory is not clean" in result.output
+
+
+def test_non_scm_operations_if_scm_not_installed(tmp_path: Path, monkeypatch):
+    """Everything works except SCM commands if the SCM is unusable."""
+    # Arrange
+    monkeypatch.setenv("PATH", "")
+
+    with inside_dir(tmp_path):
+        version_path = tmp_path / "VERSION"
+        version_path.write_text("31.0.3")
+
+        runner: CliRunner = CliRunner()
+
+        # Act
+        runner.invoke(cli.cli, ["bump", "major", "--current-version", "31.0.3", "VERSION"])
+
+    # Assert
+    assert version_path.read_text() == "32.0.0"
+
+
+@pytest.mark.parametrize(
+    ["version_part"],
+    [
+        param("charlie", id="bad_version_part"),
+        param("", id="missing_version_part"),
+    ],
+)
+def test_detects_bad_or_missing_version_part(version_part: str, tmp_path: Path, monkeypatch):
+    """It properly detects bad or missing version part."""
+    # Arrange
+    monkeypatch.setenv("PATH", "")
+
+    with inside_dir(tmp_path):
+        version_path = tmp_path / "VERSION"
+        version_path.write_text("31.0.3")
+
+        runner: CliRunner = CliRunner()
+        args = ["bump", "--current-version", "31.0.3"]
+        if version_part:
+            args.append(version_part)
+        args.append("VERSION")
+        # Act
+        result = runner.invoke(cli.cli, args)
+
+    # Assert
+    assert result.exception is not None
+    assert "Unknown version part:" in result.stdout
diff --git a/tests/test_cli/test_legacy.py b/tests/test_cli/test_legacy.py
new file mode 100644
index 00000000..8427dfe4
--- /dev/null
+++ b/tests/test_cli/test_legacy.py
@@ -0,0 +1,22 @@
+from click.testing import CliRunner, Result
+
+from bumpversion import cli
+from tests.conftest import inside_dir
+
+
+def test_no_subcommand_calls_bump(mocker, tmp_path):
+    """
+    The legacy behavior of calling `bumpversion` with no subcommand is to call `bump`.
+    """
+    mocked_do_bump = mocker.patch("bumpversion.cli.do_bump")
+    runner: CliRunner = CliRunner()
+    with inside_dir(tmp_path):
+        result: Result = runner.invoke(cli.cli, ["--current-version", "1.0.0", "--no-configured-files", "patch"])
+
+    if result.exit_code != 0:
+        print(result.output)
+
+    assert result.exit_code == 0
+
+    call_args = mocked_do_bump.call_args[0]
+    assert len(call_args[2].files) == 0
diff --git a/tests/test_cli/test_replace.py b/tests/test_cli/test_replace.py
new file mode 100644
index 00000000..eef80cf7
--- /dev/null
+++ b/tests/test_cli/test_replace.py
@@ -0,0 +1,220 @@
+"""Tests for `bumpversion` package."""
+import shutil
+import traceback
+from pathlib import Path
+
+from click.testing import CliRunner, Result
+
+from bumpversion import cli
+from tests.conftest import inside_dir
+
+
+def test_replace(mocker, tmp_path, fixtures_path):
+    """The replace subcommand should replace the version in the file."""
+    # Arrange
+    toml_path = fixtures_path / "basic_cfg.toml"
+    config_path = tmp_path / "pyproject.toml"
+    shutil.copy(toml_path, config_path)
+
+    mocked_modify_files = mocker.patch("bumpversion.cli.modify_files")
+    runner: CliRunner = CliRunner()
+    with inside_dir(tmp_path):
+        result: Result = runner.invoke(cli.cli, ["replace", "--new-version", "1.1.0"])
+
+    if result.exit_code != 0:
+        print(result.output)
+
+    assert result.exit_code == 0
+
+    call_args = mocked_modify_files.call_args[0]
+    configured_files = call_args[0]
+    assert len(configured_files) == 3
+    actual_filenames = {f.file_change.filename for f in configured_files}
+    assert actual_filenames == {"setup.py", "CHANGELOG.md", "bumpversion/__init__.py"}
+
+
+def test_replace_no_newversion(mocker, tmp_path, fixtures_path):
+    """The replace subcommand should set new_version to None in the context."""
+    # Arrange
+    toml_path = fixtures_path / "basic_cfg.toml"
+    config_path = tmp_path / "pyproject.toml"
+    shutil.copy(toml_path, config_path)
+
+    mocked_modify_files = mocker.patch("bumpversion.cli.modify_files")
+    runner: CliRunner = CliRunner()
+    with inside_dir(tmp_path):
+        result: Result = runner.invoke(cli.cli, ["replace"])
+
+    if result.exit_code != 0:
+        print(result.output)
+
+    assert result.exit_code == 0
+
+    call_args = mocked_modify_files.call_args[0]
+    assert call_args[2] is None
+
+
+def test_replace_specific_files(mocker, git_repo, fixtures_path):
+    """The replace subcommand should set the files to only the specified files."""
+    # Arrange
+    toml_path = fixtures_path / "basic_cfg.toml"
+    config_path = git_repo / "pyproject.toml"
+    shutil.copy(toml_path, config_path)
+
+    mocked_modify_files = mocker.patch("bumpversion.cli.modify_files")
+    runner: CliRunner = CliRunner()
+    with inside_dir(git_repo):
+        result: Result = runner.invoke(cli.cli, ["replace", "--no-configured-files", "VERSION"])
+
+    if result.exit_code != 0:
+        print(result.output)
+
+    assert result.exit_code == 0
+
+    call_args = mocked_modify_files.call_args[0]
+    configured_files = call_args[0]
+    assert len(configured_files) == 1
+    assert configured_files[0].file_change.filename == "VERSION"
+
+
+TEST_REPLACE_CONFIG = {
+    "tool": {
+        "bumpversion": {
+            "allow_dirty": True,
+            "commit": False,
+            "current_version": "2.17.7",
+            "files": [],
+            "message": "Bump version: {current_version} → {new_version}",
+            "parse": "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)",
+            "parts": {
+                "major": {},
+                "minor": {},
+                "patch": {},
+            },
+            "replace": "{new_version}",
+            "search": "{current_version}",
+            "serialize": ["{major}.{minor}.{patch}"],
+            "sign_tags": False,
+            "tag": False,
+            "tag_message": "Bump version: {current_version} → {new_version}",
+            "tag_name": "v{new_version}",
+        }
+    }
+}
+
+
+def test_replace_search_with_plain_string(tmp_path, fixtures_path):
+    """Replace should not worry if the search or replace values have version info."""
+    from tomlkit import dumps
+
+    # Arrange
+    config_path = tmp_path / "pyproject.toml"
+    config_path.write_text(dumps(TEST_REPLACE_CONFIG))
+    doc_path = tmp_path / "docs.yaml"
+    doc_path.write_text("url: https://github.com/sampleuser/workflows/main/.github/update_mailmap.py")
+
+    runner: CliRunner = CliRunner()
+    with inside_dir(tmp_path):
+        result: Result = runner.invoke(
+            cli.cli,
+            [
+                "replace",
+                "--no-configured-files",
+                "--search",
+                "/workflows/main/",
+                "--replace",
+                "/workflows/v{current_version}/",
+                "./docs.yaml",
+            ],
+        )
+
+    if result.exit_code != 0:
+        print("Here is the output:")
+        print(result.output)
+        print(traceback.print_exception(result.exc_info[1]))
+
+    assert result.exit_code == 0
+
+
+def test_replace_with_empty_string(tmp_path, fixtures_path):
+    """Replace should not worry if the replace value has version info."""
+    from tomlkit import dumps
+
+    # Arrange
+    config_path = tmp_path / "pyproject.toml"
+    config_path.write_text(dumps(TEST_REPLACE_CONFIG))
+    doc_path = tmp_path / "docs.yaml"
+    doc_path.write_text("We should censor profanity\n\n")
+
+    runner: CliRunner = CliRunner()
+    with inside_dir(tmp_path):
+        result: Result = runner.invoke(
+            cli.cli,
+            [
+                "replace",
+                "--verbose",
+                "--no-configured-files",
+                "--allow-dirty",
+                "--search",
+                "profanity",
+                "--replace",
+                "",
+                "./docs.yaml",
+            ],
+        )
+
+    if result.exit_code != 0:
+        print("Here is the output:")
+        print(result.output)
+        print(traceback.print_exception(result.exc_info[1]))
+    assert result.exit_code == 0
+    assert doc_path.read_text() == "We should censor \n\n"
+
+
+def test_valid_regex_not_ignoring_regex(tmp_path: Path, caplog) -> None:
+    """A search string not meant to be a regex (but is) is still found and replaced correctly."""
+    # Arrange
+    search = "(unreleased)"
+    replace = "(2023-01-01)"
+
+    version_path = tmp_path / "VERSION"
+    version_path.write_text("# Changelog\n\n## [0.0.1 (unreleased)](https://cool.url)\n\n- Test unreleased package.\n")
+    config_file = tmp_path / ".bumpversion.toml"
+    config_file.write_text(
+        "[tool.bumpversion]\n"
+        'current_version = "0.0.1"\n'
+        "allow_dirty = true\n\n"
+        "[[tool.bumpversion.files]]\n"
+        'filename = "VERSION"\n'
+        "regex = false\n"
+        f'search = "{search}"\n'
+        f'replace = "{replace}"\n'
+    )
+
+    # Act
+    runner: CliRunner = CliRunner()
+    with inside_dir(tmp_path):
+        result: Result = runner.invoke(
+            cli.cli,
+            [
+                "replace",
+                "--verbose",
+                "--no-regex",
+                "--no-configured-files",
+                "--search",
+                search,
+                "--replace",
+                replace,
+                "VERSION",
+            ],
+        )
+
+    # Assert
+    if result.exit_code != 0:
+        print(result.output)
+
+    assert result.exit_code == 0
+    assert (
+        version_path.read_text()
+        == "# Changelog\n\n## [0.0.1 (2023-01-01)](https://cool.url)\n\n- Test unreleased package.\n"
+    )
diff --git a/tests/test_cli/test_show.py b/tests/test_cli/test_show.py
new file mode 100644
index 00000000..4074cefd
--- /dev/null
+++ b/tests/test_cli/test_show.py
@@ -0,0 +1,66 @@
+import shutil
+from pathlib import Path
+
+from click.testing import CliRunner, Result
+
+from bumpversion import cli
+from tests.conftest import inside_dir
+
+
+def test_show(tmp_path: Path, fixtures_path: Path):
+    """The show subcommand should list the parts of the configuration."""
+    # Arrange
+    config_path = tmp_path / "pyproject.toml"
+    toml_path = fixtures_path / "basic_cfg.toml"
+    shutil.copy(toml_path, config_path)
+    runner: CliRunner = CliRunner()
+
+    with inside_dir(tmp_path):
+        result: Result = runner.invoke(cli.cli, ["show", "current_version"])
+
+    if result.exit_code != 0:
+        print(result.output)
+        print(result.exception)
+
+    assert result.exit_code == 0
+    assert result.output.splitlines(keepends=False) == ["1.0.0"]
+
+
+def test_show_and_increment(tmp_path: Path, fixtures_path: Path):
+    """The show subcommand should incrment the version and display it."""
+    # Arrange
+    config_path = tmp_path / "pyproject.toml"
+    toml_path = fixtures_path / "basic_cfg.toml"
+    shutil.copy(toml_path, config_path)
+    runner: CliRunner = CliRunner()
+
+    with inside_dir(tmp_path):
+        result: Result = runner.invoke(cli.cli, ["show", "new_version", "--increment", "minor"])
+
+    if result.exit_code != 0:
+        print(result.output)
+        print(result.exception)
+
+    assert result.exit_code == 0
+    assert result.output.splitlines(keepends=False) == ["1.1.0-dev"]
+
+
+def test_show_no_args(tmp_path: Path, fixtures_path: Path):
+    """The show subcommand should list the entire configuration."""
+    # Arrange
+    config_path = tmp_path / "pyproject.toml"
+    toml_path = fixtures_path / "basic_cfg.toml"
+    expected_output = fixtures_path.joinpath("basic_cfg_expected.yaml").read_text()
+
+    shutil.copy(toml_path, config_path)
+    runner: CliRunner = CliRunner()
+
+    with inside_dir(tmp_path):
+        result: Result = runner.invoke(cli.cli, ["show", "--format", "yaml"])
+
+    if result.exit_code != 0:
+        print(result.output)
+        print(result.exception)
+
+    assert result.exit_code == 0
+    assert result.output.strip() == expected_output.strip()
diff --git a/tests/test_cli/test_show_bump.py b/tests/test_cli/test_show_bump.py
new file mode 100644
index 00000000..b9d6cb2c
--- /dev/null
+++ b/tests/test_cli/test_show_bump.py
@@ -0,0 +1,61 @@
+"""Tests the show_bump command."""
+import shutil
+from pathlib import Path
+
+from click.testing import CliRunner, Result
+from bumpversion import cli
+from tests.conftest import inside_dir
+
+
+def test_show_bump_uses_current_version(tmp_path: Path, fixtures_path: Path):
+    """The show_bump subcommand should list the parts of the configuration."""
+    # Arrange
+    config_path = tmp_path / "pyproject.toml"
+    toml_path = fixtures_path / "basic_cfg.toml"
+    shutil.copy(toml_path, config_path)
+    runner: CliRunner = CliRunner()
+
+    with inside_dir(tmp_path):
+        result: Result = runner.invoke(cli.cli, ["show-bump"])
+
+    if result.exit_code != 0:
+        print(result.output)
+        print(result.exception)
+
+    assert result.exit_code == 0
+    assert result.output == "\n".join(
+        [
+            "1.0.0 ── bump ─┬─ major ─── 2.0.0-dev",
+            "               ├─ minor ─── 1.1.0-dev",
+            "               ├─ patch ─── 1.0.1-dev",
+            "               ╰─ release ─ invalid: The part has already the maximum value among "
+            "['dev', 'gamma'] and cannot be bumped.\n",
+        ]
+    )
+
+
+def test_show_bump_uses_passed_version(tmp_path: Path, fixtures_path: Path):
+    """The show_bump subcommand should list the parts of the configuration."""
+    # Arrange
+    config_path = tmp_path / "pyproject.toml"
+    toml_path = fixtures_path / "basic_cfg.toml"
+    shutil.copy(toml_path, config_path)
+    runner: CliRunner = CliRunner()
+
+    with inside_dir(tmp_path):
+        result: Result = runner.invoke(cli.cli, ["show-bump", "1.2.3"])
+
+    if result.exit_code != 0:
+        print(result.output)
+        print(result.exception)
+
+    assert result.exit_code == 0
+    assert result.output == "\n".join(
+        [
+            "1.2.3 ── bump ─┬─ major ─── 2.0.0-dev",
+            "               ├─ minor ─── 1.3.0-dev",
+            "               ├─ patch ─── 1.2.4-dev",
+            "               ╰─ release ─ invalid: The part has already the maximum value among ['dev', 'gamma'] "
+            "and cannot be bumped.\n",
+        ]
+    )
diff --git a/tests/test_cli/test_version_display.py b/tests/test_cli/test_version_display.py
new file mode 100644
index 00000000..06bd5be7
--- /dev/null
+++ b/tests/test_cli/test_version_display.py
@@ -0,0 +1,15 @@
+from click.testing import CliRunner, Result
+
+from bumpversion import cli
+from bumpversion import __version__
+
+
+def test_version_displays_library_version():
+    """
+    Arrange/Act: Run the `version` subcommand.
+
+    Assert: The output matches the library version.
+    """
+    runner: CliRunner = CliRunner()
+    result: Result = runner.invoke(cli.cli, ["--version"])
+    assert __version__ in result.output.strip(), "Version number should match library version."
diff --git a/tests/test_config/test_create.py b/tests/test_config/test_create.py
new file mode 100644
index 00000000..524bc719
--- /dev/null
+++ b/tests/test_config/test_create.py
@@ -0,0 +1,126 @@
+"""Test the create function in the config module."""
+from pathlib import Path
+import pytest
+
+from tests.conftest import inside_dir
+from tomlkit import dumps
+
+from bumpversion.config.create import create_configuration, get_defaults_from_dest
+from bumpversion.config import DEFAULTS
+
+
+BASIC_CONFIG = {
+    "tool": {
+        "bumpversion": {
+            "commit": True,
+            "tag": True,
+            "current_version": "1.0.0",
+            "parse": "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+))?",
+            "serialize": ["{major}.{minor}.{patch}-{release}", "{major}.{minor}.{patch}"],
+            "files": [
+                {"filename": "setup.py"},
+                {"filename": "bumpversion/__init__.py"},
+                {"filename": "CHANGELOG.md", "search": "**unreleased**", "replace": "**v{new_version}**"},
+            ],
+            "parts": {
+                "release": {
+                    "optional_value": "gamma",
+                    "values": ["dev", "gamma"],
+                }
+            },
+        }
+    }
+}
+
+
+@pytest.fixture
+def default_config() -> dict:
+    """The default configuration with the scm_info and parts removed."""
+    defaults = DEFAULTS.copy()
+    del defaults["scm_info"]
+    del defaults["parts"]
+    del defaults["files"]
+    return defaults
+
+
+class TestGetDefaultsFromDest:
+    """Test the get_defaults_from_dest function."""
+
+    def test_existing_path_returns_existing_config(self, tmp_path: Path, default_config: dict):
+        """If the destination exists, the existing config should be returned."""
+        dest_path = tmp_path / "pyproject-1.toml"
+        dest_path.write_text(dumps(BASIC_CONFIG))
+
+        with inside_dir(tmp_path):
+            config, destination_config = get_defaults_from_dest("pyproject-1.toml")
+
+        default_config.update(BASIC_CONFIG["tool"]["bumpversion"])
+        del default_config["files"]
+        del default_config["parts"]
+
+        assert config == default_config
+
+    def test_missing_path_returns_default_config(self, tmp_path: Path, default_config: dict):
+        """If the destination does not exist, the default config should be returned."""
+        with inside_dir(tmp_path):
+            config, destination_config = get_defaults_from_dest("pyproject-2.toml")
+
+        default_config["current_version"] = "0.1.0"
+
+        assert config == default_config
+
+    def test_existing_path_with_no_config_returns_default_config(self, tmp_path: Path, default_config: dict):
+        """If the destination exists but has no config, the default config should be returned."""
+        dest_path = tmp_path / "pyproject-3.toml"
+        dest_path.write_text(dumps({"tool": {"poetry": {}}}))
+
+        with inside_dir(tmp_path):
+            config, destination_config = get_defaults_from_dest("pyproject-3.toml")
+
+        default_config["current_version"] = "0.1.0"
+
+        assert config == default_config
+
+    def test_existing_path_with_project_version_uses_project_version(self, tmp_path: Path, default_config: dict):
+        """If the destination exists and has a project version, the project version should be used as current_version."""
+        dest_path = tmp_path / "pyproject-4.toml"
+        dest_path.write_text(dumps({"tool": {"poetry": {}}, "project": {"version": "1.2.3"}}))
+
+        with inside_dir(tmp_path):
+            config, destination_config = get_defaults_from_dest("pyproject-4.toml")
+
+        default_config["current_version"] = "1.2.3"
+        assert config == default_config
+
+
+class TestCreateConfiguration:
+    """Test the create_configuration function."""
+
+    def test_destination_without_prompt_returns_default_config(self, tmp_path: Path, default_config: dict):
+        """If the destination is provided and no prompt is requested, the default config should be returned."""
+        with inside_dir(tmp_path):
+            destination_config = create_configuration("pyproject.toml", prompt=False)
+
+        default_config["current_version"] = "0.1.0"
+        default_config["commit_args"] = ""
+
+        assert destination_config["tool"]["bumpversion"] == default_config
+
+    def test_prompt_true_asks_questions_and_updates_result(self, tmp_path: Path, default_config: dict, mocker):
+        """If prompt is True, the user should be prompted for the configuration."""
+        answer = {
+            "current_version": "1.2.3",
+            "commit": True,
+            "allow_dirty": True,
+            "tag": True,
+            "commit_args": "--no-verify",
+        }
+        mocked_questionary = mocker.patch("bumpversion.config.create.questionary", autospec=True)
+        mocked_questionary.form.return_value.ask.return_value = answer
+        with inside_dir(tmp_path):
+            destination_config = create_configuration("pyproject.toml", prompt=True)
+
+        default_config.update(answer)
+
+        assert destination_config["tool"]["bumpversion"] == default_config
+        assert mocked_questionary.form.return_value.ask.call_count == 1
diff --git a/tests/test_visualize.py b/tests/test_visualize.py
new file mode 100644
index 00000000..3044d056
--- /dev/null
+++ b/tests/test_visualize.py
@@ -0,0 +1,70 @@
+"""Tests of the visualize module."""
+from pathlib import Path
+
+from bumpversion.visualize import Border, BOX_CHARS, connection_str, lead_string, labeled_line, visualize
+from bumpversion.config import get_configuration
+
+
+class TestLeadString:
+    """Tests of the lead_string function."""
+
+    def test_returns_string_with_bump(self):
+        assert lead_string("1.0.0", Border(*BOX_CHARS["light"])) == "1.0.0 ── bump ─"
+
+    def test_returns_blank_string(self):
+        assert lead_string("1.0.0", Border(*BOX_CHARS["light"]), blank=True) == "               "
+
+
+class TestConnectionStr:
+    """Tests of the connection_str function."""
+
+    def test_returns_correct_connection_string(self):
+        border = Border(*BOX_CHARS["light"])
+        assert connection_str(border, has_next=True, has_previous=True) == border.divider_right + border.line
+        assert connection_str(border, has_next=True, has_previous=False) == border.divider_down + border.line
+        assert connection_str(border, has_next=False, has_previous=True) == border.corner_bottom_left + border.line
+        assert connection_str(border, has_next=False, has_previous=False) == border.line * 2
+
+
+class TestLabeledLine:
+    """Tests of the labeled_line function."""
+
+    def test_no_label_fill_when_no_fit_length(self):
+        """Without a fit_length, there will be no filler in the line."""
+        border = Border(*BOX_CHARS["light"])
+        assert labeled_line("major", border, fit_length=None) == " major ─ "
+
+    def test_pads_label_if_fit_length(self):
+        """Without a fit_length, there will be no filler in the line."""
+        border = Border(*BOX_CHARS["light"])
+        assert labeled_line("major", border, fit_length=10) == " major ────── "
+
+
+class TestVisualize:
+    """Tests of the visualize function."""
+
+    def test_outputs_using_default_config(self, tmp_path: Path, capsys):
+        """Test that the correct string is returned."""
+        config = get_configuration(tmp_path.joinpath("missing.toml"), current_version="1.0.0")
+        visualize(config, "1.0.0")
+        captured = capsys.readouterr()
+        assert captured.out == "\n".join(
+            ["1.0.0 ── bump ─┬─ major ─ 2.0.0", "               ├─ minor ─ 1.1.0", "               ╰─ patch ─ 1.0.1\n"]
+        )
+
+    def test_indicates_invalid_paths(self, tmp_path: Path, fixtures_path: Path, capsys):
+        """Test that the correct string is returned."""
+        config_content = fixtures_path.joinpath("basic_cfg.toml").read_text()
+        config_path = tmp_path.joinpath(".bumpversion-1.toml")
+        config_path.write_text(config_content)
+        config = get_configuration(config_path)
+        visualize(config, "1.0.0")
+        captured = capsys.readouterr()
+        assert captured.out == "\n".join(
+            [
+                "1.0.0 ── bump ─┬─ major ─── 2.0.0-dev",
+                "               ├─ minor ─── 1.1.0-dev",
+                "               ├─ patch ─── 1.0.1-dev",
+                "               ╰─ release ─ invalid: The part has already the maximum value among ['dev', 'gamma'] and cannot be bumped.\n",
+            ]
+        )