Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions django/core/management/commands/showmigrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ def add_arguments(self, parser):
'"default" database.'
),
)
parser.add_argument(
"--unapplied",
"-u",
action="store_true",
help=(
"Like `--list`, but only shows unapplied migrations"
),
)

formats = parser.add_mutually_exclusive_group()
formats.add_argument(
Expand Down Expand Up @@ -55,15 +63,18 @@ def add_arguments(self, parser):

def handle(self, *args, **options):
self.verbosity = options["verbosity"]

self.show_unapplied_only = options["unapplied"]
# Get the database we're operating from
db = options["database"]
connection = connections[db]

if options["format"] == "plan":
return self.show_plan(connection, options["app_label"])
else:
return self.show_list(connection, options["app_label"])
return self.show_list(connection,
options["app_label"],
show_unapplied_only=self.show_unapplied_only,
)

def _validate_app_names(self, loader, app_names):
has_bad_names = False
Expand All @@ -76,7 +87,7 @@ def _validate_app_names(self, loader, app_names):
if has_bad_names:
sys.exit(2)

def show_list(self, connection, app_names=None):
def show_list(self, connection, app_names=None, show_unapplied_only=False):
"""
Show a list of all migrations on the system, or only those of
some named apps.
Expand All @@ -95,8 +106,8 @@ def show_list(self, connection, app_names=None):
# For each app, print its migrations in order from oldest (roots) to
# newest (leaves).
for app_name in app_names:
self.stdout.write(app_name, self.style.MIGRATE_LABEL)
shown = set()
outputs = []
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit hacky, and the existing code could also use some refactoring to seperate out some concerns.

But I want to be able to not print an app label if there aren't associated migrations to print out with it.

for node in graph.leaf_nodes(app_name):
for plan_node in graph.forwards_plan(node):
if plan_node not in shown and plan_node[0] == app_name:
Expand All @@ -123,12 +134,20 @@ def show_list(self, connection, app_names=None):
"%Y-%m-%d %H:%M:%S"
)
)
self.stdout.write(output)
if not show_unapplied_only:
outputs.append(output)
else:
self.stdout.write(" [ ] %s" % title)
output = " [ ] %s" % title
outputs.append(output)
shown.add(plan_node)
if outputs:
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally I'd separate out the concerns of fetching the plans, determining what to print, and the print format into separate functions.

There is some duplication between the list and plan that could also be consolidated or made a bit more consistent in approach to make it easier to maintain.

self.stdout.write(app_name, self.style.MIGRATE_LABEL)
for output in outputs:
self.stdout.write(output)

# If we didn't print anything, then a small message
if not shown:
self.stdout.write(app_name, self.style.MIGRATE_LABEL)
self.stdout.write(" (no migrations)", self.style.ERROR)

def show_plan(self, connection, app_names=None):
Expand Down
44 changes: 44 additions & 0 deletions tests/migrations/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,49 @@ def test_showmigrations_list(self):
# Cleanup by unmigrating everything
call_command("migrate", "migrations", "zero", verbosity=0)

@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
def test_showmigrations_show_unapplied_only(self):
"""
showmigrations --unapplied only displays unapplied migrations
"""
out = io.StringIO()

call_command("migrate", "migrations", "0001", verbosity=0)

# Giving the explicit app_label tests for selective `show_list` in the command
call_command(
"showmigrations",
"migrations",
unapplied=True,
format="list",
stdout=out,
verbosity=0,
no_color=True,
)
self.assertEqual("migrations\n [ ] 0002_second\n", out.getvalue().lower())
# Cleanup by unmigrating everything
call_command("migrate", "migrations", "zero", verbosity=0)

@override_settings(
INSTALLED_APPS=[
"migrations.migrations_test_apps.mutate_state_b",
"migrations.migrations_test_apps.alter_fk.author_app",
"migrations.migrations_test_apps.alter_fk.book_app",
]
)
def test_showmigrations_unapplied_multiple_app_labels(self):
"""
`showmigrations --plan app_label` output with multiple app_labels.
"""
# Multiple apps: author_app depends on book_app; mutate_state_b doesn't
# depend on other apps.
out = io.StringIO()

# run all migrations for author app
call_command("migrate", "author_app", verbosity=0)
call_command("showmigrations", unapplied=True, format="list", stdout=out)
self.assertNotIn("author_app", out.getvalue())

@override_settings(
MIGRATION_MODULES={"migrations": "migrations.test_migrations_squashed"}
)
Expand Down Expand Up @@ -820,6 +863,7 @@ def test_showmigrations_plan_multiple_app_labels(self):
call_command(
"showmigrations", "author_app", "mutate_state_b", format="plan", stdout=out
)

self.assertEqual(
"[ ] author_app.0001_initial\n"
"[ ] book_app.0001_initial\n"
Expand Down