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:
|
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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue