Commit 763ac8b6 authored by Andrew Godwin's avatar Andrew Godwin
Browse files

First pass on squashmigrations command; files are right, execution not.

parent 42f8666f
Loading
Loading
Loading
Loading
+8 −3
Original line number Diff line number Diff line
@@ -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 −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.")
+6 −0
Original line number Diff line number Diff line
@@ -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)
+5 −1
Original line number Diff line number Diff line
@@ -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