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.
This commit is contained in:
Markus Holtermann 2014-09-03 10:51:07 +02:00 committed by Tim Graham
parent 8952757698
commit a1487deebf
7 changed files with 314 additions and 57 deletions

View File

@ -5,6 +5,7 @@ from collections import OrderedDict
from importlib import import_module from importlib import import_module
import itertools import itertools
import traceback import traceback
import warnings
from django.apps import apps from django.apps import apps
from django.core.management import call_command 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.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 import connections, router, transaction, DEFAULT_DB_ALIAS
from django.db.migrations.executor import MigrationExecutor 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.state import ProjectState
from django.db.migrations.autodetector import MigrationAutodetector from django.db.migrations.autodetector import MigrationAutodetector
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.module_loading import module_has_submodule 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 they asked for a migration listing, quit main execution flow and show it
if options.get("list", False): 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 # Hook for backends needing any database preparation
connection.prepare_database() connection.prepare_database()
@ -325,44 +340,3 @@ class Command(BaseCommand):
) )
return created_models 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)

View File

@ -0,0 +1,116 @@
# -*- 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))

View File

@ -123,6 +123,8 @@ details on these changes.
* Private attribute ``django.db.models.Field.related`` will be removed. * 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: .. _deprecation-removed-in-1.9:
1.9 1.9

View File

@ -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 table into a state where manual recovery will be needed to make migrations
run correctly. run correctly.
.. django-admin-option:: --list, -l .. deprecated:: 1.8
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.
The ``--list`` option has been moved to the :djadmin:`showmigrations`
command.
runfcgi [options] runfcgi [options]
----------------- -----------------
@ -1088,6 +1083,32 @@ behavior you can use the ``--no-startup`` option. e.g.::
django-admin shell --plain --no-startup 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 ...> sql <app_label app_label ...>
----------------------------- -----------------------------

View File

@ -394,6 +394,9 @@ Management Commands
* :djadmin:`makemigrations` now supports an :djadminopt:`--exit` option to * :djadmin:`makemigrations` now supports an :djadminopt:`--exit` option to
exit with an error code if no migrations are created. 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 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` will be removed in Django 2.0. Use :class:`~django.core.management.BaseCommand`
instead, which takes no arguments by default. 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`` ``cache_choices`` option of ``ModelChoiceField`` and ``ModelMultipleChoiceField``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -9,8 +9,9 @@ from django.apps import apps
from django.db import connection, models from django.db import connection, models
from django.core.management import call_command, CommandError from django.core.management import call_command, CommandError
from django.db.migrations import questioner from django.db.migrations import questioner
from django.test import override_settings from django.test import ignore_warnings, override_settings
from django.utils import six from django.utils import six
from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.encoding import force_text from django.utils.encoding import force_text
from .models import UnicodeModel, UnserializableModel from .models import UnicodeModel, UnserializableModel
@ -50,6 +51,15 @@ class MigrateTests(MigrationTestBase):
self.assertTableNotExists("migrations_tribble") self.assertTableNotExists("migrations_tribble")
self.assertTableNotExists("migrations_book") self.assertTableNotExists("migrations_book")
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_conflict"})
def test_migrate_conflict_exit(self):
"""
Makes sure that migrate exits if it detects a conflict.
"""
with self.assertRaisesMessage(CommandError, "Conflicting migrations detected"):
call_command("migrate", "migrations")
@ignore_warnings(category=RemovedInDjango20Warning)
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"}) @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
def test_migrate_list(self): def test_migrate_list(self):
""" """
@ -72,13 +82,137 @@ class MigrateTests(MigrationTestBase):
# Cleanup by unmigrating everything # Cleanup by unmigrating everything
call_command("migrate", "migrations", "zero", verbosity=0) call_command("migrate", "migrations", "zero", verbosity=0)
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_conflict"}) @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
def test_migrate_conflict_exit(self): def test_showmigrations_list(self):
""" """
Makes sure that migrate exits if it detects a conflict. Tests --list output of showmigrations command
""" """
with self.assertRaises(CommandError): out = six.StringIO()
call_command("migrate", "migrations") call_command("showmigrations", format='list', stdout=out, verbosity=0)
self.assertIn("migrations", out.getvalue().lower())
self.assertIn("[ ] 0001_initial", out.getvalue().lower())
self.assertIn("[ ] 0002_second", out.getvalue().lower())
call_command("migrate", "migrations", "0001", verbosity=0)
out = six.StringIO()
# Giving the explicit app_label tests for selective `show_list` in the command
call_command("showmigrations", "migrations", format='list', stdout=out, verbosity=0)
self.assertIn("migrations", out.getvalue().lower())
self.assertIn("[x] 0001_initial", out.getvalue().lower())
self.assertIn("[ ] 0002_second", out.getvalue().lower())
# Cleanup by unmigrating everything
call_command("migrate", "migrations", "zero", verbosity=0)
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_run_before"})
def test_showmigrations_plan(self):
"""
Tests --plan output of showmigrations command
"""
out = six.StringIO()
call_command("showmigrations", format='plan', stdout=out)
self.assertIn(
"[ ] migrations.0001_initial\n"
"[ ] migrations.0003_third\n"
"[ ] migrations.0002_second",
out.getvalue().lower()
)
out = six.StringIO()
call_command("showmigrations", format='plan', stdout=out, verbosity=2)
self.assertIn(
"[ ] migrations.0001_initial\n"
"[ ] migrations.0003_third ... (migrations.0001_initial)\n"
"[ ] migrations.0002_second ... (migrations.0001_initial)",
out.getvalue().lower()
)
call_command("migrate", "migrations", "0003", verbosity=0)
out = six.StringIO()
call_command("showmigrations", format='plan', stdout=out)
self.assertIn(
"[x] migrations.0001_initial\n"
"[x] migrations.0003_third\n"
"[ ] migrations.0002_second",
out.getvalue().lower()
)
out = six.StringIO()
call_command("showmigrations", format='plan', stdout=out, verbosity=2)
self.assertIn(
"[x] migrations.0001_initial\n"
"[x] migrations.0003_third ... (migrations.0001_initial)\n"
"[ ] migrations.0002_second ... (migrations.0001_initial)",
out.getvalue().lower()
)
# Cleanup by unmigrating everything
call_command("migrate", "migrations", "zero", verbosity=0)
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_empty"})
def test_showmigrations_plan_no_migrations(self):
"""
Tests --plan output of showmigrations command without migrations
"""
out = six.StringIO()
call_command("showmigrations", format='plan', stdout=out)
self.assertEqual("", out.getvalue().lower())
out = six.StringIO()
call_command("showmigrations", format='plan', stdout=out, verbosity=2)
self.assertEqual("", out.getvalue().lower())
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_squashed_complex"})
def test_showmigrations_plan_squashed(self):
"""
Tests --plan output of showmigrations command with squashed migrations.
"""
out = six.StringIO()
call_command("showmigrations", format='plan', stdout=out)
self.assertEqual(
"[ ] migrations.1_auto\n"
"[ ] migrations.2_auto\n"
"[ ] migrations.3_squashed_5\n"
"[ ] migrations.6_auto\n"
"[ ] migrations.7_auto\n",
out.getvalue().lower()
)
out = six.StringIO()
call_command("showmigrations", format='plan', stdout=out, verbosity=2)
self.assertEqual(
"[ ] migrations.1_auto\n"
"[ ] migrations.2_auto ... (migrations.1_auto)\n"
"[ ] migrations.3_squashed_5 ... (migrations.2_auto)\n"
"[ ] migrations.6_auto ... (migrations.3_squashed_5)\n"
"[ ] migrations.7_auto ... (migrations.6_auto)\n",
out.getvalue().lower()
)
call_command("migrate", "migrations", "3_squashed_5", verbosity=0)
out = six.StringIO()
call_command("showmigrations", format='plan', stdout=out)
self.assertEqual(
"[x] migrations.1_auto\n"
"[x] migrations.2_auto\n"
"[x] migrations.3_squashed_5\n"
"[ ] migrations.6_auto\n"
"[ ] migrations.7_auto\n",
out.getvalue().lower()
)
out = six.StringIO()
call_command("showmigrations", format='plan', stdout=out, verbosity=2)
self.assertEqual(
"[x] migrations.1_auto\n"
"[x] migrations.2_auto ... (migrations.1_auto)\n"
"[x] migrations.3_squashed_5 ... (migrations.2_auto)\n"
"[ ] migrations.6_auto ... (migrations.3_squashed_5)\n"
"[ ] migrations.7_auto ... (migrations.6_auto)\n",
out.getvalue().lower()
)
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"}) @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
def test_sqlmigrate(self): def test_sqlmigrate(self):