summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarkus Holtermann <info@markusholtermann.eu>2014-09-03 10:51:07 +0200
committerTim Graham <timograham@gmail.com>2014-12-31 14:14:28 -0500
commita1487deebff7bf27a4946a9f5ddd68154fa4834d (patch)
tree709d13643a09ef0064ae8130483ac970ec5bcf08
parent89527576980ff14309990c152e5904ddd5ca3df7 (diff)
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.
-rw-r--r--django/core/management/commands/migrate.py60
-rw-r--r--django/core/management/commands/showmigrations.py116
-rw-r--r--docs/internals/deprecation.txt2
-rw-r--r--docs/ref/django-admin.txt37
-rw-r--r--docs/releases/1.8.txt10
-rw-r--r--tests/migrations/test_commands.py146
-rw-r--r--tests/migrations/test_migrations_empty/__init__.py0
7 files changed, 314 insertions, 57 deletions
diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py
index d63cb1f339..33d7e44703 100644
--- a/django/core/management/commands/migrate.py
+++ b/django/core/management/commands/migrate.py
@@ -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)
diff --git a/django/core/management/commands/showmigrations.py b/django/core/management/commands/showmigrations.py
new file mode 100644
index 0000000000..9ed9e1d052
--- /dev/null
+++ b/django/core/management/commands/showmigrations.py
@@ -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))
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index 8aa2c09c4d..70db0c0497 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -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
diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt
index 08ca142a14..de556b28b7 100644
--- a/docs/ref/django-admin.txt
+++ b/docs/ref/django-admin.txt
@@ -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 ...>
-----------------------------
diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt
index f1e8eceee6..c869cbc5b7 100644
--- a/docs/releases/1.8.txt
+++ b/docs/releases/1.8.txt
@@ -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``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py
index 5995468e98..a9505dd82e 100644
--- a/tests/migrations/test_commands.py
+++ b/tests/migrations/test_commands.py
@@ -9,8 +9,9 @@ from django.apps import apps
from django.db import connection, models
from django.core.management import call_command, CommandError
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.deprecation import RemovedInDjango20Warning
from django.utils.encoding import force_text
from .models import UnicodeModel, UnserializableModel
@@ -50,6 +51,15 @@ class MigrateTests(MigrationTestBase):
self.assertTableNotExists("migrations_tribble")
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"})
def test_migrate_list(self):
"""
@@ -72,13 +82,137 @@ class MigrateTests(MigrationTestBase):
# Cleanup by unmigrating everything
call_command("migrate", "migrations", "zero", verbosity=0)
- @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_conflict"})
- def test_migrate_conflict_exit(self):
+ @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
+ 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):
- call_command("migrate", "migrations")
+ out = six.StringIO()
+ 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"})
def test_sqlmigrate(self):
diff --git a/tests/migrations/test_migrations_empty/__init__.py b/tests/migrations/test_migrations_empty/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/migrations/test_migrations_empty/__init__.py