diff options
| author | David Wobrock <david.wobrock@gmail.com> | 2022-06-14 20:12:20 +0200 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2022-06-17 07:50:39 +0200 |
| commit | e286ce17ff8bf72a248ce3177bbd5163c50a6ded (patch) | |
| tree | d1aaed2412a29c44be845821ad5434a6aedaed2a /django/core | |
| parent | 3893fcdd94498da8f88ae7313b6939ab09bff509 (diff) | |
Fixed #24870 -- Added --update option to makemigrations command.
Diffstat (limited to 'django/core')
| -rw-r--r-- | django/core/management/commands/makemigrations.py | 107 |
1 files changed, 105 insertions, 2 deletions
diff --git a/django/core/management/commands/makemigrations.py b/django/core/management/commands/makemigrations.py index ec6b71f5a0..2e97d9064b 100644 --- a/django/core/management/commands/makemigrations.py +++ b/django/core/management/commands/makemigrations.py @@ -11,6 +11,8 @@ from django.db import DEFAULT_DB_ALIAS, OperationalError, connections, router from django.db.migrations import Migration from django.db.migrations.autodetector import MigrationAutodetector from django.db.migrations.loader import MigrationLoader +from django.db.migrations.migration import SwappableTuple +from django.db.migrations.optimizer import MigrationOptimizer from django.db.migrations.questioner import ( InteractiveMigrationQuestioner, MigrationQuestioner, @@ -79,6 +81,15 @@ class Command(BaseCommand): "paths of generated migration files to stdout." ), ) + parser.add_argument( + "--update", + action="store_true", + dest="update", + help=( + "Merge model changes into the latest migration and optimize the " + "resulting operations." + ), + ) @property def log_output(self): @@ -101,6 +112,7 @@ class Command(BaseCommand): self.include_header = options["include_header"] check_changes = options["check_changes"] self.scriptable = options["scriptable"] + self.update = options["update"] # If logs and prompts are diverted to stderr, remove the ERROR style. if self.scriptable: self.stderr.style_func = None @@ -236,11 +248,86 @@ class Command(BaseCommand): else: self.log("No changes detected") else: - self.write_migration_files(changes) + if self.update: + self.write_to_last_migration_files(changes) + else: + self.write_migration_files(changes) if check_changes: sys.exit(1) - def write_migration_files(self, changes): + def write_to_last_migration_files(self, changes): + loader = MigrationLoader(connections[DEFAULT_DB_ALIAS]) + new_changes = {} + update_previous_migration_paths = {} + for app_label, app_migrations in changes.items(): + # Find last migration. + leaf_migration_nodes = loader.graph.leaf_nodes(app=app_label) + if len(leaf_migration_nodes) == 0: + raise CommandError( + f"App {app_label} has no migration, cannot update last migration." + ) + leaf_migration_node = leaf_migration_nodes[0] + # Multiple leaf nodes have already been checked earlier in command. + leaf_migration = loader.graph.nodes[leaf_migration_node] + # Updated migration cannot be a squash migration, a dependency of + # another migration, and cannot be already applied. + if leaf_migration.replaces: + raise CommandError( + f"Cannot update squash migration '{leaf_migration}'." + ) + if leaf_migration_node in loader.applied_migrations: + raise CommandError( + f"Cannot update applied migration '{leaf_migration}'." + ) + depending_migrations = [ + migration + for migration in loader.disk_migrations.values() + if leaf_migration_node in migration.dependencies + ] + if depending_migrations: + formatted_migrations = ", ".join( + [f"'{migration}'" for migration in depending_migrations] + ) + raise CommandError( + f"Cannot update migration '{leaf_migration}' that migrations " + f"{formatted_migrations} depend on." + ) + # Build new migration. + for migration in app_migrations: + leaf_migration.operations.extend(migration.operations) + + for dependency in migration.dependencies: + if isinstance(dependency, SwappableTuple): + if settings.AUTH_USER_MODEL == dependency.setting: + leaf_migration.dependencies.append( + ("__setting__", "AUTH_USER_MODEL") + ) + else: + leaf_migration.dependencies.append(dependency) + elif dependency[0] != migration.app_label: + leaf_migration.dependencies.append(dependency) + # Optimize migration. + optimizer = MigrationOptimizer() + leaf_migration.operations = optimizer.optimize( + leaf_migration.operations, app_label + ) + # Update name. + previous_migration_path = MigrationWriter(leaf_migration).path + suggested_name = ( + leaf_migration.name[:4] + "_" + leaf_migration.suggest_name() + ) + if leaf_migration.name == suggested_name: + new_name = leaf_migration.name + "_updated" + else: + new_name = suggested_name + leaf_migration.name = new_name + # Register overridden migration. + new_changes[app_label] = [leaf_migration] + update_previous_migration_paths[app_label] = previous_migration_path + + self.write_migration_files(new_changes, update_previous_migration_paths) + + def write_migration_files(self, changes, update_previous_migration_paths=None): """ Take a changes dict and write them out as migration files. """ @@ -274,6 +361,22 @@ class Command(BaseCommand): with open(writer.path, "w", encoding="utf-8") as fh: fh.write(migration_string) self.written_files.append(writer.path) + if update_previous_migration_paths: + prev_path = update_previous_migration_paths[app_label] + rel_prev_path = self.get_relative_path(prev_path) + if writer.needs_manual_porting: + migration_path = self.get_relative_path(writer.path) + self.log( + self.style.WARNING( + f"Updated migration {migration_path} requires " + f"manual porting.\n" + f"Previous migration {rel_prev_path} was kept and " + f"must be deleted after porting functions manually." + ) + ) + else: + os.remove(prev_path) + self.log(f"Deleted {rel_prev_path}") elif self.verbosity == 3: # Alternatively, makemigrations --dry-run --verbosity 3 # will log the migrations rather than saving the file to |
