Large code refactoring
* Add tests * Remove git dependency (only needed when a git identifier is specified)
This commit is contained in:
1 parent
b8ca768a15
commit
50ce4f4cad
256 files changed
+1263
-477
No files matched your search
@@ -13,161 +13,169 @@
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import print_function
|
||||
import argparse
|
||||
import logging as log
|
||||
import os
|
||||
from io import StringIO
|
||||
import re
|
||||
from subprocess import Popen, PIPE
|
||||
import sys
|
||||
from . import utils
|
||||
from .sql_analyser import analyse_sql_statements
|
||||
|
||||
|
||||
def has_default(sql, **kwargs):
|
||||
if re.search('SET DEFAULT', sql) and kwargs['errors']:
|
||||
err = next(err
|
||||
for err in kwargs['errors']
|
||||
if err['code'] == 'NOT_NULL')
|
||||
if err:
|
||||
log.info(
|
||||
('Found a NOT_NULL error in migration, '
|
||||
'but it has a default value added: {}').format(err))
|
||||
kwargs['errors'].remove(err)
|
||||
|
||||
return False # Never fails
|
||||
|
||||
|
||||
def clean_bytes_to_str(byte_input):
|
||||
return byte_input.decode('utf-8').strip()
|
||||
|
||||
|
||||
class MigrationLinter:
|
||||
class MigrationLinter(object):
|
||||
MIGRATION_FOLDER_NAME = 'migrations'
|
||||
|
||||
migration_tests = (
|
||||
{
|
||||
'code': 'NOT_NULL',
|
||||
'fn': lambda sql, **kw: re.search('NOT NULL', sql) and
|
||||
not re.search('CREATE TABLE', sql),
|
||||
'err_msg': 'NOT NULL constraint on columns'
|
||||
}, {
|
||||
'code': 'DROP_COLUMN',
|
||||
'fn': lambda sql, **kw: re.search('DROP COLUMN', sql),
|
||||
'err_msg': 'DROPPING columns'
|
||||
}, {
|
||||
'code': 'RENAME_COLUMN',
|
||||
'fn': lambda sql, **kw: re.search('ALTER TABLE .* CHANGE', sql) or
|
||||
re.search('ALTER TABLE .* RENAME COLUMN', sql),
|
||||
'err_msg': 'RENAMING columns'
|
||||
}, {
|
||||
'code': 'RENAME_TABLE',
|
||||
'fn': lambda sql, **kw: re.search('RENAME TABLE', sql) or
|
||||
re.search('ALTER TABLE .* RENAME TO', sql),
|
||||
'err_msg': 'RENAMING tables'
|
||||
}, {
|
||||
'code': '',
|
||||
'fn': has_default,
|
||||
'err_msg': ''
|
||||
}
|
||||
)
|
||||
def __init__(self, project_path, **kwargs):
|
||||
# Verify correctness
|
||||
if not utils.is_directory(project_path):
|
||||
raise ValueError('The given path {0} does not seem to be a directory.'.format(project_path))
|
||||
if not utils.is_django_project(project_path):
|
||||
raise ValueError('The given path {0} does not seem to be a django project.'.format(project_path))
|
||||
|
||||
def __init__(self, django_folder, commit_id=None, **kwargs):
|
||||
self.location = django_folder
|
||||
self.commit_id = commit_id
|
||||
# Store parameters and options
|
||||
self.django_path = project_path
|
||||
self.ignore_name_contains = kwargs.get('ignore_name_contains', None)
|
||||
self.ignore_name = kwargs.get('ignore_name', None) or tuple()
|
||||
self.include_apps = kwargs.get('include_apps', None)
|
||||
self.exclude_apps = kwargs.get('exclude_apps', None)
|
||||
self.database = kwargs.get('database', None) or 'default'
|
||||
|
||||
self.changed_migration_files = []
|
||||
self._gather_migrations()
|
||||
# Initialise counters
|
||||
self.nb_valid = 0
|
||||
self.nb_ignored = 0
|
||||
self.nb_erroneous = 0
|
||||
self.nb_total = 0
|
||||
|
||||
def _gather_migrations(self):
|
||||
# Find (one of) the initial commits
|
||||
if not self.commit_id:
|
||||
git_init_cmd = 'cd {0} && git rev-list HEAD | tail -n 1'.format(
|
||||
self.location)
|
||||
process = Popen(git_init_cmd, shell=True, stdout=PIPE, stderr=PIPE)
|
||||
for line in map(clean_bytes_to_str, process.stdout.readlines()):
|
||||
self.commit_id = line
|
||||
break
|
||||
process.wait()
|
||||
log.info('Operating until git identifier {0}'.format(self.commit_id))
|
||||
def lint_migration(self, app_name, migration_name):
|
||||
print('({0}, {1})... '.format(app_name, migration_name), end='')
|
||||
self.nb_total += 1
|
||||
|
||||
if self.should_ignore_migration(app_name, migration_name):
|
||||
print('IGNORE')
|
||||
self.nb_ignore += 1
|
||||
return
|
||||
|
||||
sql_statements = self.get_sql(
|
||||
app_name, migration_name)
|
||||
analysis_result = analyse_sql_statements(
|
||||
sql_statements)
|
||||
errors = analysis_result['errors']
|
||||
if not errors:
|
||||
print('OK')
|
||||
self.nb_valid += 1
|
||||
return
|
||||
|
||||
# Print errors
|
||||
print('ERR')
|
||||
self.nb_erroneous += 1
|
||||
for err in errors:
|
||||
error_str = '\t{0}'.format(err['err_msg'])
|
||||
if err['table']:
|
||||
error_str += ' (table: {0}'.format(err['table'])
|
||||
if err['column']:
|
||||
error_str += ', column: {0}'.format(
|
||||
err['column'])
|
||||
error_str += ')'
|
||||
print(error_str)
|
||||
|
||||
def lint_all_migrations(self, git_commit_id=None):
|
||||
# Collect migrations
|
||||
if git_commit_id:
|
||||
if not utils.is_git_project(self.django_path):
|
||||
raise ValueError('The given project {0} does not seem to be versioned by git.'.format(self.django_path))
|
||||
migrations = self._gather_migrations_git(git_commit_id)
|
||||
else:
|
||||
migrations = self._gather_all_migrations()
|
||||
|
||||
# Lint those migrations
|
||||
for m in migrations:
|
||||
self.lint_migration(*m)
|
||||
|
||||
def print_summary(self):
|
||||
print('*** Summary:')
|
||||
print(('Valid migrations: {1}/{0} - '
|
||||
'erroneous migrations: {2}/{0} - '
|
||||
'ignored migrations: {3}/{0}').format(
|
||||
self.nb_total,
|
||||
self.nb_valid,
|
||||
self.nb_erroneous,
|
||||
self.nb_ignore))
|
||||
|
||||
@property
|
||||
def has_errors(self):
|
||||
return self.nb_erroneous > 0
|
||||
|
||||
def get_sql(self, app_name, migration_name):
|
||||
""" It would be much faster to call the
|
||||
command directly from the code using
|
||||
call_command(), but requires the code
|
||||
to setup django (by calling django.setup())
|
||||
and set the DJANGO_SETTINGS_MODULE.
|
||||
However, django is global and doesn't allow
|
||||
multiple linter instances to exist at the same time.
|
||||
(because they would all just lint the same django project)
|
||||
Even if calling a shell is slow and ugly, for now,
|
||||
it allows to seperate the instances correctly.
|
||||
"""
|
||||
git_diff_command = (
|
||||
'cd {0} && '
|
||||
'python manage.py sqlmigrate {1} {2} '
|
||||
'--database {3}').format(
|
||||
self.django_path, app_name, migration_name, self.database)
|
||||
log.info('Executing {0}'.format(git_diff_command))
|
||||
sqlmigrate_process = Popen(
|
||||
git_diff_command, shell=True, stdout=PIPE, stderr=PIPE)
|
||||
|
||||
sql_statements = []
|
||||
for line in map(
|
||||
utils.clean_bytes_to_str, sqlmigrate_process.stdout.readlines()):
|
||||
sql_statements.append(line)
|
||||
sqlmigrate_process.wait()
|
||||
if sqlmigrate_process.returncode != 0:
|
||||
raise RuntimeError('sqlmigrate command failed')
|
||||
log.info('Found {0} sql migration lines'.format(len(sql_statements)))
|
||||
return sql_statements
|
||||
|
||||
def _gather_migrations_git(self, git_commit_id):
|
||||
migrations = []
|
||||
# Get changes since specified commit
|
||||
git_diff_command = 'cd {0} && git diff --name-only {1}'.format(
|
||||
self.location, self.commit_id)
|
||||
self.django_path, git_commit_id)
|
||||
log.info('Executing {0}'.format(git_diff_command))
|
||||
diff_process = Popen(
|
||||
git_diff_command, shell=True, stdout=PIPE, stderr=PIPE)
|
||||
for line in map(clean_bytes_to_str, diff_process.stdout.readlines()):
|
||||
for line in map(utils.clean_bytes_to_str, diff_process.stdout.readlines()):
|
||||
# Only gather lines that include migrations
|
||||
if re.search(
|
||||
'\/{0}\/.*\.py'.format(self.MIGRATION_FOLDER_NAME),
|
||||
line) and \
|
||||
'__init__' not in line:
|
||||
self.changed_migration_files.append(line)
|
||||
app_name, migration_name = self._split_migration_path(line)
|
||||
migrations.append((app_name, migration_name))
|
||||
diff_process.wait()
|
||||
|
||||
if diff_process.returncode != 0:
|
||||
output = []
|
||||
for line in map(
|
||||
clean_bytes_to_str, diff_process.stderr.readlines()):
|
||||
utils.clean_bytes_to_str, diff_process.stderr.readlines()):
|
||||
output.append(line)
|
||||
log.info("Error while git diff command:\n{}".format(
|
||||
"".join(output)))
|
||||
raise Exception('Error while executing git diff command')
|
||||
return migrations
|
||||
|
||||
def lint_migrations(self):
|
||||
nb_valid = 0
|
||||
nb_erroneous = 0
|
||||
nb_ignore = 0
|
||||
for migration in self.changed_migration_files:
|
||||
app_name, migration_name = self._split_migration_path(migration)
|
||||
print('{0}... '.format(migration), end='')
|
||||
if self.should_ignore_migration(app_name, migration_name):
|
||||
print('IGNORE')
|
||||
nb_ignore += 1
|
||||
else:
|
||||
sql_statements = self.django_sqlmigrate(
|
||||
app_name, migration_name)
|
||||
errors = []
|
||||
for statement in sql_statements:
|
||||
is_valid, err = \
|
||||
self._test_sql_statement_for_backward_incompatibility(
|
||||
statement, errors)
|
||||
if not is_valid:
|
||||
errors.append(err)
|
||||
if not errors:
|
||||
print('OK')
|
||||
nb_valid += 1
|
||||
else:
|
||||
print('ERR')
|
||||
nb_erroneous += 1
|
||||
for err in errors:
|
||||
error_str = '\t{0}'.format(err['err_msg'])
|
||||
if err['table']:
|
||||
error_str += ' (table: {0}'.format(err['table'])
|
||||
if err['column']:
|
||||
error_str += ', column: {0}'.format(
|
||||
err['column'])
|
||||
error_str += ')'
|
||||
print(error_str)
|
||||
print('*** Summary:')
|
||||
print(('Valid migrations: {0}/{1} - '
|
||||
'erroneous migrations: {2}/{1} - '
|
||||
'ignored migrations: {3}/{1}').format(
|
||||
nb_valid,
|
||||
len(self.changed_migration_files),
|
||||
nb_erroneous,
|
||||
nb_ignore))
|
||||
return nb_erroneous > 0
|
||||
|
||||
@classmethod
|
||||
def _split_migration_path(cls, migration_path):
|
||||
decomposed_path = split_path(migration_path)
|
||||
for i, p in enumerate(decomposed_path):
|
||||
if p == cls.MIGRATION_FOLDER_NAME:
|
||||
return (decomposed_path[i-1],
|
||||
os.path.splitext(decomposed_path[i+1])[0])
|
||||
def _gather_all_migrations(self):
|
||||
migrations = []
|
||||
for root, dirs, files in os.walk(self.django_path):
|
||||
for file_name in files:
|
||||
if os.path.basename(root) == self.MIGRATION_FOLDER_NAME and \
|
||||
file_name.endswith('.py') and \
|
||||
file_name != '__init__.py':
|
||||
full_migration_path = os.path.join(root, file_name)
|
||||
app_name, migration_name = self._split_migration_path(full_migration_path)
|
||||
migrations.append((app_name, migration_name))
|
||||
return migrations
|
||||
|
||||
def should_ignore_migration(self, app_name, migration_name):
|
||||
return (self.include_apps and
|
||||
@@ -178,73 +186,17 @@ class MigrationLinter:
|
||||
self.ignore_name_contains in migration_name)\
|
||||
or (migration_name in self.ignore_name)
|
||||
|
||||
def django_sqlmigrate(self, app_name, migration_name):
|
||||
git_diff_command = (
|
||||
'cd {0} && '
|
||||
'python manage.py sqlmigrate {1} {2} '
|
||||
'--database {3}').format(
|
||||
self.location, app_name, migration_name, self.database)
|
||||
log.info('Executing {0}'.format(git_diff_command))
|
||||
sqlmigrate_process = Popen(
|
||||
git_diff_command, shell=True, stdout=PIPE, stderr=PIPE)
|
||||
sql_statements = []
|
||||
for line in map(
|
||||
clean_bytes_to_str, sqlmigrate_process.stdout.readlines()):
|
||||
if not line.startswith('--'): # Ignore comments
|
||||
sql_statements.append(line)
|
||||
sqlmigrate_process.wait()
|
||||
if sqlmigrate_process.returncode != 0:
|
||||
raise RuntimeError('sqlmigrate command failed')
|
||||
log.info('Found {0} sql migration lines'.format(len(sql_statements)))
|
||||
return sql_statements
|
||||
|
||||
def _test_sql_statement_for_backward_incompatibility(
|
||||
self, sql_statement, errors):
|
||||
for test in self.migration_tests:
|
||||
if test['fn'](sql_statement, errors=errors):
|
||||
log.info('Testing {0} -- ERROR'.format(sql_statement))
|
||||
table_search = re.search(
|
||||
'TABLE `([^`]*)`', sql_statement, re.IGNORECASE)
|
||||
col_search = re.search(
|
||||
'COLUMN `([^`]*)`', sql_statement, re.IGNORECASE)
|
||||
err = {
|
||||
'err_msg': test['err_msg'],
|
||||
'code': test['code'],
|
||||
'table': table_search.group(1) if table_search else None,
|
||||
'column': col_search.group(1) if col_search else None
|
||||
}
|
||||
return False, err
|
||||
log.info('Testing {0} -- PASSED'.format(sql_statement))
|
||||
return True, None
|
||||
|
||||
|
||||
def valid_folder(folder):
|
||||
"""Verify folder exists,
|
||||
folder is a django project
|
||||
and folder is git versioned
|
||||
"""
|
||||
if not os.path.isdir(folder):
|
||||
print("The passed argument doesn't seem to be a folder.")
|
||||
return False
|
||||
django_manage_file = os.path.join(folder, 'manage.py')
|
||||
if not os.path.isfile(django_manage_file):
|
||||
print(("The passed folder doesn't seem to be a "
|
||||
"django project (no manage.py found)."))
|
||||
return False
|
||||
git_folder = os.path.join(folder, '.git')
|
||||
if not os.path.isdir(git_folder):
|
||||
print(("The passed folder doesn't seem to be "
|
||||
"versioned by git (no .git/ folder found)."))
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def split_path(path):
|
||||
a, b = os.path.split(path)
|
||||
return (split_path(a) if len(a) > 0 else []) + [b]
|
||||
@classmethod
|
||||
def _split_migration_path(cls, migration_path):
|
||||
decomposed_path = utils.split_path(migration_path)
|
||||
for i, p in enumerate(decomposed_path):
|
||||
if p == cls.MIGRATION_FOLDER_NAME:
|
||||
return (decomposed_path[i-1],
|
||||
os.path.splitext(decomposed_path[i+1])[0])
|
||||
|
||||
|
||||
def _main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Detect backward incompatible django migrations.')
|
||||
parser.add_argument(
|
||||
@@ -302,19 +254,18 @@ def _main():
|
||||
log.basicConfig(format='%(message)s')
|
||||
|
||||
folder_name = args.django_folder[0]
|
||||
if valid_folder(folder_name):
|
||||
linter = MigrationLinter(
|
||||
folder_name,
|
||||
args.commit_id,
|
||||
ignore_name_contains=args.ignore_name_contains,
|
||||
ignore_name=args.ignore_name,
|
||||
include_apps=args.include_apps,
|
||||
exclude_apps=args.exclude_apps,
|
||||
database=args.database)
|
||||
has_errors = linter.lint_migrations()
|
||||
if has_errors:
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Create and use linter
|
||||
linter = MigrationLinter(
|
||||
folder_name,
|
||||
ignore_name_contains=args.ignore_name_contains,
|
||||
ignore_name=args.ignore_name,
|
||||
include_apps=args.include_apps,
|
||||
exclude_apps=args.exclude_apps,
|
||||
database=args.database)
|
||||
linter.lint_all_migrations(
|
||||
git_commit_id=args.commit_id)
|
||||
linter.print_summary()
|
||||
if linter.has_errors:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Copyright 2017 3YOURMIND GmbH
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import re
|
||||
import logging as log
|
||||
|
||||
|
||||
def has_default(sql, **kwargs):
|
||||
if re.search('SET DEFAULT', sql) and kwargs['errors']:
|
||||
err = next(err
|
||||
for err in kwargs['errors']
|
||||
if err['code'] == 'NOT_NULL')
|
||||
if err:
|
||||
log.info(
|
||||
('Found a NOT_NULL error in migration, '
|
||||
'but it has a default value added: {}').format(err))
|
||||
kwargs['errors'].remove(err)
|
||||
|
||||
return False # Never fails
|
||||
|
||||
|
||||
migration_tests = (
|
||||
{
|
||||
'code': 'NOT_NULL',
|
||||
'fn': lambda sql, **kw: re.search('NOT NULL', sql) and
|
||||
not re.search('CREATE TABLE', sql),
|
||||
'err_msg': 'NOT NULL constraint on columns'
|
||||
}, {
|
||||
'code': 'DROP_COLUMN',
|
||||
'fn': lambda sql, **kw: re.search('DROP COLUMN', sql),
|
||||
'err_msg': 'DROPPING columns'
|
||||
}, {
|
||||
'code': 'RENAME_COLUMN',
|
||||
'fn': lambda sql, **kw: re.search('ALTER TABLE .* CHANGE', sql) or
|
||||
re.search('ALTER TABLE .* RENAME COLUMN', sql),
|
||||
'err_msg': 'RENAMING columns'
|
||||
}, {
|
||||
'code': 'RENAME_TABLE',
|
||||
'fn': lambda sql, **kw: re.search('RENAME TABLE', sql) or
|
||||
re.search('ALTER TABLE .* RENAME TO', sql),
|
||||
'err_msg': 'RENAMING tables'
|
||||
}, {
|
||||
'code': '',
|
||||
'fn': has_default,
|
||||
'err_msg': ''
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def analyse_sql_statements(sql_statements):
|
||||
errors = []
|
||||
for statement in sql_statements:
|
||||
for test in migration_tests:
|
||||
if test['fn'](statement, errors=errors):
|
||||
log.info('Testing {0} -- ERROR'.format(statement))
|
||||
table_search = re.search(
|
||||
'TABLE `([^`]*)`', statement, re.IGNORECASE)
|
||||
col_search = re.search(
|
||||
'COLUMN `([^`]*)`', statement, re.IGNORECASE)
|
||||
err = {
|
||||
'err_msg': test['err_msg'],
|
||||
'code': test['code'],
|
||||
'table': table_search.group(1) if table_search else None,
|
||||
'column': col_search.group(1) if col_search else None
|
||||
}
|
||||
errors.append(err)
|
||||
else:
|
||||
log.info('Testing {0} -- PASSED'.format(statement))
|
||||
return {
|
||||
'errors': errors,
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
# Copyright 2017 3YOURMIND GmbH
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
def is_django_project(path):
|
||||
django_manage_file = os.path.join(path, 'manage.py')
|
||||
return os.path.isfile(django_manage_file)
|
||||
|
||||
|
||||
def is_git_project(path):
|
||||
git_directory = os.path.join(path, '.git')
|
||||
return os.path.isdir(git_directory)
|
||||
|
||||
|
||||
def is_directory(path):
|
||||
return os.path.isdir(path)
|
||||
|
||||
|
||||
def print_stderr(*args, **kwargs):
|
||||
print(*args, file=sys.stderr, **kwargs)
|
||||
|
||||
|
||||
def find_project_settings_module(path):
|
||||
"""Explore path recursively to the first settings.py file
|
||||
and translate the name to package notation 'mysite.settings'
|
||||
"""
|
||||
for root, dirs, files in os.walk(path):
|
||||
for file_name in files:
|
||||
if file_name == 'settings.py':
|
||||
return os.path.join(root.replace(path, ''), file_name).replace('/', '.').rstrip('.py')
|
||||
|
||||
|
||||
def split_path(path):
|
||||
a, b = os.path.split(path)
|
||||
return (split_path(a) if (len(a) > 0 and a != '/') else []) + [b]
|
||||
|
||||
|
||||
def clean_bytes_to_str(byte_input):
|
||||
return byte_input.decode('utf-8').strip()
|
||||
+2
-6
@@ -34,6 +34,8 @@ ADD_NOT_NULL_COLUMN_FOLLOWED_BY_DEFAULT_PROJECT = os.path.join(
|
||||
_FIXTURES_FOLDER, 'test_project_add_not_null_column_followed_by_default/')
|
||||
MULTI_COMMIT_PROJECT = os.path.join(
|
||||
_FIXTURES_FOLDER, 'test_project_multi_commit/')
|
||||
CORRECT_PROJECT = os.path.join(
|
||||
_FIXTURES_FOLDER, 'test_correct_project/')
|
||||
|
||||
NOT_DJANGO_GIT_PROJECT = os.path.join(
|
||||
_FIXTURES_FOLDER, 'test_git_project/')
|
||||
@@ -42,12 +44,6 @@ NOT_GIT_DJANGO_PROJECT = os.path.join(
|
||||
|
||||
ALL_GIT_PROJECTS = (
|
||||
NOT_DJANGO_GIT_PROJECT,
|
||||
ADD_NOT_NULL_COLUMN_PROJECT,
|
||||
CREATE_TABLE_WITH_NOT_NULL_COLUMN_PROJECT,
|
||||
DROP_COLUMN_PROJECT,
|
||||
RENAME_COLUMN_PROJECT,
|
||||
RENAME_TABLE_PROJECT,
|
||||
ADD_NOT_NULL_COLUMN_FOLLOWED_BY_DEFAULT_PROJECT,
|
||||
MULTI_COMMIT_PROJECT
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import unittest
|
||||
|
||||
class CallLinterFromCommandLineTest(unittest.TestCase):
|
||||
pass
|
||||
@@ -66,19 +66,19 @@ class BackwardcompatibilityDetectionTest(unittest.TestCase):
|
||||
fixtures.prepare_git_project(test_project_path)
|
||||
self._test_linter_finds_no_errors(
|
||||
test_project_path,
|
||||
commit_id='44330921696018f8f1480df897b38310549982cb')
|
||||
commit_id='1021d98b5943db56122c1f848f371ddc38788d0f')
|
||||
|
||||
#def test_specify_git_hash_by_tag(self):
|
||||
# test_project_path = fixtures.MULTI_COMMIT_PROJECT
|
||||
# fixtures.prepare_git_project(test_project_path)
|
||||
# self._test_linter_finds_no_errors(test_project_path, commit_id='tag1')
|
||||
def test_specify_git_hash_by_tag(self):
|
||||
test_project_path = fixtures.MULTI_COMMIT_PROJECT
|
||||
fixtures.prepare_git_project(test_project_path)
|
||||
self._test_linter_finds_no_errors(test_project_path, commit_id='tag1')
|
||||
|
||||
def _test_linter_finds_errors(self, path, commit_id=None):
|
||||
linter = MigrationLinter(path, commit_id=commit_id)
|
||||
has_errors = linter.lint_migrations()
|
||||
self.assertTrue(has_errors)
|
||||
linter = MigrationLinter(path)
|
||||
linter.lint_all_migrations(git_commit_id=commit_id)
|
||||
self.assertTrue(linter.has_errors)
|
||||
|
||||
def _test_linter_finds_no_errors(self, path, commit_id=None):
|
||||
linter = MigrationLinter(path, commit_id=commit_id)
|
||||
has_errors = linter.lint_migrations()
|
||||
self.assertFalse(has_errors)
|
||||
linter = MigrationLinter(path)
|
||||
linter.lint_all_migrations(git_commit_id=commit_id)
|
||||
self.assertFalse(linter.has_errors)
|
||||
@@ -0,0 +1,23 @@
|
||||
import unittest
|
||||
|
||||
from django_migration_linter import MigrationLinter
|
||||
from tests import fixtures
|
||||
|
||||
class TestMultipleLinters(unittest.TestCase):
|
||||
def test_multiple_linters(self):
|
||||
l1 = MigrationLinter(fixtures.ADD_NOT_NULL_COLUMN_PROJECT)
|
||||
l2 = MigrationLinter(fixtures.RENAME_COLUMN_PROJECT)
|
||||
l3 = MigrationLinter(fixtures.CORRECT_PROJECT)
|
||||
|
||||
l1.lint_migration('test_app', '0002')
|
||||
l2.lint_migration('test_app', '0002')
|
||||
l3.lint_migration('test_app1', '0001')
|
||||
l3.lint_migration('test_app1', '0002')
|
||||
|
||||
self.assertTrue(l1.has_errors)
|
||||
self.assertTrue(l2.has_errors)
|
||||
self.assertFalse(l3.has_errors)
|
||||
|
||||
self.assertEqual(l1.nb_total, 1)
|
||||
self.assertEqual(l2.nb_total, 1)
|
||||
self.assertEqual(l3.nb_total, 2)
|
||||
@@ -0,0 +1,3 @@
|
||||
*.pyc
|
||||
__pycache__/
|
||||
*sqlite3
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError:
|
||||
# The above import may fail for some other reason. Ensure that the
|
||||
# issue is really that Django is missing to avoid masking other
|
||||
# exceptions on Python 2.
|
||||
try:
|
||||
import django
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
)
|
||||
raise
|
||||
execute_from_command_line(sys.argv)
|
||||
Whitespace-only changes.
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TestApp1Config(AppConfig):
|
||||
name = 'test_app1'
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.7 on 2017-11-27 15:49
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='A',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('null_field', models.IntegerField(null=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.7 on 2017-11-27 15:49
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('test_app1', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='a',
|
||||
name='new_null_field',
|
||||
field=models.IntegerField(null=True),
|
||||
),
|
||||
]
|
||||
Whitespace-only changes.
@@ -0,0 +1,5 @@
|
||||
from django.db import models
|
||||
|
||||
class A(models.Model):
|
||||
null_field = models.IntegerField(null=True)
|
||||
new_null_field = models.IntegerField(null=True)
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Whitespace-only changes.
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TestApp2Config(AppConfig):
|
||||
name = 'test_app2'
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.7 on 2017-11-29 21:08
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='B',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('null_field', models.IntegerField(null=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
Whitespace-only changes.
@@ -0,0 +1,4 @@
|
||||
from django.db import models
|
||||
|
||||
class B(models.Model):
|
||||
null_field = models.IntegerField(null=True)
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Whitespace-only changes.
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
Django settings for test_project project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 1.11.6.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.11/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.11/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'gtb+mohs%gf0+#fr89vw7e&#edmv=q8!iv+n&j_soawk$tkj3^'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'test_app1',
|
||||
'test_app2'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'test_project.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'test_project.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django_fake_database_backends.backends.mysql'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
@@ -0,0 +1,21 @@
|
||||
"""linter_test_project URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/1.11/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.conf.urls import url, include
|
||||
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.conf.urls import url
|
||||
from django.contrib import admin
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', admin.site.urls),
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for linter_test_project project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
@@ -1,22 +0,0 @@
|
||||
All files
|
||||
|
||||
# Veuillez saisir le message de validation pour vos modifications. Les lignes
|
||||
# commençant par '#' seront ignorées, et un message vide abandonne la validation.
|
||||
# Sur la branche master
|
||||
# Modifications qui seront validées :
|
||||
# nouveau fichier: .gitignore
|
||||
# nouveau fichier: manage.py
|
||||
# nouveau fichier: test_app/__init__.py
|
||||
# nouveau fichier: test_app/admin.py
|
||||
# nouveau fichier: test_app/apps.py
|
||||
# nouveau fichier: test_app/migrations/0001_create_table.py
|
||||
# nouveau fichier: test_app/migrations/0002_add_new_not_null_field.py
|
||||
# nouveau fichier: test_app/migrations/__init__.py
|
||||
# nouveau fichier: test_app/models.py
|
||||
# nouveau fichier: test_app/tests.py
|
||||
# nouveau fichier: test_app/views.py
|
||||
# nouveau fichier: test_project_add_not_null_column/__init__.py
|
||||
# nouveau fichier: test_project_add_not_null_column/settings.py
|
||||
# nouveau fichier: test_project_add_not_null_column/urls.py
|
||||
# nouveau fichier: test_project_add_not_null_column/wsgi.py
|
||||
#
|
||||
@@ -1 +0,0 @@
|
||||
ref: refs/heads/master
|
||||
@@ -1,5 +0,0 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = false
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
@@ -1 +0,0 @@
|
||||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
Binary file not shown.
@@ -1,6 +0,0 @@
|
||||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
@@ -1,3 +0,0 @@
|
||||
0000000000000000000000000000000000000000 13850658e796761657478d38eacd426e208b05b0 David-Wobrock <david.wobrock@gmail.com> 1509046620 +0200 commit (initial): Init
|
||||
13850658e796761657478d38eacd426e208b05b0 0c78bfd0462272a51e3515731a6e6776bea72f72 David-Wobrock <david.wobrock@gmail.com> 1509046634 +0200 commit: All files
|
||||
0c78bfd0462272a51e3515731a6e6776bea72f72 13ddff99f80afca39dc64eaa65928244584a88d3 David-Wobrock <david.wobrock@gmail.com> 1509046854 +0200 commit (amend): All files
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
0000000000000000000000000000000000000000 13850658e796761657478d38eacd426e208b05b0 David-Wobrock <david.wobrock@gmail.com> 1509046620 +0200 commit (initial): Init
|
||||
13850658e796761657478d38eacd426e208b05b0 0c78bfd0462272a51e3515731a6e6776bea72f72 David-Wobrock <david.wobrock@gmail.com> 1509046634 +0200 commit: All files
|
||||
0c78bfd0462272a51e3515731a6e6776bea72f72 13ddff99f80afca39dc64eaa65928244584a88d3 David-Wobrock <david.wobrock@gmail.com> 1509046854 +0200 commit (amend): All files
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
x]PËjÃ0ìÙ_±�ƒ�;Ôm øRrÈ·„m�ZY2zÒ¯¯dåY„˜™�™UÃee™?Ì }L¡•”‰~Öté«G¢¼£@U¤ÐaóU‹^B‘EöRÀ2/^Ò"O—%O«¼Œ:% ¤³Æ*$Ø0JeÀ
|
||||
æÌ‘pfœ×QÒÉ/£ÍY7°Þ…1)ô7à•QËka¦’«(»€óU�;L0Ãj|(‹£8¢ (Z†ÚŸ“ð89úí|à…ñ67o
|
||||
Ýú{_&™&=ï�¨¬âu¼¸C;†œê*„a¸“˜Ñø¼U¶¶Fn½4©Ý‹´S|錊␍µ:’o<ž�ÊmÅ~±Úºßs’T�ÔHB‰Ý&žÏï{øÐ$–s2UºFï„ÁUH÷‚)ãÿüájwr>Dqá£&
|
||||
BIN
Binary file not shown.
-2
@@ -1,2 +0,0 @@
|
||||
x¥�Á
|
||||
B!E[û³�óL…ˆmú‚ÖŽÎ+)Äê÷3ú„–ç¹'ÖRr‡ÙmW½‰€a§mŠdtdö Å^\"¯�[XÄx4*<ûµ68†WN›såVã␍vé‹Óû‡‡K ù>ÅZö0[‘FX£FTcá.\¨Ó#wõ"";ã
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
x•ŽA
|
||||
Â0E]çÙ‹ešf&SQð®ÓɨÅÖH�z}+âü»÷�'yûb]C‹2©ZñÞÍKNñ-A”V# 0
|
||||
×,€ ÄÜâ¤×bë†YCK�jÂ৆5JòŽÔw€˜ø(ç<Ù}|öiuÈÝ”åb×éƒÕë‹ÛÓû¡’<nl�Ђ'j¼]‚0ó;‡ý[ÁøS˜Ý0Øc?èݼî¦Ib
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
xm�OOAÅ=ϧhö‰îãE& Á(ÀxœvºË�Ît3S@¿½Ëžš¦¯ï÷Ú5ñîoŠ¢P_ËéjŽ�o¡áä£`2‚YL—x‹µÀ¥–JÍð»ãŒdƒp^¶DvM6ƒ…ÀnGxG¸G‚½Mþ4Š6 ƒª²]G¾¶â9VUï÷Ú'{x8␍€coî34žð2¢ÚˆtùIkÇu.ÝÖÆ–ÿ2Õ4F=*G#½áƒ°vØÿŒ¢¹õZÏT>tœ8+Õ$pv)ë^ep´(æØ›«¨Jq.1î}âXf‡�Ý‘ŠñÛË|º0ËÉj5›O—æc1þ|Ÿ·P\ÿÏXçLd1qGdj¦]8Ùˆ�m.†J]¡àþK0ª_òCš€
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1 +0,0 @@
|
||||
13ddff99f80afca39dc64eaa65928244584a88d3
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
All files
|
||||
|
||||
# Veuillez saisir le message de validation pour vos modifications. Les lignes
|
||||
# commençant par '#' seront ignorées, et un message vide abandonne la validation.
|
||||
# Sur la branche master
|
||||
# Modifications qui seront validées :
|
||||
# nouveau fichier: .gitignore
|
||||
# nouveau fichier: manage.py
|
||||
# nouveau fichier: test_app/__init__.py
|
||||
# nouveau fichier: test_app/admin.py
|
||||
# nouveau fichier: test_app/apps.py
|
||||
# nouveau fichier: test_app/migrations/0001_create_table.py
|
||||
# nouveau fichier: test_app/migrations/0002_add_new_not_null_field.py
|
||||
# nouveau fichier: test_app/migrations/__init__.py
|
||||
# nouveau fichier: test_app/migrations_utils.py
|
||||
# nouveau fichier: test_app/models.py
|
||||
# nouveau fichier: test_app/tests.py
|
||||
# nouveau fichier: test_app/views.py
|
||||
# nouveau fichier: test_project_add_not_null_column_followed_by_default/__init__.py
|
||||
# nouveau fichier: test_project_add_not_null_column_followed_by_default/settings.py
|
||||
# nouveau fichier: test_project_add_not_null_column_followed_by_default/urls.py
|
||||
# nouveau fichier: test_project_add_not_null_column_followed_by_default/wsgi.py
|
||||
#
|
||||
-1
@@ -1 +0,0 @@
|
||||
ref: refs/heads/master
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = false
|
||||
bare = false
|
||||
logallrefupdates = true
|
||||
-1
@@ -1 +0,0 @@
|
||||
Unnamed repository; edit this file 'description' to name the repository.
|
||||
BIN
Binary file not shown.
-6
@@ -1,6 +0,0 @@
|
||||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
0000000000000000000000000000000000000000 188d1e847da762e20ec43db374ffba8c5ee40581 David-Wobrock <david.wobrock@gmail.com> 1509048870 +0200 commit (initial): Init
|
||||
188d1e847da762e20ec43db374ffba8c5ee40581 1ed2db048937886ab77c892d7ffa4bb3da6ae8a9 David-Wobrock <david.wobrock@gmail.com> 1509048882 +0200 commit: All files
|
||||
1ed2db048937886ab77c892d7ffa4bb3da6ae8a9 4c2edb394b46664ac61f0b6fb29e57fe807ce21e David-Wobrock <david.wobrock@gmail.com> 1509049863 +0200 commit (amend): All files
|
||||
4c2edb394b46664ac61f0b6fb29e57fe807ce21e 16fd55a08b9ad1af4282cafccf5c14adf1f2a1dc David-Wobrock <david.wobrock@gmail.com> 1509381987 +0100 commit (amend): All files
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
0000000000000000000000000000000000000000 188d1e847da762e20ec43db374ffba8c5ee40581 David-Wobrock <david.wobrock@gmail.com> 1509048870 +0200 commit (initial): Init
|
||||
188d1e847da762e20ec43db374ffba8c5ee40581 1ed2db048937886ab77c892d7ffa4bb3da6ae8a9 David-Wobrock <david.wobrock@gmail.com> 1509048882 +0200 commit: All files
|
||||
1ed2db048937886ab77c892d7ffa4bb3da6ae8a9 4c2edb394b46664ac61f0b6fb29e57fe807ce21e David-Wobrock <david.wobrock@gmail.com> 1509049863 +0200 commit (amend): All files
|
||||
4c2edb394b46664ac61f0b6fb29e57fe807ce21e 16fd55a08b9ad1af4282cafccf5c14adf1f2a1dc David-Wobrock <david.wobrock@gmail.com> 1509381987 +0100 commit (amend): All files
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
x]PËjÃ0ìÙ_±�ƒ�;Ôm øRrÈ·„m�ZY2zÒ¯¯dåY„˜™�™UÃee™?Ì }L¡•”‰~Öté«G¢¼£@U¤ÐaóU‹^B‘EöRÀ2/^Ò"O—%O«¼Œ:% ¤³Æ*$Ø0JeÀ
|
||||
æÌ‘pfœ×QÒÉ/£ÍY7°Þ…1)ô7à•QËka¦’«(»€óU�;L0Ãj|(‹£8¢ (Z†ÚŸ“ð89úí|à…ñ67o
|
||||
Ýú{_&™&=ï�¨¬âu¼¸C;†œê*„a¸“˜Ñø¼U¶¶Fn½4©Ý‹´S|錊␍µ:’o<ž�ÊmÅ~±Úºßs’T�ÔHB‰Ý&žÏï{øÐ$–s2UºFï„ÁUH÷‚)ãÿüájwr>Dqá£&
|
||||
BIN
Binary file not shown.
-3
@@ -1,3 +0,0 @@
|
||||
x•ÎM
|
||||
Â0†a×9Eöb™üOADÁ;¸N2S-¶FjÔëñ.ßoñðå2Ïc•ÚøU]˜%š”}Šˆ �í!xta }*iŸ ™,îqá[•
|
||||
‘£␍ƒ×¬�³5”L°ÃИì˜-8T">ë¥,ò_#mN%-%_å–¾Ù½¹?Ïqœº\æ�Tz°ˆ¨å4€hk;Zù? ê14B5B¦IãÄñøK&
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
xm�OOAÅ=ϧhö‰îãE& Á(ÀxœvºË�Ît3S@¿½Ëžš¦¯ï÷Ú5ñîoŠ¢P_ËéjŽ�o¡áä£`2‚YL—x‹µÀ¥–JÍð»ãŒdƒp^¶DvM6ƒ…ÀnGxG¸G‚½Mþ4Š6 ƒª²]G¾¶â9VUï÷Ú'{x8␍€coî34žð2¢ÚˆtùIkÇu.ÝÖÆ–ÿ2Õ4F=*G#½áƒ°vØÿŒ¢¹õZÏT>tœ8+Õ$pv)ë^ep´(æØ›«¨Jq.1î}âXf‡�Ý‘ŠñÛË|º0ËÉj5›O—æc1þ|Ÿ·P\ÿÏXçLd1qGdj¦]8Ùˆ�m.†J]¡àþK0ª_òCš€
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
-2
@@ -1,2 +0,0 @@
|
||||
x‘1OÃ0…™ó+Nê�5Q\A@•2 UE¬,YN|‰Œ;rì"þ=vÜ´…®x|~þÞ½s-u␍eYÞ, »Í Ñ\¨nζÙcP’<£BÃ,r¨¿a÷ÉT§�ä„ä%h›‚<d¤È6÷@î¶E™´F÷@ië¬3H)ˆ~ÐÆ‚SÂÑJa=MŽI4ò‰—ózöõ¢óaB«q␍½prZ-eÃpm¤ÎŠ`k$Gx�Ë3*?‰«m’€?TU#p„
|
||||
Þ'1\,Ó9(]CZ¡�A_ŸZVKLWëÉú1z« Óþ‚\$?q¾(ùò”R¦fT±«”¥ôp¢ªð‹*m©rRÒ6þØ&Š+Ê_”ÅMLâØ2'mE޳F,À`pDs@:öþð~a?ˆËÍ}�]ľ1éðßÛµJÉEÉÕqÑ?8…ÆJ
|
||||
Loaded 100 of 256 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user