Source code for alembic_converger.cli

"""
Command-line interface for alembic-converger.

This module provides a Click-based CLI for converging Alembic migrations.
"""

import sys

import click

from alembic_converger import __version__
from alembic_converger.converge import converge_migrations
from alembic_converger.errors import (
    ConfigurationError,
    ConvergenceError,
    MultipleBasesError,
    ValidationError,
)


@click.group()
@click.version_option(version=__version__, prog_name="alembic-converger")
def cli():
    """Alembic Converger - Safely converge Alembic migration graphs.

    This tool automatically detects and converges multiple Alembic migration
    heads by generating minimal merge migrations without schema operations.
    """
    pass


@cli.command()
@click.option(
    "--from-revision",
    required=True,
    help="Base revision to converge from. All heads must descend from this revision.",
)
@click.option(
    "--alembic-config",
    default="alembic.ini",
    help="Path to alembic.ini configuration file.",
    show_default=True,
)
@click.option(
    "--dry-run",
    is_flag=True,
    help="Show what would be done without making changes.",
)
@click.option(
    "--skip-validation",
    is_flag=True,
    help="Skip upgrade validation (faster but less safe, not recommended).",
)
@click.option(
    "--verbose",
    "-v",
    is_flag=True,
    help="Enable debug logging.",
)
def converge(
    from_revision: str,
    alembic_config: str,
    dry_run: bool,
    skip_validation: bool,
    verbose: bool,
):
    """Converge multiple migration heads into a single head.

    This command finds all heads descending from FROM_REVISION and merges
    them iteratively until only one head remains.

    Examples:

      # Converge heads from revision ae1027a6acf
      alembic-converger converge --from-revision ae1027a6acf

      # Dry run to see what would be done
      alembic-converger converge --from-revision ae1027a6acf --dry-run

      # Use custom alembic.ini location
      alembic-converger converge --from-revision ae1027a6acf \\
          --alembic-config config/alembic.ini

    Exit codes:
      0: Success (already converged or successfully merged)
      1: Convergence failed (validation error, conflicts, etc.)
      2: Configuration error
    """
    try:
        # Run convergence
        result = converge_migrations(
            base_revision=from_revision,
            config_path=alembic_config,
            dry_run=dry_run,
            skip_validation=skip_validation,
            verbose=verbose,
        )

        # Print summary
        if result.already_converged:
            click.echo(
                click.style("✓ ", fg="green", bold=True)
                + "Already converged (0 or 1 head)"
            )
            if result.final_head:
                click.echo(f"  Current head: {result.final_head}")
        else:
            click.echo(
                click.style("✓ ", fg="green", bold=True) + "Convergence successful!"
            )
            click.echo(f"  Initial heads: {len(result.initial_heads)}")
            click.echo(f"  Merges created: {len(result.merges_created)}")
            click.echo(f"  Final head: {result.final_head}")

            if dry_run:
                click.echo(
                    click.style("\n  Note: ", fg="yellow", bold=True)
                    + "This was a dry run. No changes were made."
                )

        sys.exit(0)

    except ConfigurationError as e:
        click.echo(
            click.style("✗ Configuration Error: ", fg="red", bold=True) + str(e),
            err=True,
        )
        sys.exit(2)

    except MultipleBasesError as e:
        click.echo(
            click.style("✗ Multiple Bases Error: ", fg="red", bold=True) + str(e),
            err=True,
        )
        click.echo(
            "\n"
            + click.style("Hint: ", fg="yellow", bold=True)
            + "The heads do not share a common ancestor. This usually means "
            "you have migrations from unrelated sources or branches.",
            err=True,
        )
        sys.exit(1)

    except ValidationError as e:
        click.echo(
            click.style("✗ Validation Error: ", fg="red", bold=True) + str(e), err=True
        )
        click.echo(
            "\n"
            + click.style("Hint: ", fg="yellow", bold=True)
            + "The merge migration validation failed. This usually indicates "
            "schema conflicts that require manual resolution.",
            err=True,
        )
        sys.exit(1)

    except ConvergenceError as e:
        click.echo(
            click.style("✗ Convergence Error: ", fg="red", bold=True) + str(e), err=True
        )
        sys.exit(1)

    except Exception as e:
        click.echo(
            click.style("✗ Unexpected Error: ", fg="red", bold=True) + str(e), err=True
        )
        if verbose:
            import traceback

            traceback.print_exc()
        sys.exit(1)


[docs] def main(): """Entry point for the CLI.""" cli()
if __name__ == "__main__": main()