mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	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:
		
				
					committed by
					
						 Tim Graham
						Tim Graham
					
				
			
			
				
	
			
			
			
						parent
						
							8952757698
						
					
				
				
					commit
					a1487deebf
				
			| @@ -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
									
								
								django/core/management/commands/showmigrations.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								django/core/management/commands/showmigrations.py
									
									
									
									
									
										Normal 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)) | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 ...> | ||||
| ----------------------------- | ||||
|  | ||||
|   | ||||
| @@ -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`` | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
|  | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
							
								
								
									
										0
									
								
								tests/migrations/test_migrations_empty/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/migrations/test_migrations_empty/__init__.py
									
									
									
									
									
										Normal file
									
								
							
		Reference in New Issue
	
	Block a user