Add a script to fix migration ordering conflicts

When generating a new migration we give it a number that increments
the latest existing migration on master. This means that when there
are multiple PRs open containing a migration and one of them gets
merged the others need to be updated to move their migration files
to apply on top of the recently merged one.

This requires renaming the file and changing migration references
for both the migration revision and down_revision.

If a PR introduced more than 1 migration they all need to be updated
one after another since each one needs to be renamed.

This adds a script to simplify this process. `./scripts/fix_migrations.py`
will check for any branch points If it finds exactly one (which
should be the common case), it asks which migration should be moved
and renames / updates references to move the selected branch on top
of the other one.

It won't resolve any conflicts within migrations themselves (eg if
both branches modified the same column) and it won't try to resolve
cases with more than 1 branch.
This commit is contained in:
Alexey Bezhan
2017-11-17 13:58:44 +00:00
parent 0df44040e3
commit e813f1ff87

87
scripts/fix_migrations.py Executable file
View File

@@ -0,0 +1,87 @@
#!/usr/bin/env python
# encoding: utf-8
import os
import sys
from alembic.script import ScriptDirectory
sys.path.append('.')
def get_branch_points(migrations):
return [m for m in migrations.walk_revisions() if m.is_branch_point]
def get_branches(migrations, branch_point, heads):
return [list(migrations.iterate_revisions(m, branch_point.revision))[::-1]
for m in heads]
def choice(prompt, options, option_fmt=lambda x: x):
print("{}:\n".format(prompt))
for i, option in enumerate(options):
print("{}. {}".format(i + 1, option_fmt(option)))
print()
choice = input("Option> ")
return options[int(choice) - 1]
def rename_revision(current_revision, new_base):
new_id = int(new_base[:4]) + 1
return "{:04d}{}".format(new_id, current_revision[4:])
def reorder_revisions(revisions, old_base, new_base):
if not revisions:
return
head, *tail = revisions
new_revision_id = rename_revision(head.revision, new_base)
print("Moving {} to {}".format(head.revision, new_revision_id))
with open(head.path, 'r') as rev_file:
file_data = rev_file.read()
file_data = file_data.replace(head.revision, new_revision_id).replace(old_base, new_base)
with open(head.path.replace(head.revision, new_revision_id), 'w') as rev_file:
rev_file.write(file_data)
print("Removing {}".format(head.path))
os.remove(head.path)
reorder_revisions(tail, head.revision, new_revision_id)
def fix_branch_point(migrations, branch_point, heads):
print("Migrations directory has a branch point at {}".format(branch_point.revision))
branches = get_branches(migrations, branch_point, heads)
move_branch = choice("Select migrations to move", branches,
lambda x: " -> ".join(m.revision for m in x))
branches.remove(move_branch)
reorder_revisions(move_branch, branch_point.revision, branches[0][-1].revision)
def main(migrations_path):
migrations = ScriptDirectory(migrations_path)
branch_points = get_branch_points(migrations)
heads = migrations.get_heads()
if not branch_points:
print("Migrations are ordered")
elif len(branch_points) == 1 and len(heads) == 2:
fix_branch_point(migrations, branch_points[0], heads)
else:
print("Found {} branch points and {} heads, can't fix automatically".format(
[bp.revision for bp in branch_points], heads))
sys.exit(1)
if __name__ == '__main__':
main('migrations/')