Skip to content

Commit 0e02e74

Browse files
committed
add initial code
1 parent c257a23 commit 0e02e74

File tree

16 files changed

+249
-4
lines changed

16 files changed

+249
-4
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
# Created by .ignore support plugin (hsz.mobi)
22
.idea
3+
manage.py
4+
django-auto-rollback
5+
__pycache__
6+
db.sqlite3

django_auto_rollback/__init__.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

django_auto_rollback/management/commands/freeze_migrations_state.py

Whitespace-only changes.

django_auto_rollback/management/commands/rollback_migrations.py

Whitespace-only changes.

django_auto_rollback/models.py

Lines changed: 0 additions & 2 deletions
This file was deleted.

django_rollback/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
name = 'django_rollback'

django_rollback/apps.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.apps import AppConfig
2+
3+
4+
class StatusConfig(AppConfig):
5+
name = 'django_rollback'
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import json
2+
from collections import namedtuple
3+
4+
import git
5+
from django.core.management import call_command
6+
from django.core.management.base import BaseCommand, CommandError
7+
8+
from django_rollback.models import AppsState
9+
10+
COMMIT_MAX_LENGTH = 40
11+
MIGRATE_COMMAND = 'migrate'
12+
13+
MigrationRecord = namedtuple('MigrationRecord', ['id', 'app', 'name'])
14+
15+
16+
class Command(BaseCommand):
17+
help = 'Rollback migrations state of all django apps to chosen tag or commit if previously stored.'
18+
19+
def add_arguments(self, parser):
20+
parser.add_argument('-t', '--tag', type=str, help='Git tag in witch needs to rollback migrations.')
21+
parser.add_argument('-c', '--commit', type=str, help='Git commit hash in witch needs to rollback migrations.')
22+
parser.add_argument('--fake', action='store_true',
23+
help='Used to block migrate commands execution, so it allow to only print info '
24+
'about processed actions without execution.')
25+
26+
def handle(self, *args, **options):
27+
other_commit = self.get_commit_from_options(options)
28+
current_commit = self.get_current_commit()
29+
30+
current_data = self.get_migrations_data_from_commit(current_commit)
31+
other_data = self.get_migrations_data_from_commit(other_commit)
32+
33+
diff = self.get_migrations_diff(current=current_data, other=other_data)
34+
35+
self.run_rollback(diff, fake=options['fake'])
36+
37+
self.stdout.write(self.style.SUCCESS('Rollback successfully finished.'))
38+
39+
def get_commit_from_options(self, options):
40+
if not options['tag'] and not options['commit']:
41+
raise CommandError('Tag or commit should be described by -t or -c arguments.')
42+
43+
if options['tag'] and options['commit']:
44+
raise CommandError('Tag and commit arguments should not be described together.')
45+
46+
if options['commit']:
47+
return options['commit']
48+
49+
tag = options['tag']
50+
try:
51+
repo = git.Repo('.')
52+
if tag not in repo.tags:
53+
raise CommandError(f'Can not find tag `{tag}` in git repository.')
54+
55+
return repo.tags[tag].commit.hexsha
56+
57+
except CommandError as err:
58+
raise err
59+
60+
except Exception as err:
61+
self.stdout.write(self.style.ERROR(f'WARNING: an error occurred while working with git repo!'))
62+
raise CommandError(err)
63+
64+
def get_current_commit(self):
65+
try:
66+
repo = git.Repo('.')
67+
return repo.head.commit.hexsha
68+
69+
except ValueError as err:
70+
self.stdout.write(self.style.ERROR(f'WARNING: an error occurred while working with git repo!'))
71+
raise CommandError(err)
72+
73+
def get_migrations_data_from_commit(self, commit):
74+
queryset = AppsState.objects.filter(commit__istartswith=commit)
75+
76+
count = queryset.count()
77+
78+
if count == 0:
79+
raise CommandError(f'Can not find stored data of migrations state for commit `{commit}`.')
80+
81+
if count > 1:
82+
is_short_commit = len(commit) < COMMIT_MAX_LENGTH
83+
raise CommandError(f'Found more than 1 ({count}) records for selected commit {commit}.'
84+
f'{" Please clarify commit hash for more identity." if is_short_commit else ""}')
85+
86+
instance = queryset.first()
87+
return json.loads(instance.migrations)
88+
89+
def get_migrations_diff(self, current, other):
90+
"""
91+
current and other is type of list of tuples in format:
92+
[(<id> : int, <app> : str, <name> : str), ...]
93+
94+
:return dict that indicates what migrations should be executed
95+
migration_id is useful to detect migration order (from higher to lower)
96+
{
97+
<migration_id>: <migrate command args>
98+
}
99+
"""
100+
result = []
101+
102+
current = [MigrationRecord(*rec) for rec in current]
103+
other = [MigrationRecord(*rec) for rec in other]
104+
other_apps = {migration.app for migration in other}
105+
106+
# find what is changed in current relative to other
107+
diff = set(current) - set(other)
108+
for migration in diff:
109+
is_new_app = migration.app not in other_apps
110+
result.append(MigrationRecord(
111+
migration.id,
112+
migration.app,
113+
'zero' if is_new_app else list(filter(lambda x: x.app == migration.app, other))[0].name,
114+
))
115+
116+
return result
117+
118+
def run_rollback(self, migrations_diff_records, fake=False):
119+
"""
120+
sort all migrations by migration.id order from higher to lower and execute migrate command for them
121+
"""
122+
123+
if not migrations_diff_records:
124+
self.stdout.write(f'>>> There is no migrations to rollback.')
125+
return
126+
127+
for migration in sorted(migrations_diff_records, key=lambda r: int(r.id), reverse=True):
128+
execute_args = (MIGRATE_COMMAND, migration.app, migration.name)
129+
self.stdout.write(f'>>> Executing command: {" ".join(execute_args)}')
130+
131+
if not fake:
132+
call_command(*execute_args)

0 commit comments

Comments
 (0)