From d0da2820cab495c35eac10680213f927be8b91b0 Mon Sep 17 00:00:00 2001 From: Gordon Pendleton Date: Thu, 26 Mar 2020 05:08:58 -0400 Subject: [PATCH] Fixed #31402 -- Added migrate --check option. Command exits with non-zero status if unapplied migrations exist. --- django/core/management/commands/migrate.py | 10 +++++++ docs/ref/django-admin.txt | 7 +++++ docs/releases/3.1.txt | 3 ++ tests/migrations/test_commands.py | 33 ++++++++++++++++++++++ 4 files changed, 53 insertions(+) diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py index 575ed47993..85e04b6fcc 100644 --- a/django/core/management/commands/migrate.py +++ b/django/core/management/commands/migrate.py @@ -1,3 +1,4 @@ +import sys import time from importlib import import_module @@ -62,6 +63,10 @@ class Command(BaseCommand): '--run-syncdb', action='store_true', help='Creates tables for apps without migrations.', ) + parser.add_argument( + '--check', action='store_true', dest='check_unapplied', + help='Exits with a non-zero status if unapplied migrations exist.', + ) @no_translations def handle(self, *args, **options): @@ -143,6 +148,7 @@ class Command(BaseCommand): targets = executor.loader.graph.leaf_nodes() plan = executor.migration_plan(targets) + exit_dry = plan and options['check_unapplied'] if options['plan']: self.stdout.write('Planned operations:', self.style.MIGRATE_LABEL) @@ -154,7 +160,11 @@ class Command(BaseCommand): message, is_error = self.describe_operation(operation, backwards) style = self.style.WARNING if is_error else None self.stdout.write(' ' + message, style) + if exit_dry: + sys.exit(1) return + if exit_dry: + sys.exit(1) # At this point, ignore run_syncdb if there aren't any apps to sync. run_syncdb = options['run_syncdb'] and executor.loader.unmigrated_apps diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 56358b4ef8..4011da32fc 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -865,6 +865,13 @@ with hundreds of models. Suppresses all user prompts. An example prompt is asking about removing stale content types. +.. django-admin-option:: --check + +.. versionadded:: 3.1 + +Makes ``migrate`` exit with a non-zero status when unapplied migrations are +detected. + ``runserver`` ------------- diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index 6cbdac4a1e..ecd70feb2e 100644 --- a/docs/releases/3.1.txt +++ b/docs/releases/3.1.txt @@ -300,6 +300,9 @@ Management Commands enabled for all configured :setting:`DATABASES` by passing the ``database`` tag to the command. +* The new :option:`migrate --check` option makes the command exit with a + non-zero status when unapplied migrations are detected. + Migrations ~~~~~~~~~~ diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index 14c09c8f98..f7f8a68d4c 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -249,6 +249,39 @@ class MigrateTests(MigrationTestBase): with self.assertRaisesMessage(CommandError, "Conflicting migrations detected"): call_command("migrate", "migrations") + @override_settings(MIGRATION_MODULES={ + 'migrations': 'migrations.test_migrations', + }) + def test_migrate_check(self): + with self.assertRaises(SystemExit): + call_command('migrate', 'migrations', '0001', check_unapplied=True, verbosity=0) + self.assertTableNotExists('migrations_author') + self.assertTableNotExists('migrations_tribble') + self.assertTableNotExists('migrations_book') + + @override_settings(MIGRATION_MODULES={ + 'migrations': 'migrations.test_migrations_plan', + }) + def test_migrate_check_plan(self): + out = io.StringIO() + with self.assertRaises(SystemExit): + call_command( + 'migrate', + 'migrations', + '0001', + check_unapplied=True, + plan=True, + stdout=out, + no_color=True, + ) + self.assertEqual( + 'Planned operations:\n' + 'migrations.0001_initial\n' + ' Create model Salamander\n' + ' Raw Python operation -> Grow salamander tail.\n', + out.getvalue(), + ) + @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"}) def test_showmigrations_list(self): """