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: | ||||
|                     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'. 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)] | ||||
|             target_app_labels_only = False | ||||
|         elif len(args) == 1: | ||||
| @@ -279,10 +279,15 @@ class Command(BaseCommand): | ||||
|             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: | ||||
|                         # 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" % plan_node[1]) | ||||
|                             self.stdout.write(" [X] %s" % title) | ||||
|                         else: | ||||
|                             self.stdout.write(" [ ] %s" % plan_node[1]) | ||||
|                             self.stdout.write(" [ ] %s" % title) | ||||
|                         shown.add(plan_node) | ||||
|             # If we didn't print anything, then a small message | ||||
|             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: | ||||
|                 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): | ||||
|         "Returns the migration(s) which match the given app label and name _prefix_" | ||||
|         # Make sure we have the disk data | ||||
| @@ -160,6 +164,8 @@ class MigrationLoader(object): | ||||
|             # and remove, repointing dependencies if needs be. | ||||
|             for replaced in migration.replaces: | ||||
|                 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] | ||||
|                 for child_key in reverse_dependencies.get(replaced, set()): | ||||
|                     normal[child_key].dependencies.remove(replaced) | ||||
|   | ||||
| @@ -26,6 +26,7 @@ class MigrationWriter(object): | ||||
|         """ | ||||
|         items = { | ||||
|             "dependencies": repr(self.migration.dependencies), | ||||
|             "replaces_str": "", | ||||
|         } | ||||
|         imports = set() | ||||
|         # Deconstruct operations | ||||
| @@ -49,6 +50,9 @@ class MigrationWriter(object): | ||||
|             items["imports"] = "" | ||||
|         else: | ||||
|             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") | ||||
|  | ||||
|     @property | ||||
| @@ -186,7 +190,7 @@ from django.db import models, migrations | ||||
| %(imports)s | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     %(replaces_str)s | ||||
|     dependencies = %(dependencies)s | ||||
|  | ||||
|     operations = %(operations)s | ||||
|   | ||||
		Reference in New Issue
	
	Block a user