diff --git a/django/core/management/commands/squashmigrations.py b/django/core/management/commands/squashmigrations.py index a784bfb345..60faf6cc1f 100644 --- a/django/core/management/commands/squashmigrations.py +++ b/django/core/management/commands/squashmigrations.py @@ -32,6 +32,10 @@ class Command(BaseCommand): '--noinput', '--no-input', action='store_false', dest='interactive', help='Tells Django to NOT prompt the user for input of any kind.', ) + parser.add_argument( + '--squashed-name', dest='squashed_name', + help='Sets the name of the new squashed migration.', + ) def handle(self, **options): @@ -41,6 +45,7 @@ class Command(BaseCommand): start_migration_name = options['start_migration_name'] migration_name = options['migration_name'] no_optimize = options['no_optimize'] + squashed_name = options['squashed_name'] # Load the current graph state, check the app and migration they asked for exists loader = MigrationLoader(connections[DEFAULT_DB_ALIAS]) @@ -154,9 +159,17 @@ class Command(BaseCommand): "replaces": replaces, }) if start_migration_name: - new_migration = subclass("%s_squashed_%s" % (start_migration.name, migration.name), app_label) + 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) else: - new_migration = subclass("0001_squashed_%s" % migration.name, app_label) + name = '0001_%s' % (squashed_name or 'squashed_%s' % migration.name) + new_migration = subclass(name, app_label) new_migration.initial = True # Write out the new migration file diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 20e1bb02de..3a83bd2b7d 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -1132,6 +1132,13 @@ behavior, as optimization is meant to be safe. Suppresses all user prompts. +.. django-admin-option:: --squashed-name SQUASHED_NAME + +.. versionadded:: 2.0 + +Sets the name of the squashed migration. When omitted, the name is based on the +first and last migration, with ``_squashed_`` in between. + ``startapp`` ------------ diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt index 0757d834f9..36359e4979 100644 --- a/docs/releases/2.0.txt +++ b/docs/releases/2.0.txt @@ -219,7 +219,8 @@ Management Commands Migrations ~~~~~~~~~~ -* ... +* The new :option:`squashmigrations --squashed-name` option allows naming + the squashed migration. Models ~~~~~~ diff --git a/docs/topics/migrations.txt b/docs/topics/migrations.txt index a2eae4ff3d..6f0e3b912e 100644 --- a/docs/topics/migrations.txt +++ b/docs/topics/migrations.txt @@ -605,6 +605,9 @@ work:: all instances of the codebase have applied the migrations you squashed, you can delete them. +Use the :option:`squashmigrations --squashed-name` option if you want to set +the name of the squashed migration rather than use an autogenerated one. + Note that model interdependencies in Django can get very complex, and squashing may result in migrations that do not run; either mis-optimized (in which case you can try again with ``--no-optimize``, though you should also report an issue), diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index 777081e3c7..3e4f7b5410 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -1356,3 +1356,25 @@ class SquashMigrationsTests(MigrationTestBase): ) with self.assertRaisesMessage(CommandError, msg): call_command("squashmigrations", "migrations", "0003", "0002", interactive=False, verbosity=0) + + def test_squashed_name_with_start_migration_name(self): + """--squashed-name specifies the new migration's name.""" + squashed_name = 'squashed_name' + with self.temporary_migration_module(module='migrations.test_migrations') as migration_dir: + call_command( + 'squashmigrations', 'migrations', '0001', '0002', + squashed_name=squashed_name, interactive=False, verbosity=0, + ) + squashed_migration_file = os.path.join(migration_dir, '0001_%s.py' % squashed_name) + self.assertTrue(os.path.exists(squashed_migration_file)) + + def test_squashed_name_without_start_migration_name(self): + """--squashed-name also works if a start migration is omitted.""" + squashed_name = 'squashed_name' + with self.temporary_migration_module(module="migrations.test_migrations") as migration_dir: + call_command( + 'squashmigrations', 'migrations', '0001', + squashed_name=squashed_name, interactive=False, verbosity=0, + ) + squashed_migration_file = os.path.join(migration_dir, '0001_%s.py' % squashed_name) + self.assertTrue(os.path.exists(squashed_migration_file))