diff --git a/django/db/migrations/graph.py b/django/db/migrations/graph.py index c1116fd444..1f0bafb00e 100644 --- a/django/db/migrations/graph.py +++ b/django/db/migrations/graph.py @@ -356,7 +356,7 @@ class MigrationGraph: plan = [] for node in nodes: for migration in self.forwards_plan(node): - if migration in plan or at_end or migration not in nodes: + if migration not in plan and (at_end or migration not in nodes): plan.append(migration) return plan diff --git a/tests/migrations/test_loader.py b/tests/migrations/test_loader.py index 3d0cc750d2..e0e7e3d258 100644 --- a/tests/migrations/test_loader.py +++ b/tests/migrations/test_loader.py @@ -81,6 +81,22 @@ class LoaderTests(TestCase): # Ensure we've included unmigrated apps in there too self.assertIn("basic", project_state.real_apps) + @override_settings(MIGRATION_MODULES={ + 'migrations': 'migrations.test_migrations', + 'migrations2': 'migrations2.test_migrations_2', + }) + @modify_settings(INSTALLED_APPS={'append': 'migrations2'}) + def test_plan_handles_repeated_migrations(self): + """ + _generate_plan() doesn't readd migrations already in the plan (#29180). + """ + migration_loader = MigrationLoader(connection) + nodes = [('migrations', '0002_second'), ('migrations2', '0001_initial')] + self.assertEqual( + migration_loader.graph._generate_plan(nodes, at_end=True), + [('migrations', '0001_initial'), ('migrations', '0002_second'), ('migrations2', '0001_initial')] + ) + @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_unmigdep"}) def test_load_unmigrated_dependency(self): """