mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	First pass on squashmigrations command; files are right, execution not.
This commit is contained in:
		| @@ -76,7 +76,7 @@ class Command(BaseCommand): | |||||||
|                 except AmbiguityError: |                 except AmbiguityError: | ||||||
|                     raise CommandError("More than one migration matches '%s' in app '%s'. Please be more specific." % (app_label, migration_name)) |                     raise CommandError("More than one migration matches '%s' in app '%s'. Please be more specific." % (app_label, migration_name)) | ||||||
|                 except KeyError: |                 except KeyError: | ||||||
|                     raise CommandError("Cannot find a migration matching '%s' from app '%s'. Is it in INSTALLED_APPS?" % (app_label, migration_name)) |                     raise CommandError("Cannot find a migration matching '%s' from app '%s'." % (app_label, migration_name)) | ||||||
|                 targets = [(app_label, migration.name)] |                 targets = [(app_label, migration.name)] | ||||||
|             target_app_labels_only = False |             target_app_labels_only = False | ||||||
|         elif len(args) == 1: |         elif len(args) == 1: | ||||||
| @@ -279,10 +279,15 @@ class Command(BaseCommand): | |||||||
|             for node in graph.leaf_nodes(app): |             for node in graph.leaf_nodes(app): | ||||||
|                 for plan_node in graph.forwards_plan(node): |                 for plan_node in graph.forwards_plan(node): | ||||||
|                     if plan_node not in shown and plan_node[0] == app: |                     if plan_node not in shown and plan_node[0] == app: | ||||||
|  |                         # 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: |                         if plan_node in loader.applied_migrations: | ||||||
|                             self.stdout.write(" [X] %s" % plan_node[1]) |                             self.stdout.write(" [X] %s" % title) | ||||||
|                         else: |                         else: | ||||||
|                             self.stdout.write(" [ ] %s" % plan_node[1]) |                             self.stdout.write(" [ ] %s" % title) | ||||||
|                         shown.add(plan_node) |                         shown.add(plan_node) | ||||||
|             # If we didn't print anything, then a small message |             # If we didn't print anything, then a small message | ||||||
|             if not shown: |             if not shown: | ||||||
|   | |||||||
							
								
								
									
										108
									
								
								django/core/management/commands/squashmigrations.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								django/core/management/commands/squashmigrations.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | import sys | ||||||
