Fixed #26429 -- Added a timestamp to merge migration names.

This reduces the possibility of a naming conflict, especially after
squashing migrations.
This commit is contained in:
Raphael Gaschignard 2016-05-11 21:19:19 +09:00 committed by Tim Graham
parent 535660b852
commit 8f6a1a1551
5 changed files with 48 additions and 20 deletions

View File

@ -76,7 +76,7 @@ def get_commands():
return commands return commands
def call_command(name, *args, **options): def call_command(command_name, *args, **options):
""" """
Calls the given command, with the given options and args/kwargs. Calls the given command, with the given options and args/kwargs.
@ -95,25 +95,25 @@ def call_command(name, *args, **options):
call_command(cmd, verbosity=0, interactive=False) call_command(cmd, verbosity=0, interactive=False)
# Do something with cmd ... # Do something with cmd ...
""" """
if isinstance(name, BaseCommand): if isinstance(command_name, BaseCommand):
# Command object passed in. # Command object passed in.
command = name command = command_name
name = command.__class__.__module__.split('.')[-1] command_name = command.__class__.__module__.split('.')[-1]
else: else:
# Load the command object by name. # Load the command object by name.
try: try:
app_name = get_commands()[name] app_name = get_commands()[command_name]
except KeyError: except KeyError:
raise CommandError("Unknown command: %r" % name) raise CommandError("Unknown command: %r" % command_name)
if isinstance(app_name, BaseCommand): if isinstance(app_name, BaseCommand):
# If the command is already loaded, use it directly. # If the command is already loaded, use it directly.
command = app_name command = app_name
else: else:
command = load_command_class(app_name, name) command = load_command_class(app_name, command_name)
# Simulate argument parsing to get the option defaults (see #10080 for details). # Simulate argument parsing to get the option defaults (see #10080 for details).
parser = command.create_parser('', name) parser = command.create_parser('', command_name)
# Use the `dest` option name from the parser option # Use the `dest` option name from the parser option
opt_mapping = { opt_mapping = {
sorted(s_opt.option_strings)[0].lstrip('-').replace('-', '_'): s_opt.dest sorted(s_opt.option_strings)[0].lstrip('-').replace('-', '_'): s_opt.dest

View File

@ -14,6 +14,7 @@ from django.db.migrations.questioner import (
NonInteractiveMigrationQuestioner, NonInteractiveMigrationQuestioner,
) )
from django.db.migrations.state import ProjectState from django.db.migrations.state import ProjectState
from django.db.migrations.utils import get_migration_name_timestamp
from django.db.migrations.writer import MigrationWriter from django.db.migrations.writer import MigrationWriter
from django.utils.deprecation import RemovedInDjango20Warning from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.six import iteritems from django.utils.six import iteritems
@ -283,7 +284,11 @@ class Command(BaseCommand):
subclass = type("Migration", (Migration, ), { subclass = type("Migration", (Migration, ), {
"dependencies": [(app_label, migration.name) for migration in merge_migrations], "dependencies": [(app_label, migration.name) for migration in merge_migrations],
}) })
new_migration = subclass("%04i_merge" % (biggest_number + 1), app_label) migration_name = "%04i_%s" % (
biggest_number + 1,
self.migration_name or ("merge_%s" % get_migration_name_timestamp())
)
new_migration = subclass(migration_name, app_label)
writer = MigrationWriter(new_migration) writer = MigrationWriter(new_migration)
if not self.dry_run: if not self.dry_run:

View File

@ -1,6 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime
import functools import functools
import re import re
from itertools import chain from itertools import chain
@ -12,7 +11,9 @@ from django.db.migrations.migration import Migration
from django.db.migrations.operations.models import AlterModelOptions from django.db.migrations.operations.models import AlterModelOptions
from django.db.migrations.optimizer import MigrationOptimizer from django.db.migrations.optimizer import MigrationOptimizer
from django.db.migrations.questioner import MigrationQuestioner from django.db.migrations.questioner import MigrationQuestioner
from django.db.migrations.utils import COMPILED_REGEX_TYPE, RegexObject from django.db.migrations.utils import (
COMPILED_REGEX_TYPE, RegexObject, get_migration_name_timestamp,
)
from django.utils import six from django.utils import six
from .topological_sort import stable_topological_sort from .topological_sort import stable_topological_sort
@ -1154,7 +1155,7 @@ class MigrationAutodetector(object):
elif len(ops) > 1: elif len(ops) > 1:
if all(isinstance(o, operations.CreateModel) for o in ops): if all(isinstance(o, operations.CreateModel) for o in ops):
return "_".join(sorted(o.name_lower for o in ops)) return "_".join(sorted(o.name_lower for o in ops))
return "auto_%s" % datetime.datetime.now().strftime("%Y%m%d_%H%M") return "auto_%s" % get_migration_name_timestamp()
@classmethod @classmethod
def parse_number(cls, name): def parse_number(cls, name):

View File

@ -1,3 +1,4 @@
import datetime
import re import re
COMPILED_REGEX_TYPE = type(re.compile('')) COMPILED_REGEX_TYPE = type(re.compile(''))
@ -10,3 +11,7 @@ class RegexObject(object):
def __eq__(self, other): def __eq__(self, other):
return self.pattern == other.pattern and self.flags == other.flags return self.pattern == other.pattern and self.flags == other.flags
def get_migration_name_timestamp():
return datetime.datetime.now().strftime("%Y%m%d_%H%M")

View File

@ -2,6 +2,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import codecs import codecs
import datetime
import importlib import importlib
import os import os
import sys import sys
@ -705,7 +706,7 @@ class MakeMigrationsTests(MigrationTestBase):
# Monkeypatch interactive questioner to auto reject # Monkeypatch interactive questioner to auto reject
with mock.patch('django.db.migrations.questioner.input', mock.Mock(return_value='N')): with mock.patch('django.db.migrations.questioner.input', mock.Mock(return_value='N')):
with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir: with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir:
call_command("makemigrations", "migrations", merge=True, interactive=True, verbosity=0) call_command("makemigrations", "migrations", name="merge", merge=True, interactive=True, verbosity=0)
merge_file = os.path.join(migration_dir, '0003_merge.py') merge_file = os.path.join(migration_dir, '0003_merge.py')
self.assertFalse(os.path.exists(merge_file)) self.assertFalse(os.path.exists(merge_file))
@ -717,11 +718,22 @@ class MakeMigrationsTests(MigrationTestBase):
with mock.patch('django.db.migrations.questioner.input', mock.Mock(return_value='y')): with mock.patch('django.db.migrations.questioner.input', mock.Mock(return_value='y')):
out = six.StringIO() out = six.StringIO()
with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir: with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir:
call_command("makemigrations", "migrations", merge=True, interactive=True, stdout=out) call_command("makemigrations", "migrations", name="merge", merge=True, interactive=True, stdout=out)
merge_file = os.path.join(migration_dir, '0003_merge.py') merge_file = os.path.join(migration_dir, '0003_merge.py')
self.assertTrue(os.path.exists(merge_file)) self.assertTrue(os.path.exists(merge_file))
self.assertIn("Created new merge migration", force_text(out.getvalue())) self.assertIn("Created new merge migration", force_text(out.getvalue()))
@mock.patch('django.db.migrations.utils.datetime')
def test_makemigrations_default_merge_name(self, mock_datetime):
mock_datetime.datetime.now.return_value = datetime.datetime(2016, 1, 2, 3, 4)
with mock.patch('django.db.migrations.questioner.input', mock.Mock(return_value='y')):
out = six.StringIO()
with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir:
call_command("makemigrations", "migrations", merge=True, interactive=True, stdout=out)
merge_file = os.path.join(migration_dir, '0003_merge_20160102_0304.py')
self.assertTrue(os.path.exists(merge_file))
self.assertIn("Created new merge migration", force_text(out.getvalue()))
def test_makemigrations_non_interactive_not_null_addition(self): def test_makemigrations_non_interactive_not_null_addition(self):
""" """
Tests that non-interactive makemigrations fails when a default is missing on a new not-null field. Tests that non-interactive makemigrations fails when a default is missing on a new not-null field.
@ -793,7 +805,7 @@ class MakeMigrationsTests(MigrationTestBase):
""" """
out = six.StringIO() out = six.StringIO()
with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir: with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir:
call_command("makemigrations", "migrations", merge=True, interactive=False, stdout=out) call_command("makemigrations", "migrations", name="merge", merge=True, interactive=False, stdout=out)
merge_file = os.path.join(migration_dir, '0003_merge.py') merge_file = os.path.join(migration_dir, '0003_merge.py')
self.assertTrue(os.path.exists(merge_file)) self.assertTrue(os.path.exists(merge_file))
output = force_text(out.getvalue()) output = force_text(out.getvalue())
@ -809,7 +821,10 @@ class MakeMigrationsTests(MigrationTestBase):
""" """
out = six.StringIO() out = six.StringIO()
with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir: with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir:
call_command("makemigrations", "migrations", dry_run=True, merge=True, interactive=False, stdout=out) call_command(
"makemigrations", "migrations", name="merge", dry_run=True,
merge=True, interactive=False, stdout=out,
)
merge_file = os.path.join(migration_dir, '0003_merge.py') merge_file = os.path.join(migration_dir, '0003_merge.py')
self.assertFalse(os.path.exists(merge_file)) self.assertFalse(os.path.exists(merge_file))
output = force_text(out.getvalue()) output = force_text(out.getvalue())
@ -825,8 +840,10 @@ class MakeMigrationsTests(MigrationTestBase):
""" """
out = six.StringIO() out = six.StringIO()
with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir: with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir:
call_command("makemigrations", "migrations", dry_run=True, merge=True, interactive=False, call_command(
stdout=out, verbosity=3) "makemigrations", "migrations", name="merge", dry_run=True,
merge=True, interactive=False, stdout=out, verbosity=3,
)
merge_file = os.path.join(migration_dir, '0003_merge.py') merge_file = os.path.join(migration_dir, '0003_merge.py')
self.assertFalse(os.path.exists(merge_file)) self.assertFalse(os.path.exists(merge_file))
output = force_text(out.getvalue()) output = force_text(out.getvalue())
@ -928,7 +945,7 @@ class MakeMigrationsTests(MigrationTestBase):
out = six.StringIO() out = six.StringIO()
with mock.patch('django.db.migrations.questioner.input', mock.Mock(return_value='N')): with mock.patch('django.db.migrations.questioner.input', mock.Mock(return_value='N')):
with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir: with self.temporary_migration_module(module="migrations.test_migrations_conflict") as migration_dir:
call_command("makemigrations", "migrations", merge=True, stdout=out) call_command("makemigrations", "migrations", name="merge", merge=True, stdout=out)
merge_file = os.path.join(migration_dir, '0003_merge.py') merge_file = os.path.join(migration_dir, '0003_merge.py')
# This will fail if interactive is False by default # This will fail if interactive is False by default
self.assertFalse(os.path.exists(merge_file)) self.assertFalse(os.path.exists(merge_file))
@ -959,7 +976,7 @@ class MakeMigrationsTests(MigrationTestBase):
with mock.patch('django.db.migrations.questioner.input', mock.Mock(return_value='y')): with mock.patch('django.db.migrations.questioner.input', mock.Mock(return_value='y')):
out = six.StringIO() out = six.StringIO()
with self.temporary_migration_module(app_label="migrated_app") as migration_dir: with self.temporary_migration_module(app_label="migrated_app") as migration_dir:
call_command("makemigrations", "migrated_app", merge=True, interactive=True, stdout=out) call_command("makemigrations", "migrated_app", name="merge", merge=True, interactive=True, stdout=out)
merge_file = os.path.join(migration_dir, '0003_merge.py') merge_file = os.path.join(migration_dir, '0003_merge.py')
self.assertFalse(os.path.exists(merge_file)) self.assertFalse(os.path.exists(merge_file))
self.assertIn("No conflicts detected to merge.", out.getvalue()) self.assertIn("No conflicts detected to merge.", out.getvalue())