2018-06-20 15:28:04 +08:00
|
|
|
from django.apps import apps
|
2014-07-30 01:22:00 +08:00
|
|
|
from django.conf import settings
|
2015-01-28 20:35:27 +08:00
|
|
|
from django.core.management.base import BaseCommand, CommandError
|
|
|
|
from django.db import DEFAULT_DB_ALIAS, connections, migrations
|
2015-04-25 22:18:41 +08:00
|
|
|
from django.db.migrations.loader import AmbiguityError, MigrationLoader
|
2014-07-30 01:22:00 +08:00
|
|
|
from django.db.migrations.migration import SwappableTuple
|
2015-01-28 20:35:27 +08:00
|
|
|
from django.db.migrations.optimizer import MigrationOptimizer
|
|
|
|
from django.db.migrations.writer import MigrationWriter
|
2014-12-25 20:30:37 +08:00
|
|
|
from django.utils.version import get_docs_version
|
2013-10-16 19:00:07 +08:00
|
|
|
|
|
|
|
|
|
|
|
class Command(BaseCommand):
|
|
|
|
help = "Squashes an existing set of migrations (from first until specified) into a single new one."
|
|
|
|
|
2014-06-07 04:39:33 +08:00
|
|
|
def add_arguments(self, parser):
|
2016-03-29 06:33:29 +08:00
|
|
|
parser.add_argument(
|
|
|
|
'app_label',
|
|
|
|
help='App label of the application to squash migrations for.',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
2018-07-03 05:54:57 +08:00
|
|
|
'start_migration_name', nargs='?',
|
2016-03-29 06:33:29 +08:00
|
|
|
help='Migrations will be squashed starting from and including this migration.',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'migration_name',
|
|
|
|
help='Migrations will be squashed until and including this migration.',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
2018-07-03 05:54:57 +08:00
|
|
|
'--no-optimize', action='store_true',
|
2016-03-29 06:33:29 +08:00
|
|
|
help='Do not try to optimize the squashed operations.',
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
2017-04-02 08:03:56 +08:00
|
|
|
'--noinput', '--no-input', action='store_false', dest='interactive',
|
2016-03-29 06:33:29 +08:00
|
|
|
help='Tells Django to NOT prompt the user for input of any kind.',
|
|
|
|
)
|
2017-02-20 22:45:20 +08:00
|
|
|
parser.add_argument(
|
2018-07-03 05:54:57 +08:00
|
|
|
'--squashed-name',
|
2017-02-20 22:45:20 +08:00
|
|
|
help='Sets the name of the new squashed migration.',
|
|
|
|
)
|
2013-10-16 19:00:07 +08:00
|
|
|
|
2014-06-07 04:39:33 +08:00
|
|
|
def handle(self, **options):
|
2013-10-16 19:00:07 +08:00
|
|
|
|
2016-03-03 10:01:36 +08:00
|
|
|
self.verbosity = options['verbosity']
|
|
|
|
self.interactive = options['interactive']
|
2014-11-17 01:42:23 +08:00
|
|
|
app_label = options['app_label']
|
2015-09-12 15:18:24 +08:00
|
|
|
start_migration_name = options['start_migration_name']
|
2014-11-17 01:42:23 +08:00
|
|
|
migration_name = options['migration_name']
|
|
|
|
no_optimize = options['no_optimize']
|
2017-02-20 22:45:20 +08:00
|
|
|
squashed_name = options['squashed_name']
|
2018-06-20 15:28:04 +08:00
|
|
|
# Validate app_label.
|
|
|
|
try:
|
|
|
|
apps.get_app_config(app_label)
|
|
|
|
except LookupError as err:
|
|
|
|
raise CommandError(str(err))
|
2013-10-16 19:00:07 +08:00
|
|
|
# Load the current graph state, check the app and migration they asked for exists
|
2015-04-25 22:18:41 +08:00
|
|
|
loader = MigrationLoader(connections[DEFAULT_DB_ALIAS])
|
|
|
|
if app_label not in loader.migrated_apps:
|
2014-09-04 20:15:09 +08:00
|
|
|
raise CommandError(
|
|
|
|
"App '%s' does not have migrations (so squashmigrations on "
|
|
|
|
"it makes no sense)" % app_label
|
|
|
|
)
|
2015-09-12 15:18:24 +08:00
|
|
|
|
|
|
|
migration = self.find_migration(loader, app_label, migration_name)
|
2013-10-16 19:00:07 +08:00
|
|
|
|
|
|
|
# Work out the list of predecessor migrations
|
|
|
|
migrations_to_squash = [
|
2015-04-25 22:18:41 +08:00
|
|
|
loader.get_migration(al, mn)
|
|
|
|
for al, mn in loader.graph.forwards_plan((migration.app_label, migration.name))
|
2013-10-16 19:00:07 +08:00
|
|
|
if al == migration.app_label
|
|
|
|
]
|
|
|
|
|
2015-09-12 15:18:24 +08:00
|
|
|
if start_migration_name:
|
|
|
|
start_migration = self.find_migration(loader, app_label, start_migration_name)
|
|
|
|
start = loader.get_migration(start_migration.app_label, start_migration.name)
|
|
|
|
try:
|
|
|
|
start_index = migrations_to_squash.index(start)
|
|
|
|
migrations_to_squash = migrations_to_squash[start_index:]
|
|
|
|
except ValueError:
|
|
|
|
raise CommandError(
|
|
|
|
"The migration '%s' cannot be found. Maybe it comes after "
|
|
|
|
"the migration '%s'?\n"
|
|
|
|
"Have a look at:\n"
|
|
|
|
" python manage.py showmigrations %s\n"
|
|
|
|
"to debug this issue." % (start_migration, migration, app_label)
|
|
|
|
)
|
|
|
|
|
2013-10-16 19:00:07 +08:00
|
|
|
# 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":
|
2017-01-07 19:11:46 +08:00
|
|
|
answer = input("Do you wish to proceed? [yN] ")
|
2013-10-16 19:00:07 +08:00
|
|
|
if not answer:
|
|
|
|
answer = "n"
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
answer = answer[0].lower()
|
|
|
|
if answer != "y":
|
|
|
|
return
|
|
|
|
|
2014-07-30 01:22:00 +08:00
|
|
|
# Load the operations from all those migrations and concat together,
|
|
|
|
# along with collecting external dependencies and detecting
|
|
|
|
# double-squashing
|
2013-10-16 19:00:07 +08:00
|
|
|
operations = []
|
2014-07-30 01:22:00 +08:00
|
|
|
dependencies = set()
|
2015-09-12 15:18:24 +08:00
|
|
|
# We need to take all dependencies from the first migration in the list
|
|
|
|
# as it may be 0002 depending on 0001
|
|
|
|
first_migration = True
|
2013-10-16 19:00:07 +08:00
|
|
|
for smigration in migrations_to_squash:
|
2014-07-30 01:02:59 +08:00
|
|
|
if smigration.replaces:
|
2014-09-04 20:15:09 +08:00
|
|
|
raise CommandError(
|
|
|
|
"You cannot squash squashed migrations! Please transition "
|
|
|
|
"it to a normal migration first: "
|
2014-12-25 20:30:37 +08:00
|
|
|
"https://docs.djangoproject.com/en/%s/topics/migrations/#squashing-migrations" % get_docs_version()
|
2014-09-04 20:15:09 +08:00
|
|
|
)
|
2013-10-16 19:00:07 +08:00
|
|
|
operations.extend(smigration.operations)
|
2014-07-30 01:22:00 +08:00
|
|
|
for dependency in smigration.dependencies:
|
|
|
|
if isinstance(dependency, SwappableTuple):
|
|
|
|
if settings.AUTH_USER_MODEL == dependency.setting:
|
|
|
|
dependencies.add(("__setting__", "AUTH_USER_MODEL"))
|
|
|
|
else:
|
|
|
|
dependencies.add(dependency)
|
2015-09-12 15:18:24 +08:00
|
|
|
elif dependency[0] != smigration.app_label or first_migration:
|
2014-07-30 01:22:00 +08:00
|
|
|
dependencies.add(dependency)
|
2015-09-12 15:18:24 +08:00
|
|
|
first_migration = False
|
2013-10-16 19:00:07 +08:00
|
|
|
|
2014-11-17 01:42:23 +08:00
|
|
|
if no_optimize:
|
|
|
|
if self.verbosity > 0:
|
|
|
|
self.stdout.write(self.style.MIGRATE_HEADING("(Skipping optimization.)"))
|
|
|
|
new_operations = operations
|
|
|
|
else:
|
|
|
|
if self.verbosity > 0:
|
2018-08-22 21:13:58 +08:00
|
|
|
self.stdout.write(self.style.MIGRATE_HEADING("Optimizing…"))
|
2014-11-17 01:42:23 +08:00
|
|
|
|
|
|
|
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))
|
|
|
|
)
|
2013-10-16 19:00:07 +08:00
|
|
|
|
2013-11-06 23:27:08 +08:00
|
|
|
# Work out the value of replaces (any squashed ones we're re-squashing)
|
|
|
|
# need to feed their replaces into ours
|
|
|
|
replaces = []
|
|
|
|
for migration in migrations_to_squash:
|
|
|
|
if migration.replaces:
|
|
|
|
replaces.extend(migration.replaces)
|
|
|
|
else:
|
|
|
|
replaces.append((migration.app_label, migration.name))
|
|
|
|
|
2013-10-16 19:00:07 +08:00
|
|
|
# Make a new migration with those operations
|
2017-12-29 04:07:29 +08:00
|
|
|
subclass = type("Migration", (migrations.Migration,), {
|
2014-07-30 01:22:00 +08:00
|
|
|
"dependencies": dependencies,
|
2013-10-16 19:00:07 +08:00
|
|
|
"operations": new_operations,
|
2013-11-06 23:27:08 +08:00
|
|
|
"replaces": replaces,
|
2013-10-16 19:00:07 +08:00
|
|
|
})
|
2015-09-12 15:18:24 +08:00
|
|
|
if start_migration_name:
|
2017-02-20 22:45:20 +08:00
|
|
|
if squashed_name:
|
|
|
|
# Use the name from --squashed-name.
|
|
|
|
prefix, _ = start_migration.name.split('_', 1)
|
|
|
|
name = '%s_%s' % (prefix, squashed_name)
|
|
|
|
else:
|
|
|
|
# Generate a name.
|
|
|
|
name = '%s_squashed_%s' % (start_migration.name, migration.name)
|
|
|
|
new_migration = subclass(name, app_label)
|
2015-09-12 15:18:24 +08:00
|
|
|
else:
|
2017-02-20 22:45:20 +08:00
|
|
|
name = '0001_%s' % (squashed_name or 'squashed_%s' % migration.name)
|
|
|
|
new_migration = subclass(name, app_label)
|
2015-09-12 15:18:24 +08:00
|
|
|
new_migration.initial = True
|
2013-10-16 19:00:07 +08:00
|
|
|
|
|
|
|
# Write out the new migration file
|
|
|
|
writer = MigrationWriter(new_migration)
|
2017-01-19 05:05:41 +08:00
|
|
|
with open(writer.path, "w", encoding='utf-8') as fh:
|
2013-10-16 19:00:07 +08:00
|
|
|
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.")
|
2014-07-12 09:59:21 +08:00
|
|
|
if writer.needs_manual_porting:
|
|
|
|
self.stdout.write(self.style.MIGRATE_HEADING("Manual porting required"))
|
|
|
|
self.stdout.write(" Your migrations contained functions that must be manually copied over,")
|
|
|
|
self.stdout.write(" as we could not safely copy their implementation.")
|
|
|
|
self.stdout.write(" See the comment at the top of the squashed migration for details.")
|
2015-09-12 15:18:24 +08:00
|
|
|
|
|
|
|
def find_migration(self, loader, app_label, name):
|
|
|
|
try:
|
|
|
|
return loader.get_migration_by_prefix(app_label, name)
|
|
|
|
except AmbiguityError:
|
|
|
|
raise CommandError(
|
|
|
|
"More than one migration matches '%s' in app '%s'. Please be "
|
|
|
|
"more specific." % (name, app_label)
|
|
|
|
)
|
|
|
|
except KeyError:
|
|
|
|
raise CommandError(
|
|
|
|
"Cannot find a migration matching '%s' from app '%s'." %
|
|
|
|
(name, app_label)
|
|
|
|
)
|