|  | import os | ||||||
|  | from optparse import make_option | ||||||
|  |  | ||||||
|  | from django.core.management.base import BaseCommand, CommandError | ||||||
|  | from django.core.exceptions import ImproperlyConfigured | ||||||
|  | from django.utils import six | ||||||
|  | from django.db import connections, DEFAULT_DB_ALIAS, migrations | ||||||
|  | from django.db.migrations.loader import MigrationLoader, AmbiguityError | ||||||
|  | from django.db.migrations.autodetector import MigrationAutodetector, InteractiveMigrationQuestioner | ||||||
|  | from django.db.migrations.executor import MigrationExecutor | ||||||
|  | from django.db.migrations.writer import MigrationWriter | ||||||
|  | from django.db.models.loading import cache | ||||||
|  | from django.db.migrations.optimizer import MigrationOptimizer | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Command(BaseCommand): | ||||||
|  |     option_list = BaseCommand.option_list + ( | ||||||
|  |         make_option('--no-optimize', action='store_true', dest='no_optimize', default=False, | ||||||
|  |             help='Do not try to optimize the squashed operations.'), | ||||||
|  |         make_option('--noinput', action='store_false', dest='interactive', default=True, | ||||||
|  |             help='Tells Django to NOT prompt the user for input of any kind.'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     help = "Squashes an existing set of migrations (from first until specified) into a single new one." | ||||||
|  |     usage_str = "Usage: ./manage.py squashmigrations app migration_name" | ||||||
|  |  | ||||||
|  |     def handle(self, app_label=None, migration_name=None, **options): | ||||||
|  |  | ||||||
|  |         self.verbosity = int(options.get('verbosity')) | ||||||
|  |         self.interactive = options.get('interactive') | ||||||
|  |  | ||||||
|  |         if app_label is None or migration_name is None: | ||||||
|  |             self.stderr.write(self.usage_str) | ||||||
|  |             sys.exit(1) | ||||||
|  |  | ||||||
|  |         # Load the current graph state, check the app and migration they asked for exists | ||||||
|  |         executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS]) | ||||||
|  |         if app_label not in executor.loader.migrated_apps: | ||||||
|  |             raise CommandError("App '%s' does not have migrations (so squashmigrations on it makes no sense)" % app_label) | ||||||
|  |         try: | ||||||
|  |             migration = executor.loader.get_migration_by_prefix(app_label, migration_name) | ||||||
|  |         except AmbiguityError: | ||||||
|  |             raise CommandError("More than one migration matches '%s' in app '%s'. Please be more specific." % (app_label, migration_name)) | ||||||
|  |         except KeyError: | ||||||
|  |             raise CommandError("Cannot find a migration matching '%s' from app '%s'." % (app_label, migration_name)) | ||||||
|  |  | ||||||
|  |         # Work out the list of predecessor migrations | ||||||
|  |         migrations_to_squash = [ | ||||||
|  |             executor.loader.get_migration(al, mn) | ||||||
|  |             for al, mn in executor.loader.graph.forwards_plan((migration.app_label, migration.name)) | ||||||
|  |             if al == migration.app_label | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         # Tell them what we're doing and optionally ask if we should proceed | ||||||
|  |         if self.verbosity > 0 or self.interactive: | ||||||
|  |             self.stdout.write(self.style.MIGRATE_HEADING("Will squash the following migrations:")) | ||||||
|  |             for migration in migrations_to_squash: | ||||||
|  |                 self.stdout.write(" - %s" % migration.name) | ||||||
|  |  | ||||||
|  |             if self.interactive: | ||||||
|  |                 answer = None | ||||||
|  |                 while not answer or answer not in "yn": | ||||||
|  |                     answer = six.moves.input("Do you wish to proceed? [yN] ") | ||||||
|  |                     if not answer: | ||||||
|  |                         answer = "n" | ||||||
|  |                         break | ||||||
|  |                     else: | ||||||
|  |                         answer = answer[0].lower() | ||||||
|  |                 if answer != "y": | ||||||
|  |                     return | ||||||
|  |  | ||||||
|  |         # Load the operations from all those migrations and concat together | ||||||
|  |         operations = [] | ||||||
|  |         for smigration in migrations_to_squash: | ||||||
|  |             operations.extend(smigration.operations) | ||||||
|  |  | ||||||
|  |         if self.verbosity > 0: | ||||||
|  |             self.stdout.write(self.style.MIGRATE_HEADING("Optimizing...")) | ||||||
|  |  | ||||||
|  |         optimizer = MigrationOptimizer() | ||||||
|  |         new_operations = optimizer.optimize(operations, migration.app_label) | ||||||
|  |  | ||||||
|  |         if self.verbosity > 0: | ||||||
|  |             if len(new_operations) == len(operations): | ||||||
|  |                 self.stdout.write("  No optimizations possible.") | ||||||
|  |             else: | ||||||
|  |                 self.stdout.write("  Optimized from %s operations to %s operations." % (len(operations), len(new_operations))) | ||||||
|  |  | ||||||
|  |         # Make a new migration with those operations | ||||||
|  |         subclass = type("Migration", (migrations.Migration, ), { | ||||||
|  |             "dependencies": [], | ||||||
|  |             "operations": new_operations, | ||||||
|  |             "replaces": [(m.app_label, m.name) for m in migrations_to_squash], | ||||||
|  |         }) | ||||||
|  |         new_migration = subclass("0001_squashed_%s" % migration.name, app_label) | ||||||
|  |  | ||||||
|  |         # Write out the new migration file | ||||||
|  |         writer = MigrationWriter(new_migration) | ||||||
|  |         with open(writer.path, "wb") as fh: | ||||||
|  |             fh.write(writer.as_string()) | ||||||
|  |  | ||||||
|  |         if self.verbosity > 0: | ||||||
|  |             self.stdout.write(self.style.MIGRATE_HEADING("Created new squashed migration %s" % writer.path)) | ||||||
|  |             self.stdout.write("  You should commit this migration but leave the old ones in place;") | ||||||
|  |             self.stdout.write("  the new migration will be used for new installs. Once you are sure") | ||||||
|  |             self.stdout.write("  all instances of the codebase have applied the migrations you squashed,") | ||||||
|  |             self.stdout.write("  you can delete them.") | ||||||
| @@ -101,6 +101,10 @@ class MigrationLoader(object): | |||||||
|             if south_style_migrations: |             if south_style_migrations: | ||||||
|                 self.unmigrated_apps.add(app_label) |                 self.unmigrated_apps.add(app_label) | ||||||
|  |  | ||||||
|  |     def get_migration(self, app_label, name_prefix): | ||||||
|  |         "Gets the migration exactly named, or raises KeyError" | ||||||
|  |         return self.graph.nodes[app_label, name_prefix] | ||||||
|  |  | ||||||
|     def get_migration_by_prefix(self, app_label, name_prefix): |     def get_migration_by_prefix(self, app_label, name_prefix): | ||||||
|         "Returns the migration(s) which match the given app label and name _prefix_" |         "Returns the migration(s) which match the given app label and name _prefix_" | ||||||
|         # Make sure we have the disk data |         # Make sure we have the disk data | ||||||
| @@ -160,6 +164,8 @@ class MigrationLoader(object): | |||||||
|             # and remove, repointing dependencies if needs be. |             # and remove, repointing dependencies if needs be. | ||||||
|             for replaced in migration.replaces: |             for replaced in migration.replaces: | ||||||
|                 if replaced in normal: |                 if replaced in normal: | ||||||
|  |                     # We don't care if the replaced migration doesn't exist; | ||||||
|  |                     # the usage pattern here is to delete things after a while. | ||||||
|                     del normal[replaced] |                     del normal[replaced] | ||||||
|                 for child_key in reverse_dependencies.get(replaced, set()): |                 for child_key in reverse_dependencies.get(replaced, set()): | ||||||
|                     normal[child_key].dependencies.remove(replaced) |                     normal[child_key].dependencies.remove(replaced) | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ class MigrationWriter(object): | |||||||
|         """ |         """ | ||||||
|         items = { |         items = { | ||||||
|             "dependencies": repr(self.migration.dependencies), |             "dependencies": repr(self.migration.dependencies), | ||||||
|  |             "replaces_str": "", | ||||||
|         } |         } | ||||||
|         imports = set() |         imports = set() | ||||||
|         # Deconstruct operations |         # Deconstruct operations | ||||||
| @@ -49,6 +50,9 @@ class MigrationWriter(object): | |||||||
|             items["imports"] = "" |             items["imports"] = "" | ||||||
|         else: |         else: | ||||||
|             items["imports"] = "\n".join(imports) + "\n" |             items["imports"] = "\n".join(imports) + "\n" | ||||||
|  |         # If there's a replaces, make a string for it | ||||||
|  |         if self.migration.replaces: | ||||||
|  |             items['replaces_str'] = "\n    replaces = %s\n" % repr(self.migration.replaces) | ||||||
|         return (MIGRATION_TEMPLATE % items).encode("utf8") |         return (MIGRATION_TEMPLATE % items).encode("utf8") | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
| @@ -186,7 +190,7 @@ from django.db import models, migrations | |||||||
| %(imports)s | %(imports)s | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): | class Migration(migrations.Migration): | ||||||
|  |     %(replaces_str)s | ||||||
|     dependencies = %(dependencies)s |     dependencies = %(dependencies)s | ||||||
|  |  | ||||||
|     operations = %(operations)s |     operations = %(operations)s | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user