Commit a1487dee authored by Markus Holtermann's avatar Markus Holtermann Committed by Tim Graham
Browse files

Fixed #23359 -- Added showmigrations command to list migrations and plan.

Thanks to Collin Anderson, Tim Graham, Gabe Jackson, and Marc Tamlyn
for their input, ideas, and review.
parent 89527576
Loading
Loading
Loading
Loading
+17 −43
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@ from collections import OrderedDict
from importlib import import_module
import itertools
import traceback
import warnings

from django.apps import apps
from django.core.management import call_command
@@ -13,9 +14,10 @@ from django.core.management.color import no_style
from django.core.management.sql import custom_sql_for_model, emit_post_migrate_signal, emit_pre_migrate_signal
from django.db import connections, router, transaction, DEFAULT_DB_ALIAS
from django.db.migrations.executor import MigrationExecutor
from django.db.migrations.loader import MigrationLoader, AmbiguityError
from django.db.migrations.loader import AmbiguityError
from django.db.migrations.state import ProjectState
from django.db.migrations.autodetector import MigrationAutodetector
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.module_loading import module_has_submodule


@@ -62,7 +64,20 @@ class Command(BaseCommand):

        # If they asked for a migration listing, quit main execution flow and show it
        if options.get("list", False):
            return self.show_migration_list(connection, [options['app_label']] if options['app_label'] else None)
            warnings.warn(
                "The 'migrate --list' command is deprecated. Use 'showmigrations' instead.",
                RemovedInDjango20Warning, stacklevel=2)
            return call_command(
                'showmigrations',
                '--list',
                app_labels=[options['app_label']] if options['app_label'] else None,
                database=db,
                no_color=options.get('no-color'),
                settings=options.get('settings'),
                stdout=options.get('stdout', self.stdout),
                traceback=self.show_traceback,
                verbosity=self.verbosity,
            )

        # Hook for backends needing any database preparation
        connection.prepare_database()
@@ -325,44 +340,3 @@ class Command(BaseCommand):
                )

        return created_models

    def show_migration_list(self, connection, app_names=None):
        """
        Shows a list of all migrations on the system, or only those of
        some named apps.
        """
        # Load migrations from disk/DB
        loader = MigrationLoader(connection)
        graph = loader.graph
        # If we were passed a list of apps, validate it
        if app_names:
            invalid_apps = []
            for app_name in app_names:
                if app_name not in loader.migrated_apps:
                    invalid_apps.append(app_name)
            if invalid_apps:
                raise CommandError("No migrations present for: %s" % (", ".join(invalid_apps)))
        # Otherwise, show all apps in alphabetic order
        else:
            app_names = sorted(loader.migrated_apps)
        # For each app, print its migrations in order from oldest (roots) to
        # newest (leaves).
        for app_name in app_names:
            self.stdout.write(app_name, self.style.MIGRATE_LABEL)
            shown = set()
            for node in graph.leaf_nodes(app_name):
                for plan_node in graph.forwards_plan(node):
                    if plan_node not in shown and plan_node[0] == app_name:
                        # Give it a nice title if it's a squashed one
                        title = plan_node[1]
                        if graph.nodes[plan_node].replaces:
                            title += " (%s squashed migrations)" % len(graph.nodes[plan_node].replaces)
                        # Mark it as applied/unapplied
                        if plan_node in loader.applied_migrations:
                            self.stdout.write(" [X] %s" % title)
                        else:
                            self.stdout.write(" [ ] %s" % title)
                        shown.add(plan_node)
            # If we didn't print anything, then a small message
            if not shown:
                self.stdout.write(" (no migrations)", self.style.MIGRATE_FAILURE)
+116 −0
Original line number Diff line number Diff line
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.core.management.base import BaseCommand, CommandError
from django.db import connections, DEFAULT_DB_ALIAS
from django.db.migrations.loader import MigrationLoader


