summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGordon Pendleton <wgordonw1@gmail.com>2020-03-26 05:08:58 -0400
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-03-26 20:12:39 +0100
commitd0da2820cab495c35eac10680213f927be8b91b0 (patch)
tree0f065f06f8c84994741acc57f0814fb8677f3cc0
parent421622548060499881df9966b7a352bce63901cd (diff)
Fixed #31402 -- Added migrate --check option.
Command exits with non-zero status if unapplied migrations exist.
-rw-r--r--django/core/management/commands/migrate.py10
-rw-r--r--docs/ref/django-admin.txt7
-rw-r--r--docs/releases/3.1.txt3
-rw-r--r--tests/migrations/test_commands.py33
4 files changed, 53 insertions, 0 deletions
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):
"""