showmigrations.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import sys
  2. from django.apps import apps
  3. from django.core.management.base import BaseCommand
  4. from django.db import DEFAULT_DB_ALIAS, connections
  5. from django.db.migrations.loader import MigrationLoader
  6. from django.db.migrations.recorder import MigrationRecorder
  7. class Command(BaseCommand):
  8. help = "Shows all available migrations for the current project"
  9. def add_arguments(self, parser):
  10. parser.add_argument(
  11. "app_label",
  12. nargs="*",
  13. help="App labels of applications to limit the output to.",
  14. )
  15. parser.add_argument(
  16. "--database",
  17. default=DEFAULT_DB_ALIAS,
  18. help=(
  19. "Nominates a database to show migrations for. Defaults to the "
  20. '"default" database.'
  21. ),
  22. )
  23. formats = parser.add_mutually_exclusive_group()
  24. formats.add_argument(
  25. "--list",
  26. "-l",
  27. action="store_const",
  28. dest="format",
  29. const="list",
  30. help=(
  31. "Shows a list of all migrations and which are applied. "
  32. "With a verbosity level of 2 or above, the applied datetimes "
  33. "will be included."
  34. ),
  35. )
  36. formats.add_argument(
  37. "--plan",
  38. "-p",
  39. action="store_const",
  40. dest="format",
  41. const="plan",
  42. help=(
  43. "Shows all migrations in the order they will be applied. With a "
  44. "verbosity level of 2 or above all direct migration dependencies and "
  45. "reverse dependencies (run_before) will be included."
  46. ),
  47. )
  48. parser.set_defaults(format="list")
  49. def handle(self, *args, **options):
  50. self.verbosity = options["verbosity"]
  51. # Get the database we're operating from
  52. db = options["database"]
  53. connection = connections[db]
  54. if options["format"] == "plan":
  55. return self.show_plan(connection, options["app_label"])
  56. else:
  57. return self.show_list(connection, options["app_label"])
  58. def _validate_app_names(self, loader, app_names):
  59. has_bad_names = False
  60. for app_name in app_names:
  61. try:
  62. apps.get_app_config(app_name)
  63. except LookupError as err:
  64. self.stderr.write(str(err))
  65. has_bad_names = True
  66. if has_bad_names:
  67. sys.exit(2)
  68. def show_list(self, connection, app_names=None):
  69. """
  70. Show a list of all migrations on the system, or only those of
  71. some named apps.
  72. """
  73. # Load migrations from disk/DB
  74. loader = MigrationLoader(connection, ignore_no_migrations=True)
  75. recorder = MigrationRecorder(connection)
  76. recorded_migrations = recorder.applied_migrations()
  77. graph = loader.graph
  78. # If we were passed a list of apps, validate it
  79. if app_names:
  80. self._validate_app_names(loader, app_names)
  81. # Otherwise, show all apps in alphabetic order
  82. else:
  83. app_names = sorted(loader.migrated_apps)
  84. # For each app, print its migrations in order from oldest (roots) to
  85. # newest (leaves).
  86. for app_name in app_names:
  87. self.stdout.write(app_name, self.style.MIGRATE_LABEL)
  88. shown = set()
  89. for node in graph.leaf_nodes(app_name):
  90. for plan_node in graph.forwards_plan(node):
  91. if plan_node not in shown and plan_node[0] == app_name:
  92. # Give it a nice title if it's a squashed one
  93. title = plan_node[1]
  94. if graph.nodes[plan_node].replaces:
  95. title += " (%s squashed migrations)" % len(
  96. graph.nodes[plan_node].replaces
  97. )
  98. applied_migration = loader.applied_migrations.get(plan_node)
  99. # Mark it as applied/unapplied
  100. if applied_migration:
  101. if plan_node in recorded_migrations:
  102. output = " [X] %s" % title
  103. else:
  104. title += " Run 'manage.py migrate' to finish recording."
  105. output = " [-] %s" % title
  106. if self.verbosity >= 2 and hasattr(
  107. applied_migration, "applied"
  108. ):
  109. output += (
  110. " (applied at %s)"
  111. % applied_migration.applied.strftime(
  112. "%Y-%m-%d %H:%M:%S"
  113. )
  114. )
  115. self.stdout.write(output)
  116. else:
  117. self.stdout.write(" [ ] %s" % title)
  118. shown.add(plan_node)
  119. # If we didn't print anything, then a small message
  120. if not shown:
  121. self.stdout.write(" (no migrations)", self.style.ERROR)
  122. def show_plan(self, connection, app_names=None):
  123. """
  124. Show all known migrations (or only those of the specified app_names)
  125. in the order they will be applied.
  126. """
  127. # Load migrations from disk/DB
  128. loader = MigrationLoader(connection)
  129. graph = loader.graph
  130. if app_names:
  131. self._validate_app_names(loader, app_names)
  132. targets = [key for key in graph.leaf_nodes() if key[0] in app_names]
  133. else:
  134. targets = graph.leaf_nodes()
  135. plan = []
  136. seen = set()
  137. # Generate the plan
  138. for target in targets:
  139. for migration in graph.forwards_plan(target):
  140. if migration not in seen:
  141. node = graph.node_map[migration]
  142. plan.append(node)
  143. seen.add(migration)
  144. # Output
  145. def print_deps(node):
  146. out = []
  147. for parent in sorted(node.parents):
  148. out.append("%s.%s" % parent.key)
  149. if out:
  150. return " ... (%s)" % ", ".join(out)
  151. return ""
  152. for node in plan:
  153. deps = ""
  154. if self.verbosity >= 2:
  155. deps = print_deps(node)
  156. if node.key in loader.applied_migrations:
  157. self.stdout.write("[X] %s.%s%s" % (node.key[0], node.key[1], deps))
  158. else:
  159. self.stdout.write("[ ] %s.%s%s" % (node.key[0], node.key[1], deps))
  160. if not plan:
  161. self.stdout.write("(no migrations)", self.style.ERROR)