First pass on squashmigrations command; files are right, execution not.
This commit is contained in:
parent
42f8666f6a
commit
763ac8b642
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue