Per-branch HEAD files for conflict management

We have pep8 check for validation revisions, but it allows
outdated changes go into merge queue. To prevent this added
CONTRACT_HEAD, EXPAND_HEAD files.

Closes-bug: #1505701

Change-Id: Ie4b727e55a0b1ecb12e915a0037094a928d8f975
This commit is contained in:
AKamyshnikova 2015-10-08 18:18:21 +03:00
parent 7a9e8175c9
commit f144a283ea
5 changed files with 206 additions and 44 deletions

View File

@ -312,6 +312,19 @@ following directive should be added in the contraction script::
depends_on = ('<expansion-revision>',)
HEAD files for conflict management
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In directory ``neutron/db/migration/alembic_migrations/versions`` there are two
files, ``CONTRACT_HEAD`` and ``EXPAND_HEAD``. These files contain the ID of the
head revision in each branch. The purpose of these files is to validate the
revision timelines and prevent non-linear changes from entering the merge queue.
When you create a new migration script by neutron-db-manage these files will be
updated automatically. But if another migration script is merged while your
change is under review, you will need to resolve the conflict manually by
changing the ``down_revision`` in your migration script.
Applying database migration rules
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -0,0 +1 @@
1b294093239c

View File

@ -0,0 +1 @@
59cb5b6cf4d

View File

@ -32,6 +32,8 @@ from neutron.db import migration
HEAD_FILENAME = 'HEAD'
HEADS_FILENAME = 'HEADS'
CONTRACT_HEAD_FILENAME = 'CONTRACT_HEAD'
EXPAND_HEAD_FILENAME = 'EXPAND_HEAD'
CURRENT_RELEASE = migration.MITAKA
RELEASES = (
@ -235,7 +237,6 @@ def _check_bootstrap_new_branch(branch, version_path, addn_kwargs):
def do_revision(config, cmd):
'''Generate new revision files, one per branch.'''
kwargs = {
'message': CONF.command.message,
'autogenerate': CONF.command.autogenerate,
@ -247,7 +248,10 @@ def do_revision(config, cmd):
kwargs['head'] = 'contract@head'
do_alembic_command(config, cmd, **kwargs)
update_head_file(config)
if _use_separate_migration_branches(config):
update_head_files(config)
else:
update_head_file(config)
def _get_release_labels(labels):
@ -340,12 +344,14 @@ def _get_branch_points(script):
def validate_head_file(config):
'''Check that HEAD file contains the latest head for the branch.'''
if _use_separate_migration_branches(config):
return
_validate_head_file(config)
_validate_head_files(config)
else:
_validate_head_file(config)
@debtcollector.removals.remove(message=BRANCHLESS_WARNING)
def _validate_head_file(config):
'''Check that HEAD file contains the latest head for the branch.'''
script = alembic_script.ScriptDirectory.from_config(config)
expected_head = script.get_heads()
head_path = _get_head_file_path(config)
@ -361,22 +367,63 @@ def _validate_head_file(config):
% expected_head)
def update_head_file(config):
'''Update HEAD file with the latest branch head.'''
if _use_separate_migration_branches(config):
# Kill any HEAD(S) files because we don't rely on them for branch-aware
# chains anymore
files_to_remove = [
_get_head_file_path(config), _get_heads_file_path(config)
]
for file_ in files_to_remove:
fileutils.delete_if_exists(file_)
def _get_heads_map(config):
script = alembic_script.ScriptDirectory.from_config(config)
heads = script.get_heads()
head_map = {}
for head in heads:
if CONTRACT_BRANCH in script.get_revision(head).branch_labels:
head_map[CONTRACT_BRANCH] = head
else:
head_map[EXPAND_BRANCH] = head
return head_map
def _check_head(branch_name, head_file, head):
try:
with open(head_file) as file_:
observed_head = file_.read().strip()
except IOError:
pass
else:
if observed_head != head:
alembic_util.err(
_('%(branch)s HEAD file does not match migration timeline '
'head, expected: %(head)s') % {'branch': branch_name.title(),
'head': head})
def _validate_head_files(config):
'''Check that HEAD files contain the latest head for the branch.'''
contract_head = _get_contract_head_file_path(config)
expand_head = _get_expand_head_file_path(config)
if not os.path.exists(contract_head) or not os.path.exists(expand_head):
alembic_util.warn(_("Repository does not contain HEAD files for "
"contract and expand branches."))
return
_update_head_file(config)
head_map = _get_heads_map(config)
_check_head(CONTRACT_BRANCH, contract_head, head_map[CONTRACT_BRANCH])
_check_head(EXPAND_BRANCH, expand_head, head_map[EXPAND_BRANCH])
def update_head_files(config):
'''Update HEAD files with the latest branch heads.'''
head_map = _get_heads_map(config)
contract_head = _get_contract_head_file_path(config)
expand_head = _get_expand_head_file_path(config)
with open(contract_head, 'w+') as f:
f.write(head_map[CONTRACT_BRANCH] + '\n')
with open(expand_head, 'w+') as f:
f.write(head_map[EXPAND_BRANCH] + '\n')
old_head_file = _get_head_file_path(config)
old_heads_file = _get_heads_file_path(config)
for file_ in (old_head_file, old_heads_file):
fileutils.delete_if_exists(file_)
@debtcollector.removals.remove(message=BRANCHLESS_WARNING)
def _update_head_file(config):
def update_head_file(config):
script = alembic_script.ScriptDirectory.from_config(config)
head = script.get_heads()
with open(_get_head_file_path(config), 'w+') as f:
@ -483,6 +530,24 @@ def _get_heads_file_path(config):
HEADS_FILENAME)
def _get_contract_head_file_path(config):
'''
Return the path of the file that is used to maintain contract head
'''
return os.path.join(
_get_root_versions_dir(config),
CONTRACT_HEAD_FILENAME)
def _get_expand_head_file_path(config):
'''
Return the path of the file that is used to maintain expand head
'''
return os.path.join(
_get_root_versions_dir(config),
EXPAND_HEAD_FILENAME)
def _get_version_branch_path(config, release=None, branch=None):
version_path = _get_root_versions_dir(config)
if branch and release:

View File

@ -24,6 +24,7 @@ from alembic.operations import ops as alembic_ops
from alembic import script as alembic_script
import fixtures
import mock
from oslo_utils import fileutils
import pkg_resources
import sqlalchemy as sa
@ -118,6 +119,7 @@ class TestCli(base.BaseTestCase):
self.do_alembic_cmd_p = mock.patch.object(cli, 'do_alembic_command')
self.do_alembic_cmd = self.do_alembic_cmd_p.start()
self.mock_alembic_err = mock.patch('alembic.util.err').start()
self.mock_alembic_warn = mock.patch('alembic.util.warn').start()
self.mock_alembic_err.side_effect = SystemExit
def mocked_root_dir(cfg):
@ -210,7 +212,7 @@ class TestCli(base.BaseTestCase):
self.assertEqual(len(self.projects), validate.call_count)
def _test_database_sync_revision(self, separate_branches=True):
with mock.patch.object(cli, 'update_head_file') as update,\
with mock.patch.object(cli, 'update_head_files') as update,\
mock.patch.object(cli, '_use_separate_migration_branches',
return_value=separate_branches):
if separate_branches:
@ -355,21 +357,18 @@ class TestCli(base.BaseTestCase):
def test_upgrade_rejects_delta_with_relative_revision(self, use_mock):
self.assert_command_fails(['prog', 'upgrade', '+2', '--delta', '3'])
def _test_validate_head_file_helper(self, heads, file_heads=None,
branchless=False):
def _test_validate_head_file_helper(self, heads, file_heads=None):
if file_heads is None:
file_heads = []
fake_config = self.configs[0]
mock_open = self.useFixture(
tools.OpenFixture(cli._get_head_file_path(fake_config),
'\n'.join(file_heads))
).mock_open
tools.OpenFixture(cli._get_head_file_path(fake_config),
'\n'.join(file_heads))).mock_open
with mock.patch('alembic.script.ScriptDirectory.from_config') as fc,\
mock.patch.object(cli, '_use_separate_migration_branches',
return_value=not branchless):
return_value=False):
fc.return_value.get_heads.return_value = heads
if not branchless or all(head in file_heads for head in heads):
if all(head in file_heads for head in heads):
cli.validate_head_file(fake_config)
else:
self.assertRaises(
@ -378,36 +377,119 @@ class TestCli(base.BaseTestCase):
fake_config
)
self.assertTrue(self.mock_alembic_err.called)
if branchless:
mock_open.assert_called_with(
mock_open.assert_called_with(
cli._get_head_file_path(fake_config))
fc.assert_called_once_with(fake_config)
fc.assert_called_once_with(fake_config)
def _test_validate_head_files_helper(self, heads, contract_head='',
expand_head=''):
fake_config = self.configs[0]
head_files_not_exist = (contract_head == expand_head == '')
with mock.patch('alembic.script.ScriptDirectory.from_config') as fc,\
mock.patch('os.path.exists') as os_mock,\
mock.patch.object(cli, '_use_separate_migration_branches',
return_value=True):
if head_files_not_exist:
os_mock.return_value = False
else:
self.assertFalse(mock_open.called)
self.assertFalse(fc.called)
os_mock.return_value = True
def test_validate_head_file_multiple_heads(self):
self._test_validate_head_file_helper(['a', 'b'])
fc.return_value.get_heads.return_value = heads
def test_validate_head_file_missing_file(self):
self._test_validate_head_file_helper(['a'])
revs = {heads[0]: FakeRevision(labels=cli.CONTRACT_BRANCH),
heads[1]: FakeRevision(labels=cli.EXPAND_BRANCH)}
fc.return_value.get_revision.side_effect = revs.__getitem__
mock_open_con = self.useFixture(
tools.OpenFixture(cli._get_contract_head_file_path(
fake_config), contract_head + '\n')).mock_open
mock_open_ex = self.useFixture(
tools.OpenFixture(cli._get_expand_head_file_path(
fake_config), expand_head + '\n')).mock_open
def test_validate_head_file_wrong_contents(self):
if contract_head in heads and expand_head in heads:
cli.validate_head_file(fake_config)
elif head_files_not_exist:
cli.validate_head_file(fake_config)
self.assertTrue(self.mock_alembic_warn.called)
else:
self.assertRaises(
SystemExit,
cli.validate_head_file,
fake_config
)
self.assertTrue(self.mock_alembic_err.called)
if contract_head in heads and expand_head in heads:
mock_open_ex.assert_called_with(
cli._get_expand_head_file_path(fake_config))
mock_open_con.assert_called_with(
cli._get_contract_head_file_path(fake_config))
if not head_files_not_exist:
fc.assert_called_once_with(fake_config)
def test_validate_head_files_success(self):
self._test_validate_head_files_helper(['a', 'b'], contract_head='a',
expand_head='b')
def test_validate_head_files_missing_file(self):
self._test_validate_head_files_helper(['a', 'b'])
def test_validate_head_files_wrong_contents(self):
self._test_validate_head_files_helper(['a', 'b'], contract_head='c',
expand_head='d')
def test_validate_head_file_branchless_wrong_contents(self):
self._test_validate_head_file_helper(['a'], ['b'])
def test_validate_head_file_success(self):
def test_validate_head_file_branchless_success(self):
self._test_validate_head_file_helper(['a'], ['a'])
@mock.patch.object(cli, '_use_separate_migration_branches',
return_value=False)
def test_validate_head_file_branchless_failure(self, *args):
self._test_validate_head_file_helper(['a'], ['b'], branchless=True)
def test_validate_head_file_branchless_missing_file(self):
self._test_validate_head_file_helper(['a'])
def test_update_head_file_success(self):
head = ['b']
mock_open = self.useFixture(
tools.OpenFixture(cli._get_head_file_path(
self.configs[0]))).mock_open
with mock.patch('alembic.script.ScriptDirectory.from_config') as fc:
fc.return_value.get_heads.return_value = head
cli.update_head_file(self.configs[0])
mock_open.return_value.write.assert_called_with(
'\n'.join(head))
@mock.patch.object(cli, '_use_separate_migration_branches',
return_value=False)
def test_validate_head_file_branchless_success(self, *args):
self._test_validate_head_file_helper(['a'], ['a'], branchless=True)
return_value=True)
@mock.patch.object(fileutils, 'delete_if_exists')
def test_update_head_files_success(self, *mocks):
heads = ['a', 'b']
mock_open_con = self.useFixture(
tools.OpenFixture(cli._get_contract_head_file_path(
self.configs[0]))).mock_open
mock_open_ex = self.useFixture(
tools.OpenFixture(cli._get_expand_head_file_path(
self.configs[0]))).mock_open
with mock.patch('alembic.script.ScriptDirectory.from_config') as fc:
fc.return_value.get_heads.return_value = heads
revs = {heads[0]: FakeRevision(labels=cli.CONTRACT_BRANCH),
heads[1]: FakeRevision(labels=cli.EXPAND_BRANCH)}
fc.return_value.get_revision.side_effect = revs.__getitem__
cli.update_head_files(self.configs[0])
mock_open_con.return_value.write.assert_called_with(
heads[0] + '\n')
mock_open_ex.return_value.write.assert_called_with(heads[1] + '\n')
old_head_file = cli._get_head_file_path(
self.configs[0])
old_heads_file = cli._get_heads_file_path(
self.configs[0])
delete_if_exists = mocks[0]
self.assertIn(mock.call(old_head_file),
delete_if_exists.call_args_list)
self.assertIn(mock.call(old_heads_file),
delete_if_exists.call_args_list)
def test_get_project_base(self):
config = alembic_config.Config()