class Command(BaseCommand):
    help = "Shows all available migrations for the current project"

    def add_arguments(self, parser):
        parser.add_argument('app_labels', nargs='*',
            help='App labels of applications to limit the output to.')
        parser.add_argument('--database', action='store', dest='database', default=DEFAULT_DB_ALIAS,
            help='Nominates a database to synchronize. Defaults to the "default" database.')

        formats = parser.add_mutually_exclusive_group()
        formats.add_argument('--list', '-l', action='store_const', dest='format', const='list',
            help='Shows a list of all migrations and which are applied.')
        formats.add_argument('--plan', '-p', action='store_const', dest='format', const='plan',
            help='Shows all migrations in the order they will be applied.')

        parser.set_defaults(format='list')

    def handle(self, *args, **options):
        self.verbosity = options.get('verbosity')

        # Get the database we're operating from
        db = options.get('database')
        connection = connections[db]

        if options['format'] == "plan":
            return self.show_plan(connection)
        else:
            return self.show_list(connection, options['app_labels'])

    def show_list(self, connection, app_names=None):
        """
        Shows a list of all migrations on the system, or only those of
        some named apps.
        """
        # Load migrations from disk/DB
        loader = MigrationLoader(connection)
        graph = loader.graph
        # If we were passed a list of apps, validate it
        if app_names:
            invalid_apps = []
            for app_name in app_names:
                if app_name not in loader.migrated_apps:
                    invalid_apps.append(app_name)
            if invalid_apps:
                raise CommandError("No migrations present for: %s" % (", ".join(invalid_apps)))
        # Otherwise, show all apps in alphabetic order
        else:
            app_names = sorted(loader.migrated_apps)
        # For each app, print its migrations in order from oldest (roots) to
        # newest (leaves).
        for app_name in app_names:
            self.stdout.write(app_name, self.style.MIGRATE_LABEL)
            shown = set()
            for node in graph.leaf_nodes(app_name):
                for plan_node in graph.forwards_plan(node):
                    if plan_node not in shown and plan_node[0] == app_name:
                        # Give it a nice title if it's a squashed one
                        title = plan_node[1]
                        if graph.nodes[plan_node].replaces:
                            title += " (%s squashed migrations)" % len(graph.nodes[plan_node].replaces)
                        # Mark it as applied/unapplied
                        if plan_node in loader.applied_migrations:
                            self.stdout.write(" [X] %s" % title)
                        else:
                            self.stdout.write(" [ ] %s" % title)
                        shown.add(plan_node)
            # If we didn't print anything, then a small message
            if not shown:
                self.stdout.write(" (no migrations)", self.style.MIGRATE_FAILURE)

    def show_plan(self, connection):
        """
        Shows all known migrations in the order they will be applied
        """
        # Load migrations from disk/DB
        loader = MigrationLoader(connection)
        graph = loader.graph
        targets = graph.leaf_nodes()
        plan = []
        seen = set()

        # Generate the plan
        for target in targets:
            for migration in graph.forwards_plan(target):
                if migration not in seen:
                    plan.append(graph.nodes[migration])
                    seen.add(migration)

        # Output
        def print_deps(migration):
            out = []
            for dep in migration.dependencies:
                if dep[1] == "__first__":
                    roots = graph.root_nodes(dep[0])
                    dep = roots[0] if roots else (dep[0], "__first__")
                out.append("%s.%s" % dep)
            if out:
                return " ... (%s)" % ", ".join(out)
            return ""

        for migration in plan:
            deps = ""
            if self.verbosity >= 2:
                deps = print_deps(migration)
            if (migration.app_label, migration.name) in loader.applied_migrations:
                self.stdout.write("[X]  %s%s" % (migration, deps))
            else:
                self.stdout.write("[ ]  %s%s" % (migration, deps))
+2 −0
Original line number Diff line number Diff line
@@ -123,6 +123,8 @@ details on these changes.

* Private attribute ``django.db.models.Field.related`` will be removed.

* The ``--list`` option of the ``migrate`` management command will be removed.

.. _deprecation-removed-in-1.9:

1.9
+29 −8
Original line number Diff line number Diff line
@@ -770,15 +770,10 @@ be warned that using ``--fake`` runs the risk of putting the migration state
table into a state where manual recovery will be needed to make migrations
run correctly.

.. django-admin-option:: --list, -l

The ``--list`` option will list all of the apps Django knows about, the
migrations available for each app and if they are applied or not (marked by
an ``[X]`` next to the migration name).

Apps without migrations are also included in the list, but will have
``(no migrations)`` printed under them.
.. deprecated:: 1.8

    The ``--list`` option has been moved to the :djadmin:`showmigrations`
    command.

runfcgi [options]
-----------------
@@ -1088,6 +1083,32 @@ behavior you can use the ``--no-startup`` option. e.g.::

    django-admin shell --plain --no-startup

showmigrations [<app_label> [<app_label>]]
------------------------------------------

.. django-admin:: showmigrations

.. versionadded:: 1.8

Shows all migrations in a project.

.. django-admin-option:: --list, -l

The ``--list`` option lists all of the apps Django knows about, the
migrations available for each app, and whether or not each migrations is
applied (marked by an ``[X]`` next to the migration name).

Apps without migrations are also listed, but have ``(no migrations)`` printed
under them.

.. django-admin-option:: --plan, -p

The ``--plan`` option shows the migration plan Django will follow to apply
migrations. Any supplied app labels are ignored because the plan might go
beyond those apps. Same as ``--list``, applied migrations are marked by an
``[X]``. For a verbosity of 2 and above, all dependencies of a migration will
also be shown.

sql <app_label app_label ...>
-----------------------------

+10 −0
Original line number Diff line number Diff line
@@ -394,6 +394,9 @@ Management Commands
* :djadmin:`makemigrations` now supports an :djadminopt:`--exit` option to
  exit with an error code if no migrations are created.

* The new :djadmin:`showmigrations` command allows listing all migrations and
  their dependencies in a project.

Middleware
^^^^^^^^^^

@@ -1134,6 +1137,13 @@ The class :class:`~django.core.management.NoArgsCommand` is now deprecated and
will be removed in Django 2.0. Use :class:`~django.core.management.BaseCommand`
instead, which takes no arguments by default.

Listing all migrations in a project
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The ``--list`` option of the :djadmin:`migrate` management command is
deprecated and will be removed in Django 2.0. Use :djadmin:`showmigrations`
instead.

``cache_choices`` option of ``ModelChoiceField`` and ``ModelMultipleChoiceField``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Loading