Loading django/core/management/commands/migrate.py +8 −3 Original line number Diff line number Diff line Loading @@ -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: Loading Loading @@ -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: Loading django/core/management/commands/squashmigrations.py 0 → 100644 +108 −0 Original line number Diff line number Diff line 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.") django/db/migrations/loader.py +6 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) Loading django/db/migrations/writer.py +5 −1 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ class MigrationWriter(object): """ items = { "dependencies": repr(self.migration.dependencies), "replaces_str": "", } imports = set() # Deconstruct operations Loading @@ -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 Loading Loading @@ -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 Loading Loading
django/core/management/commands/migrate.py +8 −3 Original line number Diff line number Diff line Loading @@ -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: Loading Loading @@ -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: Loading
django/core/management/commands/squashmigrations.py 0 → 100644 +108 −0 Original line number Diff line number Diff line 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.")
django/db/migrations/loader.py +6 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) Loading
django/db/migrations/writer.py +5 −1 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ class MigrationWriter(object): """ items = { "dependencies": repr(self.migration.dependencies), "replaces_str": "", } imports = set() # Deconstruct operations Loading @@ -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 Loading Loading @@ -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 Loading