diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py index 9899be7e03..96cc70e50b 100644 --- a/django/core/management/commands/migrate.py +++ b/django/core/management/commands/migrate.py @@ -1,3 +1,5 @@ +# encoding: utf8 +from __future__ import unicode_literals from optparse import make_option from collections import OrderedDict from importlib import import_module @@ -11,7 +13,7 @@ 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, models, DEFAULT_DB_ALIAS from django.db.migrations.executor import MigrationExecutor -from django.db.migrations.loader import AmbiguityError +from django.db.migrations.loader import MigrationLoader, AmbiguityError from django.utils.module_loading import module_has_submodule @@ -26,6 +28,8 @@ class Command(BaseCommand): 'Defaults to the "default" database.'), make_option('--fake', action='store_true', dest='fake', default=False, help='Mark migrations as run without actually running them'), + make_option('--list', action='store_true', dest='list', default=False, + help='Show a list of all known migrations and which are applied'), ) help = "Updates database schema. Manages both apps with migrations and those without." @@ -48,6 +52,10 @@ class Command(BaseCommand): db = options.get('database') connection = connections[db] + # 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, args) + # Work out which apps have migrations and which do not executor = MigrationExecutor(connection, self.migration_progress_callback) @@ -243,3 +251,39 @@ class Command(BaseCommand): call_command('loaddata', 'initial_data', verbosity=self.verbosity, database=connection.alias, skip_validation=True) return created_models + + def show_migration_list(self, connection, apps=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 apps: + invalid_apps = [] + for app in apps: + if app_label not in loader.migrated_apps: + invalid_apps.append(app) + if invalid_apps: + raise CommandError("No migrations present for: %s" % (", ".join(invalid_apps))) + # Otherwise, show all apps in alphabetic order + else: + apps = sorted(loader.migrated_apps) + # For each app, print its migrations in order from oldest (roots) to + # newest (leaves). + for app in apps: + self.stdout.write(app, self.style.MIGRATE_LABEL) + shown = set() + for node in graph.leaf_nodes(app): + for plan_node in graph.forwards_plan(node): + if plan_node not in shown and plan_node[0] == app: + if plan_node in loader.applied_migrations: + self.stdout.write(" [X] %s" % plan_node[1]) + else: + self.stdout.write(" [ ] %s" % plan_node[1]) + 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/db/migrations/graph.py b/django/db/migrations/graph.py index fcd83913c8..f61a83f4f2 100644 --- a/django/db/migrations/graph.py +++ b/django/db/migrations/graph.py @@ -74,7 +74,7 @@ class MigrationGraph(object): roots.add(node) return roots - def leaf_nodes(self): + def leaf_nodes(self, app=None): """ Returns all leaf nodes - that is, nodes with no dependents in their app. These are the "most current" version of an app's schema. @@ -84,7 +84,7 @@ class MigrationGraph(object): """ leaves = set() for node in self.nodes: - if not any(key[0] == node[0] for key in self.dependents.get(node, set())): + if not any(key[0] == node[0] for key in self.dependents.get(node, set())) and (not app or app == node[0]): leaves.add(node) return leaves