From f692a39030488531e08487d105e27f8cea43f78a Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Sat, 20 Jan 2018 16:24:31 +0100 Subject: [PATCH 01/46] Add sentence about example integration in readme --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 30875a0..2e80e6f 100644 --- a/README.rst +++ b/README.rst @@ -50,6 +50,7 @@ Examples -------- 3YOURMIND is running the linter on `Bitbucket Pipelines`_ on every build getting pushed. +Checkout the ``examples/`` folder to see how you could integrate the linter in your test suite. Tests ----- From 73644550731ad51fc87015f2c46a4f62d2685d47 Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Mon, 7 May 2018 20:32:12 +0200 Subject: [PATCH 02/46] Add test to assert that a not null FK on a new table is accepted --- .../migrations/0002_b_foreign_keys.py | 22 +++++++++++++++++++ .../test_app/models.py | 5 +++++ 2 files changed, 27 insertions(+) create mode 100644 tests/test_project_fixtures/test_project_create_table_with_not_null_column/test_app/migrations/0002_b_foreign_keys.py diff --git a/tests/test_project_fixtures/test_project_create_table_with_not_null_column/test_app/migrations/0002_b_foreign_keys.py b/tests/test_project_fixtures/test_project_create_table_with_not_null_column/test_app/migrations/0002_b_foreign_keys.py new file mode 100644 index 0000000..9ffdb89 --- /dev/null +++ b/tests/test_project_fixtures/test_project_create_table_with_not_null_column/test_app/migrations/0002_b_foreign_keys.py @@ -0,0 +1,22 @@ +# Generated by Django 2.0.1 on 2018-05-07 18:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('test_app', '0001_create_table_with_not_null_field'), + ] + + operations = [ + migrations.CreateModel( + name='B', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('not_null_fk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='nn_b', to='test_app.A')), + ('null_fk', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='b', to='test_app.A')), + ], + ), + ] diff --git a/tests/test_project_fixtures/test_project_create_table_with_not_null_column/test_app/models.py b/tests/test_project_fixtures/test_project_create_table_with_not_null_column/test_app/models.py index 76cc5dc..b347a42 100644 --- a/tests/test_project_fixtures/test_project_create_table_with_not_null_column/test_app/models.py +++ b/tests/test_project_fixtures/test_project_create_table_with_not_null_column/test_app/models.py @@ -2,3 +2,8 @@ from django.db import models class A(models.Model): not_null_field = models.IntegerField(null=False) + + +class B(models.Model): + not_null_fk = models.ForeignKey(to=A, null=False, on_delete=models.CASCADE, related_name='nn_b') + null_fk = models.ForeignKey(to=A, null=True, on_delete=models.CASCADE, related_name='b') From 10d36bbab240d27d5340749cf0badcca1de0cfe1 Mon Sep 17 00:00:00 2001 From: Serj Zavadsky Date: Mon, 16 Jul 2018 12:31:55 +0200 Subject: [PATCH 03/46] logging config fixed (now logging is made not by `root` logger, but by 'django_migration_linter.sql_analyzer') --- django_migration_linter/migration_linter.py | 4 +++- django_migration_linter/sql_analyser.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index 00a44eb..ac4169f 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -13,7 +13,7 @@ # limitations under the License. from __future__ import print_function -import logging as log +import logging import os import re from subprocess import Popen, PIPE @@ -21,6 +21,8 @@ import sys from . import utils from .sql_analyser import analyse_sql_statements +log = logging.getLogger(__name__) + class MigrationLinter(object): MIGRATION_FOLDER_NAME = 'migrations' diff --git a/django_migration_linter/sql_analyser.py b/django_migration_linter/sql_analyser.py index 636be21..f1d074f 100644 --- a/django_migration_linter/sql_analyser.py +++ b/django_migration_linter/sql_analyser.py @@ -13,7 +13,9 @@ # limitations under the License. import re -import logging as log +import logging + +log = logging.getLogger(__name__) def has_default(sql, **kwargs): From bfb32ba312860f579ed2e3e213d503a4c6393956 Mon Sep 17 00:00:00 2001 From: Serj Zavadsky Date: Mon, 16 Jul 2018 12:50:41 +0200 Subject: [PATCH 04/46] logging config fixed --- django_migration_linter/migration_linter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index ac4169f..f37a5bf 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -267,9 +267,9 @@ def _main(): args = parser.parse_args() if args.verbose: - log.basicConfig(format='%(message)s', level=log.DEBUG) + logging.basicConfig(format='%(message)s', level=logging.DEBUG) else: - log.basicConfig(format='%(message)s') + logging.basicConfig(format='%(message)s') folder_name = args.django_folder[0] # Create and use linter From 555d17adcddcce6728ac75b0e0cff27d0c70cf63 Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Thu, 19 Jul 2018 22:03:37 +0200 Subject: [PATCH 05/46] Rename log object to logger (to follow the standard) --- django_migration_linter/migration_linter.py | 10 +++++----- django_migration_linter/sql_analyser.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index f37a5bf..c4cd0f0 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -21,7 +21,7 @@ import sys from . import utils from .sql_analyser import analyse_sql_statements -log = logging.getLogger(__name__) +logger = logging.getLogger(__name__) class MigrationLinter(object): @@ -134,7 +134,7 @@ class MigrationLinter(object): self.django_path, self.python_exe, app_name, migration_name, self.database) - log.info('Executing {0}'.format(sqlmigrate_command)) + logger.info('Executing {0}'.format(sqlmigrate_command)) sqlmigrate_process = Popen( sqlmigrate_command, shell=True, stdout=PIPE, stderr=PIPE) @@ -148,7 +148,7 @@ class MigrationLinter(object): _, err = sqlmigrate_process.communicate() raise RuntimeError('sqlmigrate command failed {0}'.format( err.decode('utf-8'))) - log.info('Found {0} sql migration lines'.format(len(sql_statements))) + logger.info('Found {0} sql migration lines'.format(len(sql_statements))) return sql_statements def _gather_migrations_git(self, git_commit_id): @@ -158,7 +158,7 @@ class MigrationLinter(object): 'cd {0} && ' 'git diff --name-only --diff-filter=A {1}').format( self.django_path, git_commit_id) - log.info('Executing {0}'.format(git_diff_command)) + logger.info('Executing {0}'.format(git_diff_command)) diff_process = Popen( git_diff_command, shell=True, stdout=PIPE, stderr=PIPE) for line in map( @@ -177,7 +177,7 @@ class MigrationLinter(object): for line in map( utils.clean_bytes_to_str, diff_process.stderr.readlines()): output.append(line) - log.info("Error while git diff command:\n{}".format( + logger.info("Error while git diff command:\n{}".format( "".join(output))) raise Exception('Error while executing git diff command') return migrations diff --git a/django_migration_linter/sql_analyser.py b/django_migration_linter/sql_analyser.py index f1d074f..aa06ec2 100644 --- a/django_migration_linter/sql_analyser.py +++ b/django_migration_linter/sql_analyser.py @@ -15,7 +15,7 @@ import re import logging -log = logging.getLogger(__name__) +logger = logging.getLogger(__name__) def has_default(sql, **kwargs): @@ -24,7 +24,7 @@ def has_default(sql, **kwargs): for err in kwargs['errors'] if err['code'] == 'NOT_NULL') if err: - log.info( + logger.info( ('Found a NOT_NULL error in migration, ' 'but it has a default value added: {}').format(err)) kwargs['errors'].remove(err) @@ -70,7 +70,7 @@ def analyse_sql_statements(sql_statements): for statement in sql_statements: for test in migration_tests: if test['fn'](statement, errors=errors): - log.info('Testing {0} -- ERROR'.format(statement)) + logger.info('Testing {0} -- ERROR'.format(statement)) table_search = re.search( 'TABLE `([^`]*)`', statement, re.IGNORECASE) col_search = re.search( @@ -83,7 +83,7 @@ def analyse_sql_statements(sql_statements): } errors.append(err) else: - log.info('Testing {0} -- PASSED'.format(statement)) + logger.info('Testing {0} -- PASSED'.format(statement)) return { 'errors': errors, } From d672f0663518a62400ebe3d62612367993ccdff3 Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Thu, 19 Jul 2018 22:14:13 +0200 Subject: [PATCH 06/46] Drop django 1.10 support (and fix some PEP8) --- .travis.yml | 6 ------ django_migration_linter/migration_linter.py | 3 ++- setup.py | 3 +-- tox.ini | 2 -- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5fea858..759aa10 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,20 +3,14 @@ language: python matrix: include: - - python: 2.7 - env: TOXENV=py27-django110 - python: 2.7 env: TOXENV=py27-django111 - - python: 3.4 - env: TOXENV=py34-django110 - python: 3.4 env: TOXENV=py34-django111 - python: 3.4 env: TOXENV=py34-django20 - - python: 3.5 - env: TOXENV=py35-django110 - python: 3.5 env: TOXENV=py35-django111 - python: 3.5 diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index c4cd0f0..b2cd6aa 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -148,7 +148,8 @@ class MigrationLinter(object): _, err = sqlmigrate_process.communicate() raise RuntimeError('sqlmigrate command failed {0}'.format( err.decode('utf-8'))) - logger.info('Found {0} sql migration lines'.format(len(sql_statements))) + logger.info( + 'Found {0} sql migration lines'.format(len(sql_statements))) return sql_statements def _gather_migrations_git(self, git_commit_id): diff --git a/setup.py b/setup.py index f50c7d5..01b7a49 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ with open(path.join(PROJECT_DIR, 'README.rst')) as f: long_description = f.read() install_requirements = [ - 'django>=1.10', + 'django>=1.11', ] test_requirements = [ @@ -58,7 +58,6 @@ setup( 'Intended Audience :: Developers', 'Environment :: Web Environment', 'Framework :: Django', - 'Framework :: Django :: 1.10', 'Framework :: Django :: 1.11', 'Framework :: Django :: 2.0', diff --git a/tox.ini b/tox.ini index ac72378..7ffc655 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,5 @@ [tox] envlist = - py{27,34,35}-django110 py{27,34,35,36}-django111 py{34,35,36}-django20 lint @@ -10,7 +9,6 @@ commands = py.test {posargs} extras = test pip_pre = true deps = - django110: django>=1.10,<1.11 django111: django>=1.11,<2.0 django20: django>=2.0 From 6174c76c361a85f9ee120370c3d13d83967b62a0 Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Thu, 19 Jul 2018 22:14:25 +0200 Subject: [PATCH 07/46] Bump to version 0.0.6 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 01b7a49..29b33b1 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ test_requirements = [ setup( name='django-migration-linter', - version='0.0.5', + version='0.0.6', description='Detect backward incompatible migrations for your django project', long_description=long_description, From c26bd2b62809b6253c16af162e339d0284582a6a Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Thu, 6 Sep 2018 21:47:28 +0200 Subject: [PATCH 08/46] Provide a default value to next() function in case of empty errors array --- django_migration_linter/sql_analyser.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/django_migration_linter/sql_analyser.py b/django_migration_linter/sql_analyser.py index aa06ec2..0fe3c1b 100644 --- a/django_migration_linter/sql_analyser.py +++ b/django_migration_linter/sql_analyser.py @@ -20,13 +20,15 @@ logger = logging.getLogger(__name__) 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') + err = next( + (err for err in kwargs['errors'] if err['code'] == 'NOT_NULL'), + None, + ) if err: logger.info( ('Found a NOT_NULL error in migration, ' - 'but it has a default value added: {}').format(err)) + 'but it has a default value added: {}').format(err) + ) kwargs['errors'].remove(err) return False # Never fails From 178d8036c2618f78e2ddceff1768c2ac297e879a Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Thu, 6 Sep 2018 21:48:52 +0200 Subject: [PATCH 09/46] Bump to version 0.0.7 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 29b33b1..79ca34d 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ test_requirements = [ setup( name='django-migration-linter', - version='0.0.6', + version='0.0.7', description='Detect backward incompatible migrations for your django project', long_description=long_description, From b3815b6b98df95f68b06f401417ac1be464e1f01 Mon Sep 17 00:00:00 2001 From: Felix Date: Tue, 18 Dec 2018 15:33:26 +0100 Subject: [PATCH 10/46] #23 Add tests for caching --- django_migration_linter/constants.py | 21 +++++++ django_migration_linter/utils.py | 6 ++ setup.py | 8 ++- tests/functional/test_cache.py | 85 ++++++++++++++++++++++++++ tests/functional/test_cmd_line_call.py | 45 ++++++++++++++ tox.ini | 1 + 6 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 django_migration_linter/constants.py create mode 100644 tests/functional/test_cache.py diff --git a/django_migration_linter/constants.py b/django_migration_linter/constants.py new file mode 100644 index 0000000..3086825 --- /dev/null +++ b/django_migration_linter/constants.py @@ -0,0 +1,21 @@ +# Copyright 2018 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 pathlib import Path + +from appdirs import user_cache_dir + +__VERSION__ = '0.0.7' + +DEFAULT_CACHE_PATH = Path(user_cache_dir("django-migration-linter", version=__VERSION__)) diff --git a/django_migration_linter/utils.py b/django_migration_linter/utils.py index b2b8f23..4575a4f 100644 --- a/django_migration_linter/utils.py +++ b/django_migration_linter/utils.py @@ -16,6 +16,8 @@ from __future__ import print_function import os import sys +from django_migration_linter.constants import DEFAULT_CACHE_PATH + def is_django_project(path): django_manage_file = os.path.join(path, 'manage.py') @@ -54,3 +56,7 @@ def split_path(path): def clean_bytes_to_str(byte_input): return byte_input.decode('utf-8').strip() + + +def get_default_cache_file(project_name): + os.join(DEFAULT_CACHE_PATH, '{0}.pickle'.format(project_name)) diff --git a/setup.py b/setup.py index 79ca34d..c1f728c 100644 --- a/setup.py +++ b/setup.py @@ -18,11 +18,16 @@ from setuptools import setup, find_packages PROJECT_DIR = path.abspath(path.dirname(__file__)) +# Read __VERSION__ +exec(open('./django_migration_linter/constants.py').read()) + + with open(path.join(PROJECT_DIR, 'README.rst')) as f: long_description = f.read() install_requirements = [ 'django>=1.11', + 'appdirs==1.4.3' ] test_requirements = [ @@ -31,9 +36,10 @@ test_requirements = [ 'django-fake-database-backends', ] +# noinspection PyUnresolvedReferences setup( name='django-migration-linter', - version='0.0.7', + version=__VERSION__, description='Detect backward incompatible migrations for your django project', long_description=long_description, diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py new file mode 100644 index 0000000..85a94ef --- /dev/null +++ b/tests/functional/test_cache.py @@ -0,0 +1,85 @@ +# Copyright 2018 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 os +import shutil +import tempfile +import unittest +from unittest import mock + +from django_migration_linter.utils import get_default_cache_file +from tests import fixtures + +from django_migration_linter import MigrationLinter +from tests.fixtures import ADD_NOT_NULL_COLUMN_PROJECT + + +class CacheTest(unittest.TestCase): + MIGRATION_FILE = os.path.join(ADD_NOT_NULL_COLUMN_PROJECT, 'test_app', 'migrations', '0001_create_table.py') + fd, temp_path = tempfile.mkstemp() + + def setUp(self): + shutil.copy2(self.MIGRATION_FILE, self.temp_path) + + def tearDown(self): + shutil.copy2(self.temp_path, self.MIGRATION_FILE) + os.remove(self.temp_path) + + def test_cache_normal(self): + if os.path.exists(get_default_cache_file('test_project_add_not_null_column')): + os.remove(get_default_cache_file('test_project_add_not_null_column')) + linter = MigrationLinter(fixtures.ADD_NOT_NULL_COLUMN_PROJECT) + linter.lint_all_migrations() + cache = linter.get_cache() + self.assertEqual(cache['hash']['result'], 'OK') + self.assertEqual(cache['hashERR']['result'], 'ERR') + self.assertDictEqual(cache['hashERR']['errors'], ...) + + with mock.patch( + 'subprocess.Popen' + ) as popen_mock: + linter.lint_all_migrations() + self.assertEqual(popen_mock.call_count, 2) + + with mock.patch( + 'subprocess.Popen' + ) as popen_mock: + linter.lint_all_migrations() + + self.assertEqual(popen_mock.call_count, 0) + self.assertFalse(linter.has_errors()) + + def test_cache_ignored(self): + pass + + def test_cache_modified(self): + if os.path.exists(get_default_cache_file('test_project_add_not_null_column')): + os.remove(get_default_cache_file('test_project_add_not_null_column')) + linter = MigrationLinter(fixtures.ADD_NOT_NULL_COLUMN_PROJECT) + linter.lint_all_migrations() + cache = linter.get_cache() + self.assertEqual(cache['hash']['result'], 'OK') + self.assertEqual(cache['hashERR']['result'], 'ERR') + self.assertDictEqual(cache['hashERR']['errors'], ...) + + with open( self.MIGRATION_FILE, "a") as f: + f.write("# modification at the end of the file\n") + + linter.lint_all_migrations() + cache = linter.get_cache() + + self.assertNotIn('hash', cache) + self.assertEqual(cache['newhash']['result'], 'OK') + + + diff --git a/tests/functional/test_cmd_line_call.py b/tests/functional/test_cmd_line_call.py index a3afd33..4f1c4b5 100644 --- a/tests/functional/test_cmd_line_call.py +++ b/tests/functional/test_cmd_line_call.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os import unittest from subprocess import Popen, PIPE from django_migration_linter import utils +from django_migration_linter.utils import get_default_cache_file from tests import fixtures import sys @@ -115,6 +117,49 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertTrue(lines[1].endswith('OK')) self.assertTrue(lines[2].endswith('IGNORE')) + def test_call_linter_cmd_line_cache(self): + if os.path.exists(get_default_cache_file('test_correct_project')): + os.remove(get_default_cache_file('test_correct_project')) + cmd = '{0} {1}'.format( + self.linter_exec, + fixtures.CORRECT_PROJECT + ) + + process = Popen( + cmd, shell=True, stdout=PIPE, stderr=PIPE) + process.wait() + self.assertTrue(os.path.exists(get_default_cache_file('test_correct_project'))) + + def test_call_linter_cmd_line_cache_path(self): + if os.path.exists('/tmp/migration-linter-cache-tests'): + os.remove('/tmp/migration-linter-cache-tests') + cmd = '{0} {1} --cache-path={2}'.format( + self.linter_exec, + fixtures.CORRECT_PROJECT, + '/tmp/migration-linter-cache-tests' + ) + + process = Popen( + cmd, shell=True, stdout=PIPE, stderr=PIPE) + process.wait() + self.assertEqual(process.returncode, 0) + self.assertTrue(os.path.exists('/tmp/migration-linter-cache-tests')) + + def test_call_linter_cmd_line_no_cache(self): + if os.path.exists(get_default_cache_file('test_correct_project')): + os.remove(get_default_cache_file('test_correct_project')) + + cmd = '{0} {1} --no-cache'.format( + self.linter_exec, + fixtures.CORRECT_PROJECT + ) + + process = Popen( + cmd, shell=True, stdout=PIPE, stderr=PIPE) + process.wait() + self.assertEqual(process.returncode, 0) + self.assertFalse(os.path.exists(get_default_cache_file('test_correct_project'))) + def test_call_linter_cmd_line_git_id(self): cmd = '{0} {1} d7125d5f4f0cc9623f670a66c54f131acc50032d'.format( self.linter_exec, diff --git a/tox.ini b/tox.ini index 7ffc655..a1e4ccc 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,7 @@ pip_pre = true deps = django111: django>=1.11,<2.0 django20: django>=2.0 + appdirs==1.4.3 [testenv:lint] basepython = python3.6 From 9608e991500f8daea3565e982e0788bcdbb81ade Mon Sep 17 00:00:00 2001 From: Felix Date: Tue, 18 Dec 2018 15:54:47 +0100 Subject: [PATCH 11/46] #23 Fix errors and flake8 --- django_migration_linter/constants.py | 4 +++- django_migration_linter/utils.py | 2 +- setup.py | 16 ++++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/django_migration_linter/constants.py b/django_migration_linter/constants.py index 3086825..a667d9a 100644 --- a/django_migration_linter/constants.py +++ b/django_migration_linter/constants.py @@ -18,4 +18,6 @@ from appdirs import user_cache_dir __VERSION__ = '0.0.7' -DEFAULT_CACHE_PATH = Path(user_cache_dir("django-migration-linter", version=__VERSION__)) +DEFAULT_CACHE_PATH = Path( + user_cache_dir("django-migration-linter", version=__VERSION__) +) diff --git a/django_migration_linter/utils.py b/django_migration_linter/utils.py index 4575a4f..0f83ad5 100644 --- a/django_migration_linter/utils.py +++ b/django_migration_linter/utils.py @@ -59,4 +59,4 @@ def clean_bytes_to_str(byte_input): def get_default_cache_file(project_name): - os.join(DEFAULT_CACHE_PATH, '{0}.pickle'.format(project_name)) + return os.path.join(DEFAULT_CACHE_PATH, '{0}.pickle'.format(project_name)) diff --git a/setup.py b/setup.py index c1f728c..7337984 100644 --- a/setup.py +++ b/setup.py @@ -11,15 +11,23 @@ # 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 ast +import re from os import path from setuptools import setup, find_packages PROJECT_DIR = path.abspath(path.dirname(__file__)) -# Read __VERSION__ -exec(open('./django_migration_linter/constants.py').read()) + +def get_version(): + constants = path.join(PROJECT_DIR, 'django_migration_linter', 'constants.py') + _version_re = re.compile(r"__VERSION__\s+=\s+(?P.*)") + with open(constants, "r") as f: + match = _version_re.search(f.read()) + version = match.group("version") if match is not None else '"unknown"' + return str(ast.literal_eval(version)) + with open(path.join(PROJECT_DIR, 'README.rst')) as f: @@ -39,7 +47,7 @@ test_requirements = [ # noinspection PyUnresolvedReferences setup( name='django-migration-linter', - version=__VERSION__, + version=get_version(), description='Detect backward incompatible migrations for your django project', long_description=long_description, From b68c0183bb6262fad6d37503dd11578281739125 Mon Sep 17 00:00:00 2001 From: Felix Date: Thu, 20 Dec 2018 14:45:35 +0100 Subject: [PATCH 12/46] Finish up cache and fix tests. Still TODO: Merge with ignore functionality. --- django_migration_linter/cache.py | 53 ++++++++++ django_migration_linter/constants.py | 10 +- django_migration_linter/migration_linter.py | 75 +++++++++++--- django_migration_linter/utils.py | 23 ++++- setup.py | 3 +- tests/functional/test_cache.py | 106 +++++++++++++------- tests/functional/test_cmd_line_call.py | 45 +++++---- tests/functional/test_multiple_calls.py | 8 +- tests/unit/test_linter.py | 24 +---- tests/unit/test_utils.py | 21 +++- tox.ini | 1 + 11 files changed, 263 insertions(+), 106 deletions(-) create mode 100644 django_migration_linter/cache.py diff --git a/django_migration_linter/cache.py b/django_migration_linter/cache.py new file mode 100644 index 0000000..0d3f524 --- /dev/null +++ b/django_migration_linter/cache.py @@ -0,0 +1,53 @@ +# Copyright 2018 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 hashlib +import os +import pickle + +from django_migration_linter.utils import split_path, compose_migration_path + + +class Cache(dict): + def __init__(self, django_folder, cache_path): + self.django_folder = django_folder + project_name = split_path(django_folder)[-2] + self.filename = os.path.join(cache_path, '{0}.pickle'.format(project_name)) + + if not os.path.exists(os.path.dirname(self.filename)): + os.makedirs(os.path.dirname(self.filename)) + + super(Cache, self).__init__() + + def load(self): + try: + f = open(self.filename, 'rb') + tmp_dict = pickle.load(f) + self.update(tmp_dict) + f.close() + except IOError: + pass + + def save(self): + f = open(self.filename, 'wb') + pickle.dump(self, f, 2) + f.close() + + def md5(self, app_name, migration): + path = compose_migration_path(self.django_folder, app_name, migration) + hash_md5 = hashlib.md5() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() diff --git a/django_migration_linter/constants.py b/django_migration_linter/constants.py index a667d9a..1bdfc9b 100644 --- a/django_migration_linter/constants.py +++ b/django_migration_linter/constants.py @@ -12,12 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pathlib import Path - from appdirs import user_cache_dir -__VERSION__ = '0.0.7' +__version__ = '0.0.7' -DEFAULT_CACHE_PATH = Path( - user_cache_dir("django-migration-linter", version=__VERSION__) +MIGRATION_FOLDER_NAME = 'migrations' + +DEFAULT_CACHE_PATH = user_cache_dir( + "django-migration-linter", version=__version__ ) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index b2cd6aa..66fa34c 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -18,6 +18,10 @@ import os import re from subprocess import Popen, PIPE import sys + +from django_migration_linter.cache import Cache +from django_migration_linter.constants import DEFAULT_CACHE_PATH, MIGRATION_FOLDER_NAME +from django_migration_linter.utils import split_migration_path from . import utils from .sql_analyser import analyse_sql_statements @@ -25,7 +29,6 @@ logger = logging.getLogger(__name__) class MigrationLinter(object): - MIGRATION_FOLDER_NAME = 'migrations' def __init__(self, project_path, **kwargs): # Verify correctness @@ -45,6 +48,9 @@ class MigrationLinter(object): 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.cache_path = kwargs.get('cache_path', None) or DEFAULT_CACHE_PATH + self.no_cache = kwargs.get('no_cache', None) or False + self.python_exe = '{0}/bin/{1}'.format(sys.prefix, 'python') if \ hasattr(sys, 'real_prefix') else 'python' @@ -54,12 +60,37 @@ class MigrationLinter(object): self.nb_erroneous = 0 self.nb_total = 0 + # Initialise cache. Read from old, write to new to prune old entries. + self.old_cache = Cache(self.django_path, self.cache_path) + self.new_cache = Cache(self.django_path, self.cache_path) + if not self.no_cache: + self.old_cache.load() + def lint_migration(self, app_name, migration_name): print('({0}, {1})... '.format(app_name, migration_name), end='') self.nb_total += 1 + md5hash = self.old_cache.md5(app_name, migration_name) + if md5hash in self.old_cache: + if self.old_cache[md5hash]['result'] == 'IGNORE': + print('IGNORE (cached)') + self.nb_ignored += 1 + elif self.old_cache[md5hash]['result'] == 'OK': + print('OK (cached)') + self.nb_valid += 1 + else: + print('ERR (cached)') + self.nb_erroneous += 1 + + if 'errors' in self.old_cache[md5hash]: + self.print_errors(self.old_cache[md5hash]['errors']) + + self.new_cache[md5hash] = self.old_cache[md5hash] + return + if self.should_ignore_migration(app_name, migration_name): print('IGNORE') + self.new_cache[md5hash] = {'result': 'IGNORE'} self.nb_ignored += 1 return @@ -70,12 +101,17 @@ class MigrationLinter(object): errors = analysis_result['errors'] if not errors: print('OK') + self.new_cache[md5hash] = {'result': 'OK'} self.nb_valid += 1 return - # Print errors print('ERR') + self.new_cache[md5hash] = {'result': 'ERR', 'errors': errors} self.nb_erroneous += 1 + self.print_errors(errors) + + @staticmethod + def print_errors(errors): for err in errors: error_str = '\t{0}'.format(err['err_msg']) if err['table']: @@ -101,6 +137,9 @@ class MigrationLinter(object): for m in migrations: self.lint_migration(*m) + if not self.no_cache: + self.new_cache.save() + def print_summary(self): print('*** Summary:') print(('Valid migrations: {1}/{0} - ' @@ -166,10 +205,10 @@ class MigrationLinter(object): utils.clean_bytes_to_str, diff_process.stdout.readlines()): # Only gather lines that include added migrations if re.search( - '\/{0}\/.*\.py'.format(self.MIGRATION_FOLDER_NAME), + '\/{0}\/.*\.py'.format(MIGRATION_FOLDER_NAME), line) and \ '__init__' not in line: - app_name, migration_name = self._split_migration_path(line) + app_name, migration_name = split_migration_path(line) migrations.append((app_name, migration_name)) diff_process.wait() @@ -187,11 +226,11 @@ class MigrationLinter(object): 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 \ + if os.path.basename(root) == 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( + app_name, migration_name = split_migration_path( full_migration_path) migrations.append((app_name, migration_name)) return migrations @@ -205,14 +244,6 @@ class MigrationLinter(object): self.ignore_name_contains in migration_name)\ or (migration_name in self.ignore_name) - @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 @@ -253,6 +284,17 @@ def _main(): help=('specify the database for which to generate the SQL. ' 'Defaults to default')) + cache_group = parser.add_mutually_exclusive_group( + required=False) + cache_group.add_argument( + '--cache-path', + type=str, + help='specify a directory that should be used to store cache-files in.') + cache_group.add_argument( + '--no-cache', + action='store_true', + help='don\'t use a cache') + incl_excl_group = parser.add_mutually_exclusive_group( required=False) incl_excl_group.add_argument( @@ -280,7 +322,10 @@ def _main(): ignore_name=args.ignore_name, include_apps=args.include_apps, exclude_apps=args.exclude_apps, - database=args.database) + database=args.database, + cache_path=args.cache_path, + no_cache=args.no_cache + ) linter.lint_all_migrations( git_commit_id=args.commit_id) linter.print_summary() diff --git a/django_migration_linter/utils.py b/django_migration_linter/utils.py index 0f83ad5..8e26011 100644 --- a/django_migration_linter/utils.py +++ b/django_migration_linter/utils.py @@ -13,10 +13,11 @@ # limitations under the License. from __future__ import print_function + import os import sys -from django_migration_linter.constants import DEFAULT_CACHE_PATH +from django_migration_linter.constants import MIGRATION_FOLDER_NAME def is_django_project(path): @@ -54,9 +55,25 @@ def split_path(path): return (split_path(a) if (len(a) > 0 and a != '/') else []) + [b] +def split_migration_path(migration_path): + decomposed_path = split_path(migration_path) + for i, p in enumerate(decomposed_path): + if p == MIGRATION_FOLDER_NAME: + return (decomposed_path[i-1], + os.path.splitext(decomposed_path[i+1])[0]) + + +def compose_migration_path(django_folder, app_name, migration): + return os.path.join( + django_folder, + app_name, + MIGRATION_FOLDER_NAME, + '{0}.py'.format(migration) + ) + + def clean_bytes_to_str(byte_input): return byte_input.decode('utf-8').strip() -def get_default_cache_file(project_name): - return os.path.join(DEFAULT_CACHE_PATH, '{0}.pickle'.format(project_name)) + diff --git a/setup.py b/setup.py index 7337984..dd6bee3 100644 --- a/setup.py +++ b/setup.py @@ -22,14 +22,13 @@ PROJECT_DIR = path.abspath(path.dirname(__file__)) def get_version(): constants = path.join(PROJECT_DIR, 'django_migration_linter', 'constants.py') - _version_re = re.compile(r"__VERSION__\s+=\s+(?P.*)") + _version_re = re.compile(r"__version__\s+=\s+(?P.*)") with open(constants, "r") as f: match = _version_re.search(f.read()) version = match.group("version") if match is not None else '"unknown"' return str(ast.literal_eval(version)) - with open(path.join(PROJECT_DIR, 'README.rst')) as f: long_description = f.read() diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 85a94ef..4ea631f 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -12,20 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. import os +import pprint import shutil import tempfile import unittest -from unittest import mock +import sys -from django_migration_linter.utils import get_default_cache_file from tests import fixtures -from django_migration_linter import MigrationLinter -from tests.fixtures import ADD_NOT_NULL_COLUMN_PROJECT +from django_migration_linter import MigrationLinter, Cache, DEFAULT_CACHE_PATH + +if sys.version_info >= (3, 3): + import unittest.mock as mock +else: + import mock + class CacheTest(unittest.TestCase): - MIGRATION_FILE = os.path.join(ADD_NOT_NULL_COLUMN_PROJECT, 'test_app', 'migrations', '0001_create_table.py') + MIGRATION_FILE = os.path.join(fixtures.ALTER_COLUMN_PROJECT, 'test_app', 'migrations', '0001_initial.py') fd, temp_path = tempfile.mkstemp() def setUp(self): @@ -36,50 +41,83 @@ class CacheTest(unittest.TestCase): os.remove(self.temp_path) def test_cache_normal(self): - if os.path.exists(get_default_cache_file('test_project_add_not_null_column')): - os.remove(get_default_cache_file('test_project_add_not_null_column')) + cache_file = os.path.join(DEFAULT_CACHE_PATH, 'test_project_add_not_null_column.pickle') + if os.path.exists(cache_file): + os.remove(cache_file) linter = MigrationLinter(fixtures.ADD_NOT_NULL_COLUMN_PROJECT) - linter.lint_all_migrations() - cache = linter.get_cache() - self.assertEqual(cache['hash']['result'], 'OK') - self.assertEqual(cache['hashERR']['result'], 'ERR') - self.assertDictEqual(cache['hashERR']['errors'], ...) - with mock.patch( - 'subprocess.Popen' - ) as popen_mock: + with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql)as sql_mock: linter.lint_all_migrations() - self.assertEqual(popen_mock.call_count, 2) + sql_mock.assert_called() - with mock.patch( - 'subprocess.Popen' - ) as popen_mock: + cache = Cache( + fixtures.ADD_NOT_NULL_COLUMN_PROJECT, + DEFAULT_CACHE_PATH + ) + cache.load() + + self.assertEqual(cache['3ef74e7f3e53e273e2fc95379248d58d']['result'], 'OK') + self.assertEqual(cache['e1e312b6d08ecbe017c25c58fc2be257']['result'], 'ERR') + self.assertListEqual( + cache['e1e312b6d08ecbe017c25c58fc2be257']['errors'], + [{'err_msg': 'RENAMING tables', 'code': 'RENAME_TABLE', 'table': None, 'column': None}] + ) + + # Start the Linter again -> should use cache now. + linter = MigrationLinter(fixtures.ADD_NOT_NULL_COLUMN_PROJECT) + + with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql)as sql_mock: linter.lint_all_migrations() + sql_mock.assert_not_called() - self.assertEqual(popen_mock.call_count, 0) - self.assertFalse(linter.has_errors()) + self.assertTrue(linter.has_errors) def test_cache_ignored(self): pass def test_cache_modified(self): - if os.path.exists(get_default_cache_file('test_project_add_not_null_column')): - os.remove(get_default_cache_file('test_project_add_not_null_column')) - linter = MigrationLinter(fixtures.ADD_NOT_NULL_COLUMN_PROJECT) + cache_file = os.path.join(DEFAULT_CACHE_PATH, 'test_project_alter_column.pickle') + if os.path.exists(cache_file): + os.remove(cache_file) + linter = MigrationLinter(fixtures.ALTER_COLUMN_PROJECT) linter.lint_all_migrations() - cache = linter.get_cache() - self.assertEqual(cache['hash']['result'], 'OK') - self.assertEqual(cache['hashERR']['result'], 'ERR') - self.assertDictEqual(cache['hashERR']['errors'], ...) + cache = Cache( + fixtures.ALTER_COLUMN_PROJECT, + DEFAULT_CACHE_PATH + ) + cache.load() - with open( self.MIGRATION_FILE, "a") as f: + self.assertEqual(cache['8589aa107b6da296c4b49cd2681d2230']['result'], 'OK', + 'If this fails, tearDown might have failed to remove ' + 'the modification from tests/test_project_fixtures/' + 'test_project_alter_column/test_app/migrations/0001_initial.py' + ) + self.assertEqual(cache['8f54c4a434cfaa9838e8ca12eb988255']['result'], 'ERR') + self.assertListEqual( + cache['8f54c4a434cfaa9838e8ca12eb988255']['errors'], + [{u'err_msg': u'ALTERING columns (Could be backward compatible. ' + u'You may ignore this migration.)', + u'code': u'ALTER_COLUMN', + u'table': u'test_app_a', + u'column': None} + ] + ) + + # Modify migration + with open(self.MIGRATION_FILE, "a") as f: f.write("# modification at the end of the file\n") + # Start the Linter again -> Cache should look different now + linter = MigrationLinter(fixtures.ALTER_COLUMN_PROJECT) linter.lint_all_migrations() - cache = linter.get_cache() - - self.assertNotIn('hash', cache) - self.assertEqual(cache['newhash']['result'], 'OK') - + cache = Cache( + fixtures.ALTER_COLUMN_PROJECT, + DEFAULT_CACHE_PATH + ) + cache.load() + pp = pprint.PrettyPrinter(indent=4) + pp.pprint(cache) + self.assertNotIn('8589aa107b6da296c4b49cd2681d2230', cache) + self.assertEqual(cache['44f9d6d019d6f158946c2819b2bd8bf9']['result'], 'OK') diff --git a/tests/functional/test_cmd_line_call.py b/tests/functional/test_cmd_line_call.py index 4f1c4b5..e651ea2 100644 --- a/tests/functional/test_cmd_line_call.py +++ b/tests/functional/test_cmd_line_call.py @@ -13,10 +13,10 @@ # limitations under the License. import os +import shutil import unittest from subprocess import Popen, PIPE -from django_migration_linter import utils -from django_migration_linter.utils import get_default_cache_file +from django_migration_linter import utils, DEFAULT_CACHE_PATH from tests import fixtures import sys @@ -29,7 +29,7 @@ class CallLinterFromCommandLineTest(unittest.TestCase): cls.linter_exec = '{0}/bin/{1}'.format(sys.prefix, linter_name) if hasattr(sys, 'real_prefix') else linter_name def test_call_linter_cmd_line_working(self): - cmd = '{0} {1}'.format(self.linter_exec, fixtures.CORRECT_PROJECT) + cmd = '{0} --no-cache {1}'.format(self.linter_exec, fixtures.CORRECT_PROJECT) process = Popen( cmd, shell=True, stdout=PIPE, stderr=PIPE) @@ -42,7 +42,7 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertTrue(lines[2].endswith('OK')) def test_call_linter_cmd_line_errors(self): - cmd = '{0} {1}'.format( + cmd = '{0} --no-cache {1}'.format( self.linter_exec, fixtures.ADD_NOT_NULL_COLUMN_PROJECT) @@ -58,12 +58,12 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertTrue('RENAMING tables' in lines[2]) def test_call_linter_cmd_line_exclude_apps(self): - cmd = '{0} {1} --exclude-apps test_app2'.format( + cmd = '{0} --no-cache {1} --exclude-apps test_app2'.format( self.linter_exec, fixtures.CORRECT_PROJECT) process = Popen( - cmd, shell=True, stdout=PIPE, stderr=PIPE) + cmd, shell=True, stdout=PIPE, stderr=sys.stderr) process.wait() self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) @@ -73,7 +73,7 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertTrue(lines[2].endswith('IGNORE')) def test_call_linter_cmd_line_include_apps(self): - cmd = '{0} {1} --include-apps test_app2'.format( + cmd = '{0} --no-cache {1} --include-apps test_app2'.format( self.linter_exec, fixtures.CORRECT_PROJECT) @@ -88,7 +88,7 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertTrue(lines[2].endswith('OK')) def test_call_linter_cmd_line_ignore_name(self): - cmd = '{0} {1} --ignore-name 0001_initial'.format( + cmd = '{0} --no-cache {1} --ignore-name 0001_initial'.format( self.linter_exec, fixtures.CORRECT_PROJECT) @@ -103,7 +103,7 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertTrue(lines[2].endswith('OK')) def test_call_linter_cmd_line_ignore_name_contains(self): - cmd = '{0} {1} --ignore-name-contains 0001'.format( + cmd = '{0} --no-cache {1} --ignore-name-contains 0001'.format( self.linter_exec, fixtures.CORRECT_PROJECT) @@ -118,8 +118,9 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertTrue(lines[2].endswith('IGNORE')) def test_call_linter_cmd_line_cache(self): - if os.path.exists(get_default_cache_file('test_correct_project')): - os.remove(get_default_cache_file('test_correct_project')) + cache_file = os.path.join(DEFAULT_CACHE_PATH, 'test_correct_project.pickle') + if os.path.exists(cache_file): + os.remove(cache_file) cmd = '{0} {1}'.format( self.linter_exec, fixtures.CORRECT_PROJECT @@ -128,15 +129,15 @@ class CallLinterFromCommandLineTest(unittest.TestCase): process = Popen( cmd, shell=True, stdout=PIPE, stderr=PIPE) process.wait() - self.assertTrue(os.path.exists(get_default_cache_file('test_correct_project'))) + self.assertTrue(os.path.exists(DEFAULT_CACHE_PATH)) def test_call_linter_cmd_line_cache_path(self): - if os.path.exists('/tmp/migration-linter-cache-tests'): - os.remove('/tmp/migration-linter-cache-tests') + if os.path.exists('/tmp/migration-linter-cache-tests/'): + shutil.rmtree('/tmp/migration-linter-cache-tests/') cmd = '{0} {1} --cache-path={2}'.format( self.linter_exec, fixtures.CORRECT_PROJECT, - '/tmp/migration-linter-cache-tests' + '/tmp/migration-linter-cache-tests/' ) process = Popen( @@ -146,10 +147,12 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertTrue(os.path.exists('/tmp/migration-linter-cache-tests')) def test_call_linter_cmd_line_no_cache(self): - if os.path.exists(get_default_cache_file('test_correct_project')): - os.remove(get_default_cache_file('test_correct_project')) + cache_file = os.path.join(DEFAULT_CACHE_PATH, 'test_correct_project.pickle') + print(cache_file) + if os.path.exists(cache_file): + os.remove(cache_file) - cmd = '{0} {1} --no-cache'.format( + cmd = '{0} --no-cache {1}'.format( self.linter_exec, fixtures.CORRECT_PROJECT ) @@ -158,10 +161,10 @@ class CallLinterFromCommandLineTest(unittest.TestCase): cmd, shell=True, stdout=PIPE, stderr=PIPE) process.wait() self.assertEqual(process.returncode, 0) - self.assertFalse(os.path.exists(get_default_cache_file('test_correct_project'))) + self.assertFalse(os.path.exists(cache_file)) def test_call_linter_cmd_line_git_id(self): - cmd = '{0} {1} d7125d5f4f0cc9623f670a66c54f131acc50032d'.format( + cmd = '{0} --no-cache {1} d7125d5f4f0cc9623f670a66c54f131acc50032d'.format( self.linter_exec, fixtures.MULTI_COMMIT_PROJECT) fixtures.prepare_git_project(fixtures.MULTI_COMMIT_PROJECT) @@ -176,7 +179,7 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertTrue(lines[1].startswith('*** Summary')) def test_call_linter_with_deleted_migrations(self): - cmd = '{0} {1} 154ecf6119325cc3b1f3f5a4e709bfbd61a4a4ba'.format( + cmd = '{0} --no-cache {1} 154ecf6119325cc3b1f3f5a4e709bfbd61a4a4ba'.format( self.linter_exec, fixtures.DELETED_MIGRATION_PROJECT) fixtures.prepare_git_project(fixtures.DELETED_MIGRATION_PROJECT) diff --git a/tests/functional/test_multiple_calls.py b/tests/functional/test_multiple_calls.py index bea9262..0920c1b 100644 --- a/tests/functional/test_multiple_calls.py +++ b/tests/functional/test_multiple_calls.py @@ -24,10 +24,10 @@ class TestMultipleLinters(unittest.TestCase): 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') + l1.lint_migration('test_app', '0002_add_new_not_null_field') + l2.lint_migration('test_app', '0002_rename_column') + l3.lint_migration('test_app1', '0001_initial') + l3.lint_migration('test_app1', '0002_a_new_null_field') self.assertTrue(l1.has_errors) self.assertTrue(l2.has_errors) diff --git a/tests/unit/test_linter.py b/tests/unit/test_linter.py index 0a4e47e..37fc754 100644 --- a/tests/unit/test_linter.py +++ b/tests/unit/test_linter.py @@ -32,13 +32,13 @@ class TestLinterFunctions(unittest.TestCase): linter = MigrationLinter(project_path) self.assertFalse(linter.has_errors) - linter.lint_migration('test_app', '0001') + linter.lint_migration('test_app', '0001_create_table') self.assertFalse(linter.has_errors) - linter.lint_migration('test_app', '0002') + linter.lint_migration('test_app', '0002_add_new_not_null_field') self.assertTrue(linter.has_errors) - linter.lint_migration('test_app', '0001') + linter.lint_migration('test_app', '0001_create_table') self.assertTrue(linter.has_errors) def test_linter_creation(self): @@ -49,24 +49,6 @@ class TestLinterFunctions(unittest.TestCase): with self.assertRaises(ValueError): MigrationLinter(fixtures.NOT_DJANGO_GIT_PROJECT) - def test_split_migration_long_path(self): - input_path = 'apps/the_app/migrations/0001_stuff.py' - app, mig = MigrationLinter._split_migration_path(input_path) - self.assertEqual(app, 'the_app') - self.assertEqual(mig, '0001_stuff') - - def test_split_migration_path(self): - input_path = 'the_app/migrations/0001_stuff.py' - app, mig = MigrationLinter._split_migration_path(input_path) - self.assertEqual(app, 'the_app') - self.assertEqual(mig, '0001_stuff') - - def test_split_migration_full_path(self): - input_path = '/home/user/djangostuff/apps/the_app/migrations/0001_stuff.py' - app, mig = MigrationLinter._split_migration_path(input_path) - self.assertEqual(app, 'the_app') - self.assertEqual(mig, '0001_stuff') - def test_ignore_migration_include_apps(self): linter = MigrationLinter( fixtures.ADD_NOT_NULL_COLUMN_PROJECT, diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 88d20af..7ac89d5 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -16,7 +16,8 @@ import os import unittest from tests import fixtures -from django_migration_linter.utils import is_django_project, is_git_project, is_directory, find_project_settings_module, split_path +from django_migration_linter.utils import is_django_project, is_git_project, is_directory, find_project_settings_module, \ + split_path, split_migration_path class UtilityFunctionTest(unittest.TestCase): @@ -70,3 +71,21 @@ class UtilityFunctionTest(unittest.TestCase): self.assertEqual(splitted[0], 'foo') self.assertEqual(splitted[1], 'bar') self.assertEqual(splitted[2], 'fuz.py') + + def test_split_migration_long_path(self): + input_path = 'apps/the_app/migrations/0001_stuff.py' + app, mig = split_migration_path(input_path) + self.assertEqual(app, 'the_app') + self.assertEqual(mig, '0001_stuff') + + def test_split_migration_path(self): + input_path = 'the_app/migrations/0001_stuff.py' + app, mig = split_migration_path(input_path) + self.assertEqual(app, 'the_app') + self.assertEqual(mig, '0001_stuff') + + def test_split_migration_full_path(self): + input_path = '/home/user/djangostuff/apps/the_app/migrations/0001_stuff.py' + app, mig = split_migration_path(input_path) + self.assertEqual(app, 'the_app') + self.assertEqual(mig, '0001_stuff') diff --git a/tox.ini b/tox.ini index a1e4ccc..a420feb 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,7 @@ deps = django111: django>=1.11,<2.0 django20: django>=2.0 appdirs==1.4.3 + py27: mock==2.0.0 [testenv:lint] basepython = python3.6 From f351b71d6a6924e38b812794e753f50f48212c5b Mon Sep 17 00:00:00 2001 From: Felix Date: Thu, 20 Dec 2018 14:53:25 +0100 Subject: [PATCH 13/46] Fix flake8 --- django_migration_linter/cache.py | 3 ++- django_migration_linter/migration_linter.py | 6 ++++-- django_migration_linter/utils.py | 3 --- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/django_migration_linter/cache.py b/django_migration_linter/cache.py index 0d3f524..46e3a63 100644 --- a/django_migration_linter/cache.py +++ b/django_migration_linter/cache.py @@ -23,7 +23,8 @@ class Cache(dict): def __init__(self, django_folder, cache_path): self.django_folder = django_folder project_name = split_path(django_folder)[-2] - self.filename = os.path.join(cache_path, '{0}.pickle'.format(project_name)) + self.filename = \ + os.path.join(cache_path, '{0}.pickle'.format(project_name)) if not os.path.exists(os.path.dirname(self.filename)): os.makedirs(os.path.dirname(self.filename)) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index 66fa34c..a5ac985 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -20,7 +20,8 @@ from subprocess import Popen, PIPE import sys from django_migration_linter.cache import Cache -from django_migration_linter.constants import DEFAULT_CACHE_PATH, MIGRATION_FOLDER_NAME +from django_migration_linter.constants import DEFAULT_CACHE_PATH, \ + MIGRATION_FOLDER_NAME from django_migration_linter.utils import split_migration_path from . import utils from .sql_analyser import analyse_sql_statements @@ -289,7 +290,8 @@ def _main(): cache_group.add_argument( '--cache-path', type=str, - help='specify a directory that should be used to store cache-files in.') + help='specify a directory that should be used to' + 'store cache-files in.') cache_group.add_argument( '--no-cache', action='store_true', diff --git a/django_migration_linter/utils.py b/django_migration_linter/utils.py index 8e26011..355df5f 100644 --- a/django_migration_linter/utils.py +++ b/django_migration_linter/utils.py @@ -74,6 +74,3 @@ def compose_migration_path(django_folder, app_name, migration): def clean_bytes_to_str(byte_input): return byte_input.decode('utf-8').strip() - - - From 395379178bc641f0c657271d198f84c3dc93708b Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Wed, 19 Dec 2018 22:08:44 +0100 Subject: [PATCH 14/46] Fix linting through using a raw python string --- django_migration_linter/migration_linter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index b2cd6aa..18ca0d1 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -166,7 +166,7 @@ class MigrationLinter(object): utils.clean_bytes_to_str, diff_process.stdout.readlines()): # Only gather lines that include added migrations if re.search( - '\/{0}\/.*\.py'.format(self.MIGRATION_FOLDER_NAME), + r'\/{0}\/.*\.py'.format(self.MIGRATION_FOLDER_NAME), line) and \ '__init__' not in line: app_name, migration_name = self._split_migration_path(line) From 357f0aebf4771a3c30432795d601649ff73d7216 Mon Sep 17 00:00:00 2001 From: Felix Date: Tue, 18 Dec 2018 16:46:50 +0100 Subject: [PATCH 15/46] Adapt tests so that not relying on undefined behaviour. The app directories can appear in any order, we need to allow that in tests. --- django_migration_linter/migration_linter.py | 3 +- tests/functional/test_cmd_line_call.py | 56 +++++++++++++++------ tests/unit/test_linter.py | 14 +++--- 3 files changed, 50 insertions(+), 23 deletions(-) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index 18ca0d1..d952193 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -18,6 +18,7 @@ import os import re from subprocess import Popen, PIPE import sys + from . import utils from .sql_analyser import analyse_sql_statements @@ -186,7 +187,7 @@ class MigrationLinter(object): def _gather_all_migrations(self): migrations = [] for root, dirs, files in os.walk(self.django_path): - for file_name in files: + for file_name in sorted(files): if os.path.basename(root) == self.MIGRATION_FOLDER_NAME and \ file_name.endswith('.py') and \ file_name != '__init__.py': diff --git a/tests/functional/test_cmd_line_call.py b/tests/functional/test_cmd_line_call.py index a3afd33..abc9479 100644 --- a/tests/functional/test_cmd_line_call.py +++ b/tests/functional/test_cmd_line_call.py @@ -35,9 +35,14 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) self.assertEqual(len(lines), 5) - self.assertTrue(lines[0].endswith('OK')) - self.assertTrue(lines[1].endswith('OK')) - self.assertTrue(lines[2].endswith('OK')) + self.assertEqual( + sorted(lines[:3]), + sorted([ + "(test_app1, 0001_initial)... OK", + "(test_app1, 0002_a_new_null_field)... OK", + "(test_app2, 0001_foo)... OK", + ]) + ) def test_call_linter_cmd_line_errors(self): cmd = '{0} {1}'.format( @@ -50,7 +55,6 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertNotEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) self.assertEqual(len(lines), 5) - print(lines) self.assertTrue(lines[0].endswith('OK')) self.assertTrue(lines[1].endswith('ERR')) self.assertTrue('RENAMING tables' in lines[2]) @@ -66,9 +70,14 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) self.assertEqual(len(lines), 5) - self.assertTrue(lines[0].endswith('OK')) - self.assertTrue(lines[1].endswith('OK')) - self.assertTrue(lines[2].endswith('IGNORE')) + self.assertEqual( + sorted(lines[:3]), + sorted([ + "(test_app1, 0001_initial)... OK", + "(test_app1, 0002_a_new_null_field)... OK", + "(test_app2, 0001_foo)... IGNORE", + ]) + ) def test_call_linter_cmd_line_include_apps(self): cmd = '{0} {1} --include-apps test_app2'.format( @@ -81,9 +90,14 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) self.assertEqual(len(lines), 5) - self.assertTrue(lines[0].endswith('IGNORE')) - self.assertTrue(lines[1].endswith('IGNORE')) - self.assertTrue(lines[2].endswith('OK')) + self.assertEqual( + sorted(lines[:3]), + sorted([ + "(test_app1, 0001_initial)... IGNORE", + "(test_app1, 0002_a_new_null_field)... IGNORE", + "(test_app2, 0001_foo)... OK", + ]) + ) def test_call_linter_cmd_line_ignore_name(self): cmd = '{0} {1} --ignore-name 0001_initial'.format( @@ -96,9 +110,14 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) self.assertEqual(len(lines), 5) - self.assertTrue(lines[0].endswith('IGNORE')) - self.assertTrue(lines[1].endswith('OK')) - self.assertTrue(lines[2].endswith('OK')) + self.assertEqual( + sorted(lines[:3]), + sorted([ + "(test_app1, 0001_initial)... IGNORE", + "(test_app1, 0002_a_new_null_field)... OK", + "(test_app2, 0001_foo)... OK", + ]) + ) def test_call_linter_cmd_line_ignore_name_contains(self): cmd = '{0} {1} --ignore-name-contains 0001'.format( @@ -111,9 +130,14 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) self.assertEqual(len(lines), 5) - self.assertTrue(lines[0].endswith('IGNORE')) - self.assertTrue(lines[1].endswith('OK')) - self.assertTrue(lines[2].endswith('IGNORE')) + self.assertEqual( + sorted(lines[:3]), + sorted([ + "(test_app1, 0001_initial)... IGNORE", + "(test_app1, 0002_a_new_null_field)... OK", + "(test_app2, 0001_foo)... IGNORE", + ]) + ) def test_call_linter_cmd_line_git_id(self): cmd = '{0} {1} d7125d5f4f0cc9623f670a66c54f131acc50032d'.format( diff --git a/tests/unit/test_linter.py b/tests/unit/test_linter.py index 0a4e47e..57e449e 100644 --- a/tests/unit/test_linter.py +++ b/tests/unit/test_linter.py @@ -99,9 +99,11 @@ class TestLinterFunctions(unittest.TestCase): linter = MigrationLinter(fixtures.CORRECT_PROJECT) migrations = linter._gather_all_migrations() self.assertEqual(len(migrations), 3) - self.assertEqual(migrations[0][0], 'test_app1') - self.assertEqual(migrations[0][1], '0001_initial') - self.assertEqual(migrations[1][0], 'test_app1') - self.assertEqual(migrations[1][1], '0002_a_new_null_field') - self.assertEqual(migrations[2][0], 'test_app2') - self.assertEqual(migrations[2][1], '0001_foo') + self.assertEqual( + sorted(migrations), + sorted([ + ("test_app1", "0001_initial"), + ("test_app1", "0002_a_new_null_field"), + ("test_app2", "0001_foo"), + ]) + ) From 9e3dbf7778e744bfbde5a3c6eddf13bb61ae2481 Mon Sep 17 00:00:00 2001 From: Felix Date: Sun, 9 Dec 2018 15:52:57 +0100 Subject: [PATCH 16/46] Mark migrations as ignored --- README.rst | 17 +++ django_migration_linter/migration_linter.py | 11 ++ django_migration_linter/sql_analyser.py | 40 ++++-- setup.py | 1 + tests/fixtures.py | 2 + tests/functional/test_migration_linter.py | 4 + .../test_project_ignore_migration/.gitignore | 3 + .../test_project_ignore_migration/manage.py | 22 ++++ .../test_app/__init__.py | 0 .../test_app/admin.py | 3 + .../test_app/apps.py | 5 + .../test_app/migrations/0001_create_table.py | 23 ++++ .../test_app/migrations/0002_rename_table.py | 22 ++++ .../test_app/migrations/__init__.py | 0 .../test_app/models.py | 4 + .../test_app/tests.py | 3 + .../test_app/views.py | 3 + .../test_project_ignore_migration/__init__.py | 0 .../test_project_ignore_migration/settings.py | 121 ++++++++++++++++++ .../test_project_ignore_migration/urls.py | 21 +++ .../test_project_ignore_migration/wsgi.py | 16 +++ tox.ini | 1 + 22 files changed, 310 insertions(+), 12 deletions(-) create mode 100644 tests/test_project_fixtures/test_project_ignore_migration/.gitignore create mode 100644 tests/test_project_fixtures/test_project_ignore_migration/manage.py create mode 100644 tests/test_project_fixtures/test_project_ignore_migration/test_app/__init__.py create mode 100644 tests/test_project_fixtures/test_project_ignore_migration/test_app/admin.py create mode 100644 tests/test_project_fixtures/test_project_ignore_migration/test_app/apps.py create mode 100644 tests/test_project_fixtures/test_project_ignore_migration/test_app/migrations/0001_create_table.py create mode 100644 tests/test_project_fixtures/test_project_ignore_migration/test_app/migrations/0002_rename_table.py create mode 100644 tests/test_project_fixtures/test_project_ignore_migration/test_app/migrations/__init__.py create mode 100644 tests/test_project_fixtures/test_project_ignore_migration/test_app/models.py create mode 100644 tests/test_project_fixtures/test_project_ignore_migration/test_app/tests.py create mode 100644 tests/test_project_fixtures/test_project_ignore_migration/test_app/views.py create mode 100644 tests/test_project_fixtures/test_project_ignore_migration/test_project_ignore_migration/__init__.py create mode 100644 tests/test_project_fixtures/test_project_ignore_migration/test_project_ignore_migration/settings.py create mode 100644 tests/test_project_fixtures/test_project_ignore_migration/test_project_ignore_migration/urls.py create mode 100644 tests/test_project_fixtures/test_project_ignore_migration/test_project_ignore_migration/wsgi.py diff --git a/README.rst b/README.rst index 2e80e6f..0c4710c 100644 --- a/README.rst +++ b/README.rst @@ -52,6 +52,23 @@ Examples 3YOURMIND is running the linter on `Bitbucket Pipelines`_ on every build getting pushed. Checkout the ``examples/`` folder to see how you could integrate the linter in your test suite. +Ignoring Migrations +------------------- + +You can also ignore migrations by adding this to your migrations: + +.. code-block:: + + from django_migration_linter import IGNORE_MIGRATION + # ... + + operations = [ + migrations.RunSQL(IGNORE_MIGRATION), + # ... + ] + # ... + + Tests ----- diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index d952193..79f25e6 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -24,6 +24,11 @@ from .sql_analyser import analyse_sql_statements logger = logging.getLogger(__name__) +# Can be used to mark ignored migrations. +# This is a noop for SQL. Comments would be better. +# But comments are stripped by RunSQL +IGNORE_MIGRATION = "SELECT 'dml-ignore'" + class MigrationLinter(object): MIGRATION_FOLDER_NAME = 'migrations' @@ -69,6 +74,12 @@ class MigrationLinter(object): analysis_result = analyse_sql_statements( sql_statements) errors = analysis_result['errors'] + + if analysis_result['ignored']: + print('IGNORE') + self.nb_ignored += 1 + return + if not errors: print('OK') self.nb_valid += 1 diff --git a/django_migration_linter/sql_analyser.py b/django_migration_linter/sql_analyser.py index 0fe3c1b..d07f544 100644 --- a/django_migration_linter/sql_analyser.py +++ b/django_migration_linter/sql_analyser.py @@ -63,29 +63,45 @@ migration_tests = ( 'code': '', 'fn': has_default, 'err_msg': '' + }, { + 'code': 'IGNORED_MIGRATION', + 'fn': lambda sql, **kw: re.search('dml-ignore', sql), + 'err_msg': '', } ) def analyse_sql_statements(sql_statements): errors = [] + ignored = False for statement in sql_statements: for test in migration_tests: if test['fn'](statement, errors=errors): - logger.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) + if test['code'] == 'IGNORED_MIGRATION': + logger.info( + 'Testing {0} -- IGNORING MIGRATION'.format(statement) + ) + ignored = True + else: + logger.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: logger.info('Testing {0} -- PASSED'.format(statement)) return { 'errors': errors, + 'ignored': ignored } diff --git a/setup.py b/setup.py index 79ca34d..2145545 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ with open(path.join(PROJECT_DIR, 'README.rst')) as f: install_requirements = [ 'django>=1.11', + 'sqlparse>=0.2.4' ] test_requirements = [ diff --git a/tests/fixtures.py b/tests/fixtures.py index 5d97a82..7f8622a 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -40,6 +40,8 @@ CORRECT_PROJECT = os.path.join( _FIXTURES_FOLDER, 'test_correct_project/') DELETED_MIGRATION_PROJECT = os.path.join( _FIXTURES_FOLDER, 'test_deleted_migration_project/') +IGNORE_MIGRATION_PROJECT = os.path.join( + _FIXTURES_FOLDER, 'test_project_ignore_migration/') NOT_DJANGO_GIT_PROJECT = os.path.join( _FIXTURES_FOLDER, 'test_git_project/') diff --git a/tests/functional/test_migration_linter.py b/tests/functional/test_migration_linter.py index 6187613..2c9acc5 100644 --- a/tests/functional/test_migration_linter.py +++ b/tests/functional/test_migration_linter.py @@ -56,6 +56,10 @@ class BackwardcompatibilityDetectionTest(unittest.TestCase): test_project_path = fixtures.RENAME_TABLE_PROJECT self._test_linter_finds_errors(test_project_path) + def test_ignore_migration(self): + test_project_path = fixtures.IGNORE_MIGRATION_PROJECT + self._test_linter_finds_no_errors(test_project_path) + def test_accept_not_null_column_followed_by_adding_default(self): test_project_path = \ fixtures.ADD_NOT_NULL_COLUMN_FOLLOWED_BY_DEFAULT_PROJECT diff --git a/tests/test_project_fixtures/test_project_ignore_migration/.gitignore b/tests/test_project_fixtures/test_project_ignore_migration/.gitignore new file mode 100644 index 0000000..63973d0 --- /dev/null +++ b/tests/test_project_fixtures/test_project_ignore_migration/.gitignore @@ -0,0 +1,3 @@ +*.pyc +__pycache__/ +*sqlite3 diff --git a/tests/test_project_fixtures/test_project_ignore_migration/manage.py b/tests/test_project_fixtures/test_project_ignore_migration/manage.py new file mode 100644 index 0000000..eba4c1f --- /dev/null +++ b/tests/test_project_fixtures/test_project_ignore_migration/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project_ignore_migration.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) diff --git a/tests/test_project_fixtures/test_project_ignore_migration/test_app/__init__.py b/tests/test_project_fixtures/test_project_ignore_migration/test_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_project_fixtures/test_project_ignore_migration/test_app/admin.py b/tests/test_project_fixtures/test_project_ignore_migration/test_app/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/tests/test_project_fixtures/test_project_ignore_migration/test_app/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/tests/test_project_fixtures/test_project_ignore_migration/test_app/apps.py b/tests/test_project_fixtures/test_project_ignore_migration/test_app/apps.py new file mode 100644 index 0000000..fc04070 --- /dev/null +++ b/tests/test_project_fixtures/test_project_ignore_migration/test_app/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TestAppConfig(AppConfig): + name = 'test_app' diff --git a/tests/test_project_fixtures/test_project_ignore_migration/test_app/migrations/0001_create_table.py b/tests/test_project_fixtures/test_project_ignore_migration/test_app/migrations/0001_create_table.py new file mode 100644 index 0000000..d847af6 --- /dev/null +++ b/tests/test_project_fixtures/test_project_ignore_migration/test_app/migrations/0001_create_table.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-25 14:15 +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)), + ], + ), + ] diff --git a/tests/test_project_fixtures/test_project_ignore_migration/test_app/migrations/0002_rename_table.py b/tests/test_project_fixtures/test_project_ignore_migration/test_app/migrations/0002_rename_table.py new file mode 100644 index 0000000..54fc7d9 --- /dev/null +++ b/tests/test_project_fixtures/test_project_ignore_migration/test_app/migrations/0002_rename_table.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-25 14:15 +from __future__ import unicode_literals + +from django.db import migrations + +from django_migration_linter import IGNORE_MIGRATION + + +class Migration(migrations.Migration): + + dependencies = [ + ('test_app', '0001_create_table'), + ] + + operations = [ + migrations.RunSQL(IGNORE_MIGRATION), + migrations.RenameModel( + old_name='A', + new_name='B', + ), + ] diff --git a/tests/test_project_fixtures/test_project_ignore_migration/test_app/migrations/__init__.py b/tests/test_project_fixtures/test_project_ignore_migration/test_app/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_project_fixtures/test_project_ignore_migration/test_app/models.py b/tests/test_project_fixtures/test_project_ignore_migration/test_app/models.py new file mode 100644 index 0000000..2633c55 --- /dev/null +++ b/tests/test_project_fixtures/test_project_ignore_migration/test_app/models.py @@ -0,0 +1,4 @@ +from django.db import models + +class B(models.Model): + null_field = models.IntegerField(null=True) diff --git a/tests/test_project_fixtures/test_project_ignore_migration/test_app/tests.py b/tests/test_project_fixtures/test_project_ignore_migration/test_app/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/tests/test_project_fixtures/test_project_ignore_migration/test_app/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/tests/test_project_fixtures/test_project_ignore_migration/test_app/views.py b/tests/test_project_fixtures/test_project_ignore_migration/test_app/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/tests/test_project_fixtures/test_project_ignore_migration/test_app/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/tests/test_project_fixtures/test_project_ignore_migration/test_project_ignore_migration/__init__.py b/tests/test_project_fixtures/test_project_ignore_migration/test_project_ignore_migration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_project_fixtures/test_project_ignore_migration/test_project_ignore_migration/settings.py b/tests/test_project_fixtures/test_project_ignore_migration/test_project_ignore_migration/settings.py new file mode 100644 index 0000000..39bfb6c --- /dev/null +++ b/tests/test_project_fixtures/test_project_ignore_migration/test_project_ignore_migration/settings.py @@ -0,0 +1,121 @@ +""" +Django settings for test_project_ignore_migration 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_app' +] + +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_ignore_migration.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_ignore_migration.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.11/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# 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/' diff --git a/tests/test_project_fixtures/test_project_ignore_migration/test_project_ignore_migration/urls.py b/tests/test_project_fixtures/test_project_ignore_migration/test_project_ignore_migration/urls.py new file mode 100644 index 0000000..735de5d --- /dev/null +++ b/tests/test_project_fixtures/test_project_ignore_migration/test_project_ignore_migration/urls.py @@ -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), +] diff --git a/tests/test_project_fixtures/test_project_ignore_migration/test_project_ignore_migration/wsgi.py b/tests/test_project_fixtures/test_project_ignore_migration/test_project_ignore_migration/wsgi.py new file mode 100644 index 0000000..f3abf75 --- /dev/null +++ b/tests/test_project_fixtures/test_project_ignore_migration/test_project_ignore_migration/wsgi.py @@ -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_ignore_migration.settings") + +application = get_wsgi_application() diff --git a/tox.ini b/tox.ini index 7ffc655..66749df 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,7 @@ pip_pre = true deps = django111: django>=1.11,<2.0 django20: django>=2.0 + sqlparse [testenv:lint] basepython = python3.6 From ff8e1b43634506b96784cf1439332e1090ec60bd Mon Sep 17 00:00:00 2001 From: Felix Date: Mon, 17 Dec 2018 16:51:31 +0100 Subject: [PATCH 17/46] Remove sqlparse --- django_migration_linter/migration_linter.py | 2 +- setup.py | 1 - tox.ini | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index 79f25e6..139cbe6 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -27,7 +27,7 @@ logger = logging.getLogger(__name__) # Can be used to mark ignored migrations. # This is a noop for SQL. Comments would be better. # But comments are stripped by RunSQL -IGNORE_MIGRATION = "SELECT 'dml-ignore'" +IGNORE_MIGRATION = ("SELECT 'dml-ignore'", ) class MigrationLinter(object): diff --git a/setup.py b/setup.py index 2145545..79ca34d 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,6 @@ with open(path.join(PROJECT_DIR, 'README.rst')) as f: install_requirements = [ 'django>=1.11', - 'sqlparse>=0.2.4' ] test_requirements = [ diff --git a/tox.ini b/tox.ini index 66749df..7ffc655 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,6 @@ pip_pre = true deps = django111: django>=1.11,<2.0 django20: django>=2.0 - sqlparse [testenv:lint] basepython = python3.6 From 6fe51d2e0802139c03e76ccd392c592654fe321f Mon Sep 17 00:00:00 2001 From: Felix Date: Mon, 17 Dec 2018 17:04:58 +0100 Subject: [PATCH 18/46] Remove duplication --- django_migration_linter/__init__.py | 1 + django_migration_linter/constants.py | 4 ++++ django_migration_linter/migration_linter.py | 5 ----- django_migration_linter/sql_analyser.py | 4 +++- 4 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 django_migration_linter/constants.py diff --git a/django_migration_linter/__init__.py b/django_migration_linter/__init__.py index 959bac8..dd30599 100644 --- a/django_migration_linter/__init__.py +++ b/django_migration_linter/__init__.py @@ -13,3 +13,4 @@ # limitations under the License. from .migration_linter import * # noqa +from .constants import * # noqa diff --git a/django_migration_linter/constants.py b/django_migration_linter/constants.py new file mode 100644 index 0000000..b8356c6 --- /dev/null +++ b/django_migration_linter/constants.py @@ -0,0 +1,4 @@ +# Can be used to mark ignored migrations. +# This is a noop for SQL. Comments would be better. +# But comments are stripped by RunSQL +IGNORE_MIGRATION = ("SELECT 'dml-ignore'", ) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index 139cbe6..e049ed5 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -24,11 +24,6 @@ from .sql_analyser import analyse_sql_statements logger = logging.getLogger(__name__) -# Can be used to mark ignored migrations. -# This is a noop for SQL. Comments would be better. -# But comments are stripped by RunSQL -IGNORE_MIGRATION = ("SELECT 'dml-ignore'", ) - class MigrationLinter(object): MIGRATION_FOLDER_NAME = 'migrations' diff --git a/django_migration_linter/sql_analyser.py b/django_migration_linter/sql_analyser.py index d07f544..edb655b 100644 --- a/django_migration_linter/sql_analyser.py +++ b/django_migration_linter/sql_analyser.py @@ -15,6 +15,8 @@ import re import logging +from django_migration_linter.constants import IGNORE_MIGRATION + logger = logging.getLogger(__name__) @@ -65,7 +67,7 @@ migration_tests = ( 'err_msg': '' }, { 'code': 'IGNORED_MIGRATION', - 'fn': lambda sql, **kw: re.search('dml-ignore', sql), + 'fn': lambda sql, **kw: re.search(IGNORE_MIGRATION[0], sql), 'err_msg': '', } ) From fe6433a065fb98656817c3493a8590ec02d86a56 Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Sun, 23 Dec 2018 17:04:43 +0100 Subject: [PATCH 19/46] Use a custom migration operation instead of a RunSQL statement --- django_migration_linter/__init__.py | 2 +- django_migration_linter/constants.py | 4 -- django_migration_linter/operations.py | 38 +++++++++++++++++++ django_migration_linter/sql_analyser.py | 10 +++-- .../test_app/migrations/0002_rename_table.py | 5 +-- 5 files changed, 47 insertions(+), 12 deletions(-) delete mode 100644 django_migration_linter/constants.py create mode 100644 django_migration_linter/operations.py diff --git a/django_migration_linter/__init__.py b/django_migration_linter/__init__.py index dd30599..a548f66 100644 --- a/django_migration_linter/__init__.py +++ b/django_migration_linter/__init__.py @@ -13,4 +13,4 @@ # limitations under the License. from .migration_linter import * # noqa -from .constants import * # noqa +from .operations import * # noqa diff --git a/django_migration_linter/constants.py b/django_migration_linter/constants.py deleted file mode 100644 index b8356c6..0000000 --- a/django_migration_linter/constants.py +++ /dev/null @@ -1,4 +0,0 @@ -# Can be used to mark ignored migrations. -# This is a noop for SQL. Comments would be better. -# But comments are stripped by RunSQL -IGNORE_MIGRATION = ("SELECT 'dml-ignore'", ) diff --git a/django_migration_linter/operations.py b/django_migration_linter/operations.py new file mode 100644 index 0000000..cf98135 --- /dev/null +++ b/django_migration_linter/operations.py @@ -0,0 +1,38 @@ +# Copyright 2018 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 django.db.migrations.operations.base import Operation + +IGNORE_MIGRATION_SQL = "-- dml ignores this migration" + + +class IgnoreMigration(Operation): + + reversible = True + # Must be true to be generated through the sqlmigrate command + reduces_to_sql = True + + def state_forwards(self, app_label, state): + pass + + def database_forwards( + self, app_label, schema_editor, from_state, to_state): + schema_editor.execute(IGNORE_MIGRATION_SQL) + + def database_backwards( + self, app_label, schema_editor, from_state, to_state): + schema_editor.execute(IGNORE_MIGRATION_SQL + "(reversed)") + + def describe(self): + return "The Django migration linter will ignore this migration" diff --git a/django_migration_linter/sql_analyser.py b/django_migration_linter/sql_analyser.py index edb655b..fa0a0d9 100644 --- a/django_migration_linter/sql_analyser.py +++ b/django_migration_linter/sql_analyser.py @@ -15,7 +15,9 @@ import re import logging -from django_migration_linter.constants import IGNORE_MIGRATION +from django_migration_linter.operations import IGNORE_MIGRATION_SQL + +IGNORED_MIGRATION = "IGNORED_MIGRATION" logger = logging.getLogger(__name__) @@ -66,8 +68,8 @@ migration_tests = ( 'fn': has_default, 'err_msg': '' }, { - 'code': 'IGNORED_MIGRATION', - 'fn': lambda sql, **kw: re.search(IGNORE_MIGRATION[0], sql), + 'code': IGNORED_MIGRATION, + 'fn': lambda sql, **kw: re.search(IGNORE_MIGRATION_SQL, sql), 'err_msg': '', } ) @@ -79,7 +81,7 @@ def analyse_sql_statements(sql_statements): for statement in sql_statements: for test in migration_tests: if test['fn'](statement, errors=errors): - if test['code'] == 'IGNORED_MIGRATION': + if test['code'] == IGNORED_MIGRATION: logger.info( 'Testing {0} -- IGNORING MIGRATION'.format(statement) ) diff --git a/tests/test_project_fixtures/test_project_ignore_migration/test_app/migrations/0002_rename_table.py b/tests/test_project_fixtures/test_project_ignore_migration/test_app/migrations/0002_rename_table.py index 54fc7d9..d521e87 100644 --- a/tests/test_project_fixtures/test_project_ignore_migration/test_app/migrations/0002_rename_table.py +++ b/tests/test_project_fixtures/test_project_ignore_migration/test_app/migrations/0002_rename_table.py @@ -4,8 +4,7 @@ from __future__ import unicode_literals from django.db import migrations -from django_migration_linter import IGNORE_MIGRATION - +import django_migration_linter as linter class Migration(migrations.Migration): @@ -14,7 +13,7 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunSQL(IGNORE_MIGRATION), + linter.IgnoreMigration(), migrations.RenameModel( old_name='A', new_name='B', From 95274b9a67fbbd026afa97d61797645f3960ebbf Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Sun, 23 Dec 2018 17:12:36 +0100 Subject: [PATCH 20/46] Update Readme for new linter ignore migration operation Use master for PRs. No need for a dev branch --- README.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 0c4710c..d1c5161 100644 --- a/README.rst +++ b/README.rst @@ -52,20 +52,20 @@ Examples 3YOURMIND is running the linter on `Bitbucket Pipelines`_ on every build getting pushed. Checkout the ``examples/`` folder to see how you could integrate the linter in your test suite. -Ignoring Migrations +Ignoring migrations ------------------- You can also ignore migrations by adding this to your migrations: .. code-block:: - from django_migration_linter import IGNORE_MIGRATION + import django_migration_linter as linter # ... - operations = [ - migrations.RunSQL(IGNORE_MIGRATION), - # ... - ] + operations = [ + linter.IgnoreMigration(), + # ... + ] # ... @@ -78,7 +78,7 @@ Contributing ------------ First, thank you very much if you want to contribute to the project. -Please base your work on the ``dev`` branch and also target this branch in your pull request. +Please base your work on the ``master`` branch and also target this branch in your pull request. License ------- From d3805d5210d4e3ce8fd7fcc5d221a3a327dc36d5 Mon Sep 17 00:00:00 2001 From: Felix Date: Sun, 23 Dec 2018 22:49:36 +0100 Subject: [PATCH 21/46] Integrate IGNORE into cache --- django_migration_linter/cache.py | 1 + django_migration_linter/migration_linter.py | 1 + tests/functional/test_cache.py | 26 +++++++++++++++++++-- tests/functional/test_cmd_line_call.py | 2 +- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/django_migration_linter/cache.py b/django_migration_linter/cache.py index 46e3a63..ca03b2d 100644 --- a/django_migration_linter/cache.py +++ b/django_migration_linter/cache.py @@ -41,6 +41,7 @@ class Cache(dict): pass def save(self): + print(self) f = open(self.filename, 'wb') pickle.dump(self, f, 2) f.close() diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index a04f5a2..1614f8a 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -103,6 +103,7 @@ class MigrationLinter(object): if analysis_result['ignored']: print('IGNORE') + self.new_cache[md5hash] = {'result': 'IGNORE'} self.nb_ignored += 1 return diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 4ea631f..7c68ed4 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -28,7 +28,6 @@ else: import mock - class CacheTest(unittest.TestCase): MIGRATION_FILE = os.path.join(fixtures.ALTER_COLUMN_PROJECT, 'test_app', 'migrations', '0001_initial.py') fd, temp_path = tempfile.mkstemp() @@ -73,7 +72,30 @@ class CacheTest(unittest.TestCase): self.assertTrue(linter.has_errors) def test_cache_ignored(self): - pass + cache_file = os.path.join(DEFAULT_CACHE_PATH, 'test_project_ignore_migration.pickle') + if os.path.exists(cache_file): + os.remove(cache_file) + linter = MigrationLinter(fixtures.IGNORE_MIGRATION_PROJECT) + + with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql)as sql_mock: + linter.lint_all_migrations() + sql_mock.assert_called() + + cache = Cache( + fixtures.IGNORE_MIGRATION_PROJECT, + DEFAULT_CACHE_PATH + ) + cache.load() + + self.assertEqual(cache['63230606af0eccaef7f1f78c537c624c']['result'], 'OK') + self.assertEqual(cache['5c5ca1780a9f28439c1defc1f32af894']['result'], 'IGNORE') + + # Start the Linter again -> should use cache now. + linter = MigrationLinter(fixtures.IGNORE_MIGRATION_PROJECT) + + with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql)as sql_mock: + linter.lint_all_migrations() + sql_mock.assert_not_called() def test_cache_modified(self): cache_file = os.path.join(DEFAULT_CACHE_PATH, 'test_project_alter_column.pickle') diff --git a/tests/functional/test_cmd_line_call.py b/tests/functional/test_cmd_line_call.py index 8518a9e..d15a9cd 100644 --- a/tests/functional/test_cmd_line_call.py +++ b/tests/functional/test_cmd_line_call.py @@ -67,7 +67,7 @@ class CallLinterFromCommandLineTest(unittest.TestCase): fixtures.CORRECT_PROJECT) process = Popen( - cmd, shell=True, stdout=PIPE, stderr=sys.stderr) + cmd, shell=True, stdout=PIPE, stderr=PIPE) process.wait() self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) From 03e48b7575b49a21443213bdfa27bf7ad9faf9df Mon Sep 17 00:00:00 2001 From: Felix Date: Sun, 23 Dec 2018 23:06:18 +0100 Subject: [PATCH 22/46] Adjust readme --- README.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.rst b/README.rst index d1c5161..9c47e3f 100644 --- a/README.rst +++ b/README.rst @@ -44,6 +44,8 @@ Usage ``--exclude-apps EXCLUDE_APPS [EXCLUDE_APPS ...]`` Ignore migrations that are in the specified django apps. ``--verbose or -v`` Print more information during execution. ``--database DATABASE`` Specify the database for which to generate the SQL. Defaults to *default*. +``--cache-path PATH`` specify a directory that should be used to store cache-files in. +``--no-cache`` Don't use a cache. ================================================== =========================================================================================================================== Examples @@ -68,6 +70,15 @@ You can also ignore migrations by adding this to your migrations: ] # ... +Cache +----- +By default, the linter uses a cache to prevent linting the same migration again. +The default location of the cache on Linux is +``/home//.cache/django-migration-linter//.pickle``. + +Since the linter uses hashes, modifying migration files, will re-run the linter on that migration. +If you want to run the linter without cache, use the flag ``--no-cache``. +If you want to invalidate the cache, delete the cache folder. Tests ----- From 1c28412ebd9777f05910305d1ee57234607765af Mon Sep 17 00:00:00 2001 From: Felix Date: Sun, 23 Dec 2018 23:28:35 +0100 Subject: [PATCH 23/46] Improve the cache React to Comments and add a Test --- django_migration_linter/cache.py | 13 ++++------- django_migration_linter/migration_linter.py | 1 - tests/functional/test_cache.py | 26 ++++++++++++++++++--- tests/functional/test_cmd_line_call.py | 1 - 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/django_migration_linter/cache.py b/django_migration_linter/cache.py index ca03b2d..e0f00ea 100644 --- a/django_migration_linter/cache.py +++ b/django_migration_linter/cache.py @@ -33,18 +33,15 @@ class Cache(dict): def load(self): try: - f = open(self.filename, 'rb') - tmp_dict = pickle.load(f) - self.update(tmp_dict) - f.close() + with open(self.filename, 'rb') as f: + tmp_dict = pickle.load(f) + self.update(tmp_dict) except IOError: pass def save(self): - print(self) - f = open(self.filename, 'wb') - pickle.dump(self, f, 2) - f.close() + with open(self.filename, 'wb') as f: + pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL) def md5(self, app_name, migration): path = compose_migration_path(self.django_folder, app_name, migration) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index 1614f8a..e3d1fd3 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -91,7 +91,6 @@ class MigrationLinter(object): if self.should_ignore_migration(app_name, migration_name): print('IGNORE') - self.new_cache[md5hash] = {'result': 'IGNORE'} self.nb_ignored += 1 return diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 7c68ed4..f6352e1 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -65,7 +65,7 @@ class CacheTest(unittest.TestCase): # Start the Linter again -> should use cache now. linter = MigrationLinter(fixtures.ADD_NOT_NULL_COLUMN_PROJECT) - with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql)as sql_mock: + with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql) as sql_mock: linter.lint_all_migrations() sql_mock.assert_not_called() @@ -77,7 +77,7 @@ class CacheTest(unittest.TestCase): os.remove(cache_file) linter = MigrationLinter(fixtures.IGNORE_MIGRATION_PROJECT) - with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql)as sql_mock: + with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql) as sql_mock: linter.lint_all_migrations() sql_mock.assert_called() @@ -93,10 +93,30 @@ class CacheTest(unittest.TestCase): # Start the Linter again -> should use cache now. linter = MigrationLinter(fixtures.IGNORE_MIGRATION_PROJECT) - with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql)as sql_mock: + with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql) as sql_mock: linter.lint_all_migrations() sql_mock.assert_not_called() + def test_cache_ignored_command_line(self): + cache_file = os.path.join(DEFAULT_CACHE_PATH, 'test_project_ignore_migration.pickle') + if os.path.exists(cache_file): + os.remove(cache_file) + linter = MigrationLinter(fixtures.IGNORE_MIGRATION_PROJECT, + ignore_name_contains='0001') + + with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql) as sql_mock: + linter.lint_all_migrations() + sql_mock.assert_called() + + cache = Cache( + fixtures.IGNORE_MIGRATION_PROJECT, + DEFAULT_CACHE_PATH + ) + cache.load() + + self.assertNotIn('63230606af0eccaef7f1f78c537c624c', cache) + self.assertEqual(cache['5c5ca1780a9f28439c1defc1f32af894']['result'], 'IGNORE') + def test_cache_modified(self): cache_file = os.path.join(DEFAULT_CACHE_PATH, 'test_project_alter_column.pickle') if os.path.exists(cache_file): diff --git a/tests/functional/test_cmd_line_call.py b/tests/functional/test_cmd_line_call.py index d15a9cd..b8ddabe 100644 --- a/tests/functional/test_cmd_line_call.py +++ b/tests/functional/test_cmd_line_call.py @@ -172,7 +172,6 @@ class CallLinterFromCommandLineTest(unittest.TestCase): def test_call_linter_cmd_line_no_cache(self): cache_file = os.path.join(DEFAULT_CACHE_PATH, 'test_correct_project.pickle') - print(cache_file) if os.path.exists(cache_file): os.remove(cache_file) From 5be5e3f769013e6f5bb40d3644f9daca12c151fc Mon Sep 17 00:00:00 2001 From: Felix Date: Sun, 23 Dec 2018 23:46:24 +0100 Subject: [PATCH 24/46] Use call_count instead of assert_called() because later was introduced in python 3.6. --- django_migration_linter/cache.py | 2 +- tests/functional/test_cache.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/django_migration_linter/cache.py b/django_migration_linter/cache.py index e0f00ea..0ddd08c 100644 --- a/django_migration_linter/cache.py +++ b/django_migration_linter/cache.py @@ -41,7 +41,7 @@ class Cache(dict): def save(self): with open(self.filename, 'wb') as f: - pickle.dump(self, f, protocol=pickle.HIGHEST_PROTOCOL) + pickle.dump(self, f, protocol=2) def md5(self, app_name, migration): path = compose_migration_path(self.django_folder, app_name, migration) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index f6352e1..ae289b2 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -47,7 +47,7 @@ class CacheTest(unittest.TestCase): with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql)as sql_mock: linter.lint_all_migrations() - sql_mock.assert_called() + self.assertEqual(sql_mock.call_count, 2) cache = Cache( fixtures.ADD_NOT_NULL_COLUMN_PROJECT, @@ -67,7 +67,7 @@ class CacheTest(unittest.TestCase): with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql) as sql_mock: linter.lint_all_migrations() - sql_mock.assert_not_called() + self.assertEqual(sql_mock.call_count, 0) self.assertTrue(linter.has_errors) @@ -79,7 +79,7 @@ class CacheTest(unittest.TestCase): with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql) as sql_mock: linter.lint_all_migrations() - sql_mock.assert_called() + self.assertEqual(sql_mock.call_count, 2) cache = Cache( fixtures.IGNORE_MIGRATION_PROJECT, @@ -95,7 +95,7 @@ class CacheTest(unittest.TestCase): with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql) as sql_mock: linter.lint_all_migrations() - sql_mock.assert_not_called() + self.assertEqual(sql_mock.call_count, 0) def test_cache_ignored_command_line(self): cache_file = os.path.join(DEFAULT_CACHE_PATH, 'test_project_ignore_migration.pickle') @@ -106,7 +106,7 @@ class CacheTest(unittest.TestCase): with mock.patch.object(MigrationLinter, 'get_sql', wraps=linter.get_sql) as sql_mock: linter.lint_all_migrations() - sql_mock.assert_called() + self.assertEqual(sql_mock.call_count, 1) cache = Cache( fixtures.IGNORE_MIGRATION_PROJECT, From ef23a622ca15a34ebbe8d95a3f6573c21c68b26b Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Mon, 24 Dec 2018 18:25:08 +0100 Subject: [PATCH 25/46] Add python 3.7 to tests --- .travis.yml | 5 +++++ setup.py | 1 + tox.ini | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 759aa10..17bb355 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,11 @@ matrix: - python: 3.6 env: TOXENV=py36-django20 + - python: 3.7 + env: TOXENV=py37-django111 + - python: 3.7 + env: TOXENV=py37-django20 + - python: 3.6 env: TOXENV=lint diff --git a/setup.py b/setup.py index dd6bee3..b093f26 100644 --- a/setup.py +++ b/setup.py @@ -81,5 +81,6 @@ setup( 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', ] ) diff --git a/tox.ini b/tox.ini index a420feb..cc2b467 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = - py{27,34,35,36}-django111 - py{34,35,36}-django20 + py{27,34,35,36,37}-django111 + py{34,35,36,37}-django20 lint [testenv] From d041f7dbf7760b52086f55285a6cad102a544440 Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Mon, 24 Dec 2018 18:27:25 +0100 Subject: [PATCH 26/46] Add django 2.1 to tested envs --- .travis.yml | 6 ++++++ setup.py | 1 + tox.ini | 4 +++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 17bb355..9f9987b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,16 +15,22 @@ matrix: env: TOXENV=py35-django111 - python: 3.5 env: TOXENV=py35-django20 + - python: 3.5 + env: TOXENV=py35-django21 - python: 3.6 env: TOXENV=py36-django111 - python: 3.6 env: TOXENV=py36-django20 + - python: 3.6 + env: TOXENV=py36-django21 - python: 3.7 env: TOXENV=py37-django111 - python: 3.7 env: TOXENV=py37-django20 + - python: 3.7 + env: TOXENV=py37-django21 - python: 3.6 env: TOXENV=lint diff --git a/setup.py b/setup.py index b093f26..834d85d 100644 --- a/setup.py +++ b/setup.py @@ -73,6 +73,7 @@ setup( 'Framework :: Django', 'Framework :: Django :: 1.11', 'Framework :: Django :: 2.0', + 'Framework :: Django :: 2.1', 'Programming Language :: Python', 'Programming Language :: Python :: 2', diff --git a/tox.ini b/tox.ini index cc2b467..2d713aa 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = py{27,34,35,36,37}-django111 py{34,35,36,37}-django20 + py{35,36,37}-django21 lint [testenv] @@ -10,7 +11,8 @@ extras = test pip_pre = true deps = django111: django>=1.11,<2.0 - django20: django>=2.0 + django20: django>=2.0,<2.1 + django21: django>=2.1 appdirs==1.4.3 py27: mock==2.0.0 From 03bd7753e51b7c6dd46442e8ece957477c222714 Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Mon, 24 Dec 2018 18:37:00 +0100 Subject: [PATCH 27/46] Change ubuntu dist to xenial --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 9f9987b..de36ce3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ sudo: false language: python +dist: xenial matrix: include: From f05b199925c08ec6bafbfd6d332c4f9b79a001b8 Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Mon, 24 Dec 2018 16:05:38 +0100 Subject: [PATCH 28/46] Black all files --- django_migration_linter/cache.py | 7 +- django_migration_linter/constants.py | 8 +- django_migration_linter/migration_linter.py | 299 ++++++++++---------- django_migration_linter/operations.py | 6 +- django_migration_linter/sql_analyser.py | 118 ++++---- django_migration_linter/utils.py | 26 +- 6 files changed, 231 insertions(+), 233 deletions(-) diff --git a/django_migration_linter/cache.py b/django_migration_linter/cache.py index 0ddd08c..61d48a6 100644 --- a/django_migration_linter/cache.py +++ b/django_migration_linter/cache.py @@ -23,8 +23,7 @@ class Cache(dict): def __init__(self, django_folder, cache_path): self.django_folder = django_folder project_name = split_path(django_folder)[-2] - self.filename = \ - os.path.join(cache_path, '{0}.pickle'.format(project_name)) + self.filename = os.path.join(cache_path, "{0}.pickle".format(project_name)) if not os.path.exists(os.path.dirname(self.filename)): os.makedirs(os.path.dirname(self.filename)) @@ -33,14 +32,14 @@ class Cache(dict): def load(self): try: - with open(self.filename, 'rb') as f: + with open(self.filename, "rb") as f: tmp_dict = pickle.load(f) self.update(tmp_dict) except IOError: pass def save(self): - with open(self.filename, 'wb') as f: + with open(self.filename, "wb") as f: pickle.dump(self, f, protocol=2) def md5(self, app_name, migration): diff --git a/django_migration_linter/constants.py b/django_migration_linter/constants.py index 1bdfc9b..a95e8dc 100644 --- a/django_migration_linter/constants.py +++ b/django_migration_linter/constants.py @@ -14,10 +14,8 @@ from appdirs import user_cache_dir -__version__ = '0.0.7' +__version__ = "0.0.7" -MIGRATION_FOLDER_NAME = 'migrations' +MIGRATION_FOLDER_NAME = "migrations" -DEFAULT_CACHE_PATH = user_cache_dir( - "django-migration-linter", version=__version__ -) +DEFAULT_CACHE_PATH = user_cache_dir("django-migration-linter", version=__version__) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index e3d1fd3..d88f504 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -20,8 +20,7 @@ from subprocess import Popen, PIPE import sys from django_migration_linter.cache import Cache -from django_migration_linter.constants import DEFAULT_CACHE_PATH, \ - MIGRATION_FOLDER_NAME +from django_migration_linter.constants import DEFAULT_CACHE_PATH, MIGRATION_FOLDER_NAME from django_migration_linter.utils import split_migration_path from . import utils from .sql_analyser import analyse_sql_statements @@ -30,30 +29,36 @@ logger = logging.getLogger(__name__) class MigrationLinter(object): - 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)) + "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)) + ("The given path {0} does not " "seem to be a django project.").format( + project_path + ) + ) # 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.cache_path = kwargs.get('cache_path', None) or DEFAULT_CACHE_PATH - self.no_cache = kwargs.get('no_cache', None) or False + 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.cache_path = kwargs.get("cache_path", None) or DEFAULT_CACHE_PATH + self.no_cache = kwargs.get("no_cache", None) or False - self.python_exe = '{0}/bin/{1}'.format(sys.prefix, 'python') if \ - hasattr(sys, 'real_prefix') else 'python' + self.python_exe = ( + "{0}/bin/{1}".format(sys.prefix, "python") + if hasattr(sys, "real_prefix") + else "python" + ) # Initialise counters self.nb_valid = 0 @@ -68,65 +73,62 @@ class MigrationLinter(object): self.old_cache.load() def lint_migration(self, app_name, migration_name): - print('({0}, {1})... '.format(app_name, migration_name), end='') + print("({0}, {1})... ".format(app_name, migration_name), end="") self.nb_total += 1 md5hash = self.old_cache.md5(app_name, migration_name) if md5hash in self.old_cache: - if self.old_cache[md5hash]['result'] == 'IGNORE': - print('IGNORE (cached)') + if self.old_cache[md5hash]["result"] == "IGNORE": + print("IGNORE (cached)") self.nb_ignored += 1 - elif self.old_cache[md5hash]['result'] == 'OK': - print('OK (cached)') + elif self.old_cache[md5hash]["result"] == "OK": + print("OK (cached)") self.nb_valid += 1 else: - print('ERR (cached)') + print("ERR (cached)") self.nb_erroneous += 1 - if 'errors' in self.old_cache[md5hash]: - self.print_errors(self.old_cache[md5hash]['errors']) + if "errors" in self.old_cache[md5hash]: + self.print_errors(self.old_cache[md5hash]["errors"]) self.new_cache[md5hash] = self.old_cache[md5hash] return if self.should_ignore_migration(app_name, migration_name): - print('IGNORE') + print("IGNORE") self.nb_ignored += 1 return - sql_statements = self.get_sql( - app_name, migration_name) - analysis_result = analyse_sql_statements( - sql_statements) - errors = analysis_result['errors'] + sql_statements = self.get_sql(app_name, migration_name) + analysis_result = analyse_sql_statements(sql_statements) + errors = analysis_result["errors"] - if analysis_result['ignored']: - print('IGNORE') - self.new_cache[md5hash] = {'result': 'IGNORE'} + if analysis_result["ignored"]: + print("IGNORE") + self.new_cache[md5hash] = {"result": "IGNORE"} self.nb_ignored += 1 return if not errors: - print('OK') - self.new_cache[md5hash] = {'result': 'OK'} + print("OK") + self.new_cache[md5hash] = {"result": "OK"} self.nb_valid += 1 return - print('ERR') - self.new_cache[md5hash] = {'result': 'ERR', 'errors': errors} + print("ERR") + self.new_cache[md5hash] = {"result": "ERR", "errors": errors} self.nb_erroneous += 1 self.print_errors(errors) @staticmethod def print_errors(errors): 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 += ')' + 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): @@ -134,8 +136,10 @@ class MigrationLinter(object): 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)) + ( + "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() @@ -148,14 +152,14 @@ class MigrationLinter(object): self.new_cache.save() 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_ignored)) + 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_ignored) + ) @property def has_errors(self): @@ -174,153 +178,161 @@ class MigrationLinter(object): it allows to seperate the instances correctly. """ sqlmigrate_command = ( - 'cd {0} && ' - '{1} manage.py sqlmigrate {2} {3} ' - '--database {4}').format( - self.django_path, - self.python_exe, app_name, migration_name, - self.database) - logger.info('Executing {0}'.format(sqlmigrate_command)) + "cd {0} && " "{1} manage.py sqlmigrate {2} {3} " "--database {4}" + ).format( + self.django_path, self.python_exe, app_name, migration_name, self.database + ) + logger.info("Executing {0}".format(sqlmigrate_command)) sqlmigrate_process = Popen( - sqlmigrate_command, shell=True, stdout=PIPE, stderr=PIPE) + sqlmigrate_command, shell=True, stdout=PIPE, stderr=PIPE + ) sql_statements = [] for line in map( - utils.clean_bytes_to_str, - sqlmigrate_process.stdout.readlines()): + utils.clean_bytes_to_str, sqlmigrate_process.stdout.readlines() + ): sql_statements.append(line) sqlmigrate_process.wait() if sqlmigrate_process.returncode != 0: _, err = sqlmigrate_process.communicate() - raise RuntimeError('sqlmigrate command failed {0}'.format( - err.decode('utf-8'))) - logger.info( - 'Found {0} sql migration lines'.format(len(sql_statements))) + raise RuntimeError( + "sqlmigrate command failed {0}".format(err.decode("utf-8")) + ) + logger.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 --diff-filter=A {1}').format( - self.django_path, git_commit_id) - logger.info('Executing {0}'.format(git_diff_command)) - diff_process = Popen( - git_diff_command, shell=True, stdout=PIPE, stderr=PIPE) - for line in map( - utils.clean_bytes_to_str, diff_process.stdout.readlines()): + "cd {0} && " "git diff --name-only --diff-filter=A {1}" + ).format(self.django_path, git_commit_id) + logger.info("Executing {0}".format(git_diff_command)) + diff_process = Popen(git_diff_command, shell=True, stdout=PIPE, stderr=PIPE) + for line in map(utils.clean_bytes_to_str, diff_process.stdout.readlines()): # Only gather lines that include added migrations - if re.search( - r'\/{0}\/.*\.py'.format(MIGRATION_FOLDER_NAME), - line) and \ - '__init__' not in line: + if ( + re.search(r"\/{0}\/.*\.py".format(MIGRATION_FOLDER_NAME), line) + and "__init__" not in line + ): app_name, migration_name = split_migration_path(line) migrations.append((app_name, migration_name)) diff_process.wait() if diff_process.returncode != 0: output = [] - for line in map( - utils.clean_bytes_to_str, diff_process.stderr.readlines()): + for line in map(utils.clean_bytes_to_str, diff_process.stderr.readlines()): output.append(line) - logger.info("Error while git diff command:\n{}".format( - "".join(output))) - raise Exception('Error while executing git diff command') + logger.info("Error while git diff command:\n{}".format("".join(output))) + raise Exception("Error while executing git diff command") return migrations def _gather_all_migrations(self): migrations = [] for root, dirs, files in os.walk(self.django_path): for file_name in sorted(files): - if os.path.basename(root) == MIGRATION_FOLDER_NAME and \ - file_name.endswith('.py') and \ - file_name != '__init__.py': + if ( + os.path.basename(root) == 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 = split_migration_path( - full_migration_path) + app_name, migration_name = 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 - app_name not in self.include_apps)\ - or (self.exclude_apps and - app_name in self.exclude_apps)\ - or (self.ignore_name_contains and - self.ignore_name_contains in migration_name)\ + return ( + (self.include_apps and app_name not in self.include_apps) + or (self.exclude_apps and app_name in self.exclude_apps) + or ( + self.ignore_name_contains + and self.ignore_name_contains in migration_name + ) or (migration_name in self.ignore_name) + ) def _main(): import argparse + parser = argparse.ArgumentParser( - description='Detect backward incompatible django migrations.') + description="Detect backward incompatible django migrations." + ) parser.add_argument( - 'django_folder', - metavar='DJANGO_FOLDER', + "django_folder", + metavar="DJANGO_FOLDER", type=str, nargs=1, - help='the path to the django project') + help="the path to the django project", + ) parser.add_argument( - 'commit_id', - metavar='GIT_COMMIT_ID', - type=str, nargs='?', - help=('if specified, only migrations since this commit ' - 'will be taken into account. If not specified, ' - 'the initial repo commit will be used')) - parser.add_argument( - '--ignore-name-contains', + "commit_id", + metavar="GIT_COMMIT_ID", type=str, - nargs='?', - help='ignore migrations containing this name') + nargs="?", + help=( + "if specified, only migrations since this commit " + "will be taken into account. If not specified, " + "the initial repo commit will be used" + ), + ) parser.add_argument( - '--ignore-name', + "--ignore-name-contains", type=str, - nargs='*', - help='ignore migrations with exactly one of these names') + nargs="?", + help="ignore migrations containing this name", + ) parser.add_argument( - '--verbose', - '-v', - action='store_true', - help='print more information during execution') - parser.add_argument( - '--database', + "--ignore-name", type=str, - nargs='?', - help=('specify the database for which to generate the SQL. ' - 'Defaults to default')) + nargs="*", + help="ignore migrations with exactly one of these names", + ) + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="print more information during execution", + ) + parser.add_argument( + "--database", + type=str, + nargs="?", + help=( + "specify the database for which to generate the SQL. " "Defaults to default" + ), + ) - cache_group = parser.add_mutually_exclusive_group( - required=False) + cache_group = parser.add_mutually_exclusive_group(required=False) cache_group.add_argument( - '--cache-path', + "--cache-path", type=str, - help='specify a directory that should be used to' - 'store cache-files in.') + help="specify a directory that should be used to" "store cache-files in.", + ) cache_group.add_argument( - '--no-cache', - action='store_true', - help='don\'t use a cache') + "--no-cache", action="store_true", help="don't use a cache" + ) - incl_excl_group = parser.add_mutually_exclusive_group( - required=False) + incl_excl_group = parser.add_mutually_exclusive_group(required=False) incl_excl_group.add_argument( - '--include-apps', + "--include-apps", type=str, - nargs='*', - help='check only migrations that are in the specified django apps') + nargs="*", + help="check only migrations that are in the specified django apps", + ) incl_excl_group.add_argument( - '--exclude-apps', + "--exclude-apps", type=str, - nargs='*', - help='ignore migrations that are in the specified django apps') + nargs="*", + help="ignore migrations that are in the specified django apps", + ) args = parser.parse_args() if args.verbose: - logging.basicConfig(format='%(message)s', level=logging.DEBUG) + logging.basicConfig(format="%(message)s", level=logging.DEBUG) else: - logging.basicConfig(format='%(message)s') + logging.basicConfig(format="%(message)s") folder_name = args.django_folder[0] # Create and use linter @@ -332,14 +344,13 @@ def _main(): exclude_apps=args.exclude_apps, database=args.database, cache_path=args.cache_path, - no_cache=args.no_cache + no_cache=args.no_cache, ) - linter.lint_all_migrations( - git_commit_id=args.commit_id) + linter.lint_all_migrations(git_commit_id=args.commit_id) linter.print_summary() if linter.has_errors: sys.exit(1) -if __name__ == '__main__': +if __name__ == "__main__": _main() diff --git a/django_migration_linter/operations.py b/django_migration_linter/operations.py index cf98135..cea05c0 100644 --- a/django_migration_linter/operations.py +++ b/django_migration_linter/operations.py @@ -26,12 +26,10 @@ class IgnoreMigration(Operation): def state_forwards(self, app_label, state): pass - def database_forwards( - self, app_label, schema_editor, from_state, to_state): + def database_forwards(self, app_label, schema_editor, from_state, to_state): schema_editor.execute(IGNORE_MIGRATION_SQL) - def database_backwards( - self, app_label, schema_editor, from_state, to_state): + def database_backwards(self, app_label, schema_editor, from_state, to_state): schema_editor.execute(IGNORE_MIGRATION_SQL + "(reversed)") def describe(self): diff --git a/django_migration_linter/sql_analyser.py b/django_migration_linter/sql_analyser.py index fa0a0d9..4e13244 100644 --- a/django_migration_linter/sql_analyser.py +++ b/django_migration_linter/sql_analyser.py @@ -23,55 +23,58 @@ logger = logging.getLogger(__name__) 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'), - None, - ) + if re.search("SET DEFAULT", sql) and kwargs["errors"]: + err = next((err for err in kwargs["errors"] if err["code"] == "NOT_NULL"), None) if err: logger.info( - ('Found a NOT_NULL error in migration, ' - 'but it has a default value added: {}').format(err) + ( + "Found a NOT_NULL error in migration, " + "but it has a default value added: {}" + ).format(err) ) - kwargs['errors'].remove(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': 'ALTER_COLUMN', - 'fn': lambda sql, **kw: re.search('ALTER TABLE .* MODIFY', sql), - 'err_msg': ('ALTERING columns (Could be backward compatible. ' - 'You may ignore this migration.)') - }, { - 'code': '', - 'fn': has_default, - 'err_msg': '' - }, { - 'code': IGNORED_MIGRATION, - 'fn': lambda sql, **kw: re.search(IGNORE_MIGRATION_SQL, sql), - 'err_msg': '', - } + "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": "ALTER_COLUMN", + "fn": lambda sql, **kw: re.search("ALTER TABLE .* MODIFY", sql), + "err_msg": ( + "ALTERING columns (Could be backward compatible. " + "You may ignore this migration.)" + ), + }, + {"code": "", "fn": has_default, "err_msg": ""}, + { + "code": IGNORED_MIGRATION, + "fn": lambda sql, **kw: re.search(IGNORE_MIGRATION_SQL, sql), + "err_msg": "", + }, ) @@ -80,32 +83,23 @@ def analyse_sql_statements(sql_statements): ignored = False for statement in sql_statements: for test in migration_tests: - if test['fn'](statement, errors=errors): - if test['code'] == IGNORED_MIGRATION: - logger.info( - 'Testing {0} -- IGNORING MIGRATION'.format(statement) - ) + if test["fn"](statement, errors=errors): + if test["code"] == IGNORED_MIGRATION: + logger.info("Testing {0} -- IGNORING MIGRATION".format(statement)) ignored = True else: - logger.info('Testing {0} -- ERROR'.format(statement)) + logger.info("Testing {0} -- ERROR".format(statement)) table_search = re.search( - 'TABLE `([^`]*)`', statement, re.IGNORECASE) - col_search = re.search( - 'COLUMN `([^`]*)`', statement, re.IGNORECASE) + "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 + "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: - logger.info('Testing {0} -- PASSED'.format(statement)) - return { - 'errors': errors, - 'ignored': ignored - } + logger.info("Testing {0} -- PASSED".format(statement)) + return {"errors": errors, "ignored": ignored} diff --git a/django_migration_linter/utils.py b/django_migration_linter/utils.py index 355df5f..d3a88d6 100644 --- a/django_migration_linter/utils.py +++ b/django_migration_linter/utils.py @@ -21,12 +21,12 @@ from django_migration_linter.constants import MIGRATION_FOLDER_NAME def is_django_project(path): - django_manage_file = os.path.join(path, 'manage.py') + 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') + git_directory = os.path.join(path, ".git") return os.path.isdir(git_directory) @@ -44,33 +44,31 @@ def find_project_settings_module(path): """ 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') + 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] + return (split_path(a) if (len(a) > 0 and a != "/") else []) + [b] def split_migration_path(migration_path): decomposed_path = split_path(migration_path) for i, p in enumerate(decomposed_path): if p == MIGRATION_FOLDER_NAME: - return (decomposed_path[i-1], - os.path.splitext(decomposed_path[i+1])[0]) + return (decomposed_path[i - 1], os.path.splitext(decomposed_path[i + 1])[0]) def compose_migration_path(django_folder, app_name, migration): return os.path.join( - django_folder, - app_name, - MIGRATION_FOLDER_NAME, - '{0}.py'.format(migration) + django_folder, app_name, MIGRATION_FOLDER_NAME, "{0}.py".format(migration) ) def clean_bytes_to_str(byte_input): - return byte_input.decode('utf-8').strip() + return byte_input.decode("utf-8").strip() From 1e2068eed58f951d518738d8b8ee0660cdaf6d8c Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Mon, 24 Dec 2018 16:06:06 +0100 Subject: [PATCH 29/46] Add black to lint tests and increase line length for flake8 --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 2d713aa..81a15c9 100644 --- a/tox.ini +++ b/tox.ini @@ -20,5 +20,7 @@ deps = basepython = python3.6 deps = flake8 + black commands = - flake8 django_migration_linter + flake8 --max-line-length=88 django_migration_linter + black --check django_migration_linter From 952e596d9216141e5a14f729800f39345221dc39 Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Mon, 24 Dec 2018 18:22:34 +0100 Subject: [PATCH 30/46] Bump copyright version to 2019 in files --- django_migration_linter/__init__.py | 2 +- django_migration_linter/cache.py | 2 +- django_migration_linter/constants.py | 2 +- django_migration_linter/migration_linter.py | 2 +- django_migration_linter/operations.py | 2 +- django_migration_linter/sql_analyser.py | 2 +- django_migration_linter/utils.py | 2 +- examples/example_test_integration.py | 2 +- setup.py | 2 +- tests/fixtures.py | 2 +- tests/functional/test_cache.py | 2 +- tests/functional/test_cmd_line_call.py | 2 +- tests/functional/test_migration_linter.py | 2 +- tests/functional/test_multiple_calls.py | 2 +- tests/unit/test_linter.py | 2 +- tests/unit/test_utils.py | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/django_migration_linter/__init__.py b/django_migration_linter/__init__.py index a548f66..4060d62 100644 --- a/django_migration_linter/__init__.py +++ b/django_migration_linter/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2018 3YOURMIND GmbH +# Copyright 2019 3YOURMIND GmbH # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/django_migration_linter/cache.py b/django_migration_linter/cache.py index 61d48a6..1bdb318 100644 --- a/django_migration_linter/cache.py +++ b/django_migration_linter/cache.py @@ -1,4 +1,4 @@ -# Copyright 2018 3YOURMIND GmbH +# Copyright 2019 3YOURMIND GmbH # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/django_migration_linter/constants.py b/django_migration_linter/constants.py index a95e8dc..67c9e53 100644 --- a/django_migration_linter/constants.py +++ b/django_migration_linter/constants.py @@ -1,4 +1,4 @@ -# Copyright 2018 3YOURMIND GmbH +# Copyright 2019 3YOURMIND GmbH # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index d88f504..e00b6a0 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -1,4 +1,4 @@ -# Copyright 2018 3YOURMIND GmbH +# Copyright 2019 3YOURMIND GmbH # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/django_migration_linter/operations.py b/django_migration_linter/operations.py index cea05c0..0262061 100644 --- a/django_migration_linter/operations.py +++ b/django_migration_linter/operations.py @@ -1,4 +1,4 @@ -# Copyright 2018 3YOURMIND GmbH +# Copyright 2019 3YOURMIND GmbH # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/django_migration_linter/sql_analyser.py b/django_migration_linter/sql_analyser.py index 4e13244..de54f43 100644 --- a/django_migration_linter/sql_analyser.py +++ b/django_migration_linter/sql_analyser.py @@ -1,4 +1,4 @@ -# Copyright 2018 3YOURMIND GmbH +# Copyright 2019 3YOURMIND GmbH # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/django_migration_linter/utils.py b/django_migration_linter/utils.py index d3a88d6..1efc5d7 100644 --- a/django_migration_linter/utils.py +++ b/django_migration_linter/utils.py @@ -1,4 +1,4 @@ -# Copyright 2018 3YOURMIND GmbH +# Copyright 2019 3YOURMIND GmbH # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/example_test_integration.py b/examples/example_test_integration.py index 5cfd59f..aa8f576 100644 --- a/examples/example_test_integration.py +++ b/examples/example_test_integration.py @@ -1,4 +1,4 @@ -# Copyright 2018 3YOURMIND GmbH +# Copyright 2019 3YOURMIND GmbH # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/setup.py b/setup.py index 834d85d..cc19827 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# Copyright 2018 3YOURMIND GmbH +# Copyright 2019 3YOURMIND GmbH # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/fixtures.py b/tests/fixtures.py index 7f8622a..04a8114 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,4 +1,4 @@ -# Copyright 2018 3YOURMIND GmbH +# Copyright 2019 3YOURMIND GmbH # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index ae289b2..99024da 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -1,4 +1,4 @@ -# Copyright 2018 3YOURMIND GmbH +# Copyright 2019 3YOURMIND GmbH # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/functional/test_cmd_line_call.py b/tests/functional/test_cmd_line_call.py index b8ddabe..59e28d9 100644 --- a/tests/functional/test_cmd_line_call.py +++ b/tests/functional/test_cmd_line_call.py @@ -1,4 +1,4 @@ -# Copyright 2018 3YOURMIND GmbH +# Copyright 2019 3YOURMIND GmbH # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/functional/test_migration_linter.py b/tests/functional/test_migration_linter.py index 2c9acc5..42d3cc8 100644 --- a/tests/functional/test_migration_linter.py +++ b/tests/functional/test_migration_linter.py @@ -1,4 +1,4 @@ -# Copyright 2018 3YOURMIND GmbH +# Copyright 2019 3YOURMIND GmbH # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/functional/test_multiple_calls.py b/tests/functional/test_multiple_calls.py index 0920c1b..b22f623 100644 --- a/tests/functional/test_multiple_calls.py +++ b/tests/functional/test_multiple_calls.py @@ -1,4 +1,4 @@ -# Copyright 2018 3YOURMIND GmbH +# Copyright 2019 3YOURMIND GmbH # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/unit/test_linter.py b/tests/unit/test_linter.py index 1647b10..5b3c4a5 100644 --- a/tests/unit/test_linter.py +++ b/tests/unit/test_linter.py @@ -1,4 +1,4 @@ -# Copyright 2018 3YOURMIND GmbH +# Copyright 2019 3YOURMIND GmbH # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 7ac89d5..28fb363 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,4 +1,4 @@ -# Copyright 2018 3YOURMIND GmbH +# Copyright 2019 3YOURMIND GmbH # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 99534e4eb9fc02d8ea072b2616209b91d7191df3 Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Thu, 3 Jan 2019 22:13:37 +0100 Subject: [PATCH 31/46] Bump version to 0.1.0 and set to dev status Beta --- README.rst | 2 +- django_migration_linter/constants.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 9c47e3f..fa7554f 100644 --- a/README.rst +++ b/README.rst @@ -51,7 +51,7 @@ Usage Examples -------- -3YOURMIND is running the linter on `Bitbucket Pipelines`_ on every build getting pushed. +3YOURMIND is running the linter on every build getting pushed through CI. Checkout the ``examples/`` folder to see how you could integrate the linter in your test suite. Ignoring migrations diff --git a/django_migration_linter/constants.py b/django_migration_linter/constants.py index 67c9e53..052e74e 100644 --- a/django_migration_linter/constants.py +++ b/django_migration_linter/constants.py @@ -14,7 +14,7 @@ from appdirs import user_cache_dir -__version__ = "0.0.7" +__version__ = "0.1.0" MIGRATION_FOLDER_NAME = "migrations" diff --git a/setup.py b/setup.py index cc19827..9e896cb 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( keywords='django migration lint linter database backward compatibility', classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Environment :: Web Environment', 'Framework :: Django', From 574af4b7fefc35b822815f80a9eb460589b103c5 Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Fri, 4 Jan 2019 21:39:58 +0100 Subject: [PATCH 32/46] Do not check if .git folder exists git commands will fail anyway if not present --- django_migration_linter/migration_linter.py | 10 ++-------- django_migration_linter/utils.py | 5 ----- tests/unit/test_utils.py | 16 +--------------- 3 files changed, 3 insertions(+), 28 deletions(-) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index e00b6a0..999843f 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -134,12 +134,6 @@ class MigrationLinter(object): 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() @@ -178,7 +172,7 @@ class MigrationLinter(object): it allows to seperate the instances correctly. """ sqlmigrate_command = ( - "cd {0} && " "{1} manage.py sqlmigrate {2} {3} " "--database {4}" + "cd {0} && {1} manage.py sqlmigrate {2} {3} " "--database {4}" ).format( self.django_path, self.python_exe, app_name, migration_name, self.database ) @@ -205,7 +199,7 @@ class MigrationLinter(object): migrations = [] # Get changes since specified commit git_diff_command = ( - "cd {0} && " "git diff --name-only --diff-filter=A {1}" + "cd {0} && git diff --name-only --diff-filter=A {1}" ).format(self.django_path, git_commit_id) logger.info("Executing {0}".format(git_diff_command)) diff_process = Popen(git_diff_command, shell=True, stdout=PIPE, stderr=PIPE) diff --git a/django_migration_linter/utils.py b/django_migration_linter/utils.py index 1efc5d7..5d34d1b 100644 --- a/django_migration_linter/utils.py +++ b/django_migration_linter/utils.py @@ -25,11 +25,6 @@ def is_django_project(path): 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) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 28fb363..b997288 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -16,25 +16,11 @@ import os import unittest from tests import fixtures -from django_migration_linter.utils import is_django_project, is_git_project, is_directory, find_project_settings_module, \ +from django_migration_linter.utils import is_django_project, is_directory, find_project_settings_module, \ split_path, split_migration_path class UtilityFunctionTest(unittest.TestCase): - def tearDown(self, *args, **kwargs): - fixtures.clear_all_git_projects() - super( - UtilityFunctionTest, - self).tearDown(*args, **kwargs) - - def test_detect_not_git_project(self): - self.assertFalse(is_git_project(fixtures.NOT_GIT_DJANGO_PROJECT)) - - def test_detect_git_project(self): - project_path = fixtures.MULTI_COMMIT_PROJECT - fixtures.prepare_git_project(project_path) - self.assertTrue(is_git_project(project_path)) - def test_detect_django_project(self): self.assertTrue(is_django_project(fixtures.ADD_NOT_NULL_COLUMN_PROJECT)) From f0021dc13991a8d1f53542fee2ad4aed197a6235 Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Fri, 4 Jan 2019 22:00:19 +0100 Subject: [PATCH 33/46] Add test migration in folder --- .../test_correct_project/apps/__init__.py | 0 .../apps/test_app3/__init__.py | 0 .../apps/test_app3/admin.py | 3 +++ .../apps/test_app3/apps.py | 5 +++++ .../apps/test_app3/migrations/0001_initial.py | 21 +++++++++++++++++++ .../apps/test_app3/migrations/__init__.py | 0 .../apps/test_app3/models.py | 4 ++++ .../apps/test_app3/tests.py | 3 +++ .../apps/test_app3/views.py | 3 +++ .../test_project/settings.py | 5 ++++- tests/unit/test_linter.py | 3 ++- 11 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 tests/test_project_fixtures/test_correct_project/apps/__init__.py create mode 100644 tests/test_project_fixtures/test_correct_project/apps/test_app3/__init__.py create mode 100644 tests/test_project_fixtures/test_correct_project/apps/test_app3/admin.py create mode 100644 tests/test_project_fixtures/test_correct_project/apps/test_app3/apps.py create mode 100644 tests/test_project_fixtures/test_correct_project/apps/test_app3/migrations/0001_initial.py create mode 100644 tests/test_project_fixtures/test_correct_project/apps/test_app3/migrations/__init__.py create mode 100644 tests/test_project_fixtures/test_correct_project/apps/test_app3/models.py create mode 100644 tests/test_project_fixtures/test_correct_project/apps/test_app3/tests.py create mode 100644 tests/test_project_fixtures/test_correct_project/apps/test_app3/views.py diff --git a/tests/test_project_fixtures/test_correct_project/apps/__init__.py b/tests/test_project_fixtures/test_correct_project/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_project_fixtures/test_correct_project/apps/test_app3/__init__.py b/tests/test_project_fixtures/test_correct_project/apps/test_app3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_project_fixtures/test_correct_project/apps/test_app3/admin.py b/tests/test_project_fixtures/test_correct_project/apps/test_app3/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/tests/test_project_fixtures/test_correct_project/apps/test_app3/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/tests/test_project_fixtures/test_correct_project/apps/test_app3/apps.py b/tests/test_project_fixtures/test_correct_project/apps/test_app3/apps.py new file mode 100644 index 0000000..7a1fd83 --- /dev/null +++ b/tests/test_project_fixtures/test_correct_project/apps/test_app3/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TestApp3Config(AppConfig): + name = 'test_app3' diff --git a/tests/test_project_fixtures/test_correct_project/apps/test_app3/migrations/0001_initial.py b/tests/test_project_fixtures/test_correct_project/apps/test_app3/migrations/0001_initial.py new file mode 100644 index 0000000..beaa780 --- /dev/null +++ b/tests/test_project_fixtures/test_correct_project/apps/test_app3/migrations/0001_initial.py @@ -0,0 +1,21 @@ +# Generated by Django 2.1.4 on 2019-01-04 20:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='C', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('not_null_field', models.IntegerField()), + ], + ), + ] diff --git a/tests/test_project_fixtures/test_correct_project/apps/test_app3/migrations/__init__.py b/tests/test_project_fixtures/test_correct_project/apps/test_app3/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_project_fixtures/test_correct_project/apps/test_app3/models.py b/tests/test_project_fixtures/test_correct_project/apps/test_app3/models.py new file mode 100644 index 0000000..0124f42 --- /dev/null +++ b/tests/test_project_fixtures/test_correct_project/apps/test_app3/models.py @@ -0,0 +1,4 @@ +from django.db import models + +class C(models.Model): + not_null_field = models.IntegerField(null=False) diff --git a/tests/test_project_fixtures/test_correct_project/apps/test_app3/tests.py b/tests/test_project_fixtures/test_correct_project/apps/test_app3/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/tests/test_project_fixtures/test_correct_project/apps/test_app3/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/tests/test_project_fixtures/test_correct_project/apps/test_app3/views.py b/tests/test_project_fixtures/test_correct_project/apps/test_app3/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/tests/test_project_fixtures/test_correct_project/apps/test_app3/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/tests/test_project_fixtures/test_correct_project/test_project/settings.py b/tests/test_project_fixtures/test_correct_project/test_project/settings.py index f0642cc..8145e44 100644 --- a/tests/test_project_fixtures/test_correct_project/test_project/settings.py +++ b/tests/test_project_fixtures/test_correct_project/test_project/settings.py @@ -11,10 +11,12 @@ https://docs.djangoproject.com/en/1.11/ref/settings/ """ import os +import sys # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(BASE_DIR, 'apps')) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ @@ -38,7 +40,8 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'test_app1', - 'test_app2' + 'test_app2', + 'apps.test_app3', ] MIDDLEWARE = [ diff --git a/tests/unit/test_linter.py b/tests/unit/test_linter.py index 5b3c4a5..d9cbdaa 100644 --- a/tests/unit/test_linter.py +++ b/tests/unit/test_linter.py @@ -80,12 +80,13 @@ class TestLinterFunctions(unittest.TestCase): def test_gather_all_migrations(self): linter = MigrationLinter(fixtures.CORRECT_PROJECT) migrations = linter._gather_all_migrations() - self.assertEqual(len(migrations), 3) + self.assertEqual(len(migrations), 4) self.assertEqual( sorted(migrations), sorted([ ("test_app1", "0001_initial"), ("test_app1", "0002_a_new_null_field"), ("test_app2", "0001_foo"), + ("test_app3", "0001_initial"), ]) ) From c6df7e53a0abd0e74b7dfa107c0dbb6de474f5ff Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Sat, 5 Jan 2019 16:59:36 +0100 Subject: [PATCH 34/46] Add Migration class to hold the path, app name and migration name in one object + adapt existing logic --- django_migration_linter/cache.py | 5 ++--- django_migration_linter/migration.py | 7 +++++++ django_migration_linter/migration_linter.py | 16 ++++++++-------- 3 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 django_migration_linter/migration.py diff --git a/django_migration_linter/cache.py b/django_migration_linter/cache.py index 1bdb318..0beedcf 100644 --- a/django_migration_linter/cache.py +++ b/django_migration_linter/cache.py @@ -16,7 +16,7 @@ import hashlib import os import pickle -from django_migration_linter.utils import split_path, compose_migration_path +from django_migration_linter.utils import split_path class Cache(dict): @@ -42,8 +42,7 @@ class Cache(dict): with open(self.filename, "wb") as f: pickle.dump(self, f, protocol=2) - def md5(self, app_name, migration): - path = compose_migration_path(self.django_folder, app_name, migration) + def md5(self, path): hash_md5 = hashlib.md5() with open(path, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): diff --git a/django_migration_linter/migration.py b/django_migration_linter/migration.py new file mode 100644 index 0000000..5909f69 --- /dev/null +++ b/django_migration_linter/migration.py @@ -0,0 +1,7 @@ +from django_migration_linter.utils import split_migration_path + + +class Migration(object): + def __init__(self, abs_path): + self.abs_path = abs_path + self.app_name, self.name = split_migration_path(self.abs_path) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index e00b6a0..d707535 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -21,8 +21,8 @@ import sys from django_migration_linter.cache import Cache from django_migration_linter.constants import DEFAULT_CACHE_PATH, MIGRATION_FOLDER_NAME -from django_migration_linter.utils import split_migration_path from . import utils +from .migration import Migration from .sql_analyser import analyse_sql_statements logger = logging.getLogger(__name__) @@ -72,11 +72,13 @@ class MigrationLinter(object): if not self.no_cache: self.old_cache.load() - def lint_migration(self, app_name, migration_name): + def lint_migration(self, migration): + app_name = migration.app_name + migration_name = migration.name print("({0}, {1})... ".format(app_name, migration_name), end="") self.nb_total += 1 - md5hash = self.old_cache.md5(app_name, migration_name) + md5hash = self.old_cache.md5(migration.abs_path) if md5hash in self.old_cache: if self.old_cache[md5hash]["result"] == "IGNORE": print("IGNORE (cached)") @@ -146,7 +148,7 @@ class MigrationLinter(object): # Lint those migrations for m in migrations: - self.lint_migration(*m) + self.lint_migration(m) if not self.no_cache: self.new_cache.save() @@ -215,8 +217,7 @@ class MigrationLinter(object): re.search(r"\/{0}\/.*\.py".format(MIGRATION_FOLDER_NAME), line) and "__init__" not in line ): - app_name, migration_name = split_migration_path(line) - migrations.append((app_name, migration_name)) + migrations.append(Migration(line)) diff_process.wait() if diff_process.returncode != 0: @@ -237,8 +238,7 @@ class MigrationLinter(object): and file_name != "__init__.py" ): full_migration_path = os.path.join(root, file_name) - app_name, migration_name = split_migration_path(full_migration_path) - migrations.append((app_name, migration_name)) + migrations.append(Migration(full_migration_path)) return migrations def should_ignore_migration(self, app_name, migration_name): From 7a34ac901075334ea8e8009c8587089e722c704a Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Sat, 5 Jan 2019 17:39:09 +0100 Subject: [PATCH 35/46] Fix tests and git migration gathering --- django_migration_linter/migration.py | 5 +++++ django_migration_linter/migration_linter.py | 2 +- tests/functional/test_cmd_line_call.py | 25 ++++++++++++--------- tests/functional/test_multiple_calls.py | 10 +++++---- tests/unit/test_linter.py | 13 +++++++---- 5 files changed, 36 insertions(+), 19 deletions(-) diff --git a/django_migration_linter/migration.py b/django_migration_linter/migration.py index 5909f69..004f64c 100644 --- a/django_migration_linter/migration.py +++ b/django_migration_linter/migration.py @@ -5,3 +5,8 @@ class Migration(object): def __init__(self, abs_path): self.abs_path = abs_path self.app_name, self.name = split_migration_path(self.abs_path) + + def __lt__(self, other): + if not isinstance(other, Migration): + return NotImplemented + return self.abs_path < other.abs_path diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index d707535..dee83f6 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -217,7 +217,7 @@ class MigrationLinter(object): re.search(r"\/{0}\/.*\.py".format(MIGRATION_FOLDER_NAME), line) and "__init__" not in line ): - migrations.append(Migration(line)) + migrations.append(Migration(os.path.join(self.django_path, line))) diff_process.wait() if diff_process.returncode != 0: diff --git a/tests/functional/test_cmd_line_call.py b/tests/functional/test_cmd_line_call.py index 59e28d9..055d210 100644 --- a/tests/functional/test_cmd_line_call.py +++ b/tests/functional/test_cmd_line_call.py @@ -36,13 +36,14 @@ class CallLinterFromCommandLineTest(unittest.TestCase): process.wait() self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) - self.assertEqual(len(lines), 5) + self.assertEqual(len(lines), 6) self.assertEqual( - sorted(lines[:3]), + sorted(lines[:4]), sorted([ "(test_app1, 0001_initial)... OK", "(test_app1, 0002_a_new_null_field)... OK", "(test_app2, 0001_foo)... OK", + "(test_app3, 0001_initial)... OK", ]) ) @@ -71,13 +72,14 @@ class CallLinterFromCommandLineTest(unittest.TestCase): process.wait() self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) - self.assertEqual(len(lines), 5) + self.assertEqual(len(lines), 6) self.assertEqual( - sorted(lines[:3]), + sorted(lines[:4]), sorted([ "(test_app1, 0001_initial)... OK", "(test_app1, 0002_a_new_null_field)... OK", "(test_app2, 0001_foo)... IGNORE", + "(test_app3, 0001_initial)... OK", ]) ) @@ -91,13 +93,14 @@ class CallLinterFromCommandLineTest(unittest.TestCase): process.wait() self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) - self.assertEqual(len(lines), 5) + self.assertEqual(len(lines), 6) self.assertEqual( - sorted(lines[:3]), + sorted(lines[:4]), sorted([ "(test_app1, 0001_initial)... IGNORE", "(test_app1, 0002_a_new_null_field)... IGNORE", "(test_app2, 0001_foo)... OK", + "(test_app3, 0001_initial)... IGNORE", ]) ) @@ -111,13 +114,14 @@ class CallLinterFromCommandLineTest(unittest.TestCase): process.wait() self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) - self.assertEqual(len(lines), 5) + self.assertEqual(len(lines), 6) self.assertEqual( - sorted(lines[:3]), + sorted(lines[:4]), sorted([ "(test_app1, 0001_initial)... IGNORE", "(test_app1, 0002_a_new_null_field)... OK", "(test_app2, 0001_foo)... OK", + "(test_app3, 0001_initial)... IGNORE", ]) ) @@ -131,13 +135,14 @@ class CallLinterFromCommandLineTest(unittest.TestCase): process.wait() self.assertEqual(process.returncode, 0) lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) - self.assertEqual(len(lines), 5) + self.assertEqual(len(lines), 6) self.assertEqual( - sorted(lines[:3]), + sorted(lines[:4]), sorted([ "(test_app1, 0001_initial)... IGNORE", "(test_app1, 0002_a_new_null_field)... OK", "(test_app2, 0001_foo)... IGNORE", + "(test_app3, 0001_initial)... IGNORE", ]) ) diff --git a/tests/functional/test_multiple_calls.py b/tests/functional/test_multiple_calls.py index b22f623..e58a473 100644 --- a/tests/functional/test_multiple_calls.py +++ b/tests/functional/test_multiple_calls.py @@ -13,8 +13,10 @@ # limitations under the License. import unittest +import os from django_migration_linter import MigrationLinter +from django_migration_linter.migration import Migration from tests import fixtures @@ -24,10 +26,10 @@ class TestMultipleLinters(unittest.TestCase): l2 = MigrationLinter(fixtures.RENAME_COLUMN_PROJECT) l3 = MigrationLinter(fixtures.CORRECT_PROJECT) - l1.lint_migration('test_app', '0002_add_new_not_null_field') - l2.lint_migration('test_app', '0002_rename_column') - l3.lint_migration('test_app1', '0001_initial') - l3.lint_migration('test_app1', '0002_a_new_null_field') + l1.lint_migration(Migration(os.path.join(fixtures.ADD_NOT_NULL_COLUMN_PROJECT, 'test_app/migrations/0002_add_new_not_null_field.py'))) + l2.lint_migration(Migration(os.path.join(fixtures.RENAME_COLUMN_PROJECT, 'test_app/migrations/0002_rename_column.py'))) + l3.lint_migration(Migration(os.path.join(fixtures.CORRECT_PROJECT, 'test_app1/migrations/0001_initial.py'))) + l3.lint_migration(Migration(os.path.join(fixtures.CORRECT_PROJECT, 'test_app1/migrations/0002_a_new_null_field.py'))) self.assertTrue(l1.has_errors) self.assertTrue(l2.has_errors) diff --git a/tests/unit/test_linter.py b/tests/unit/test_linter.py index d9cbdaa..25d1a65 100644 --- a/tests/unit/test_linter.py +++ b/tests/unit/test_linter.py @@ -13,8 +13,10 @@ # limitations under the License. import unittest +import os from django_migration_linter import MigrationLinter +from django_migration_linter.migration import Migration from tests import fixtures @@ -32,13 +34,16 @@ class TestLinterFunctions(unittest.TestCase): linter = MigrationLinter(project_path) self.assertFalse(linter.has_errors) - linter.lint_migration('test_app', '0001_create_table') + m = Migration(os.path.join(project_path, 'test_app/migrations/0001_create_table.py')) + linter.lint_migration(m) self.assertFalse(linter.has_errors) - linter.lint_migration('test_app', '0002_add_new_not_null_field') + m = Migration(os.path.join(project_path, 'test_app/migrations/0002_add_new_not_null_field.py')) + linter.lint_migration(m) self.assertTrue(linter.has_errors) - linter.lint_migration('test_app', '0001_create_table') + m = Migration(os.path.join(project_path, 'test_app/migrations/0001_create_table.py')) + linter.lint_migration(m) self.assertTrue(linter.has_errors) def test_linter_creation(self): @@ -82,7 +87,7 @@ class TestLinterFunctions(unittest.TestCase): migrations = linter._gather_all_migrations() self.assertEqual(len(migrations), 4) self.assertEqual( - sorted(migrations), + sorted([(m.app_name, m.name) for m in migrations]), sorted([ ("test_app1", "0001_initial"), ("test_app1", "0002_a_new_null_field"), From a159e296771d3d45d0012cff2dc95c958cd3717d Mon Sep 17 00:00:00 2001 From: Felix Date: Sat, 5 Jan 2019 19:35:12 +0100 Subject: [PATCH 36/46] Print a better git error to the console --- django_migration_linter/migration_linter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index 999843f..a392881 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -217,7 +217,7 @@ class MigrationLinter(object): output = [] for line in map(utils.clean_bytes_to_str, diff_process.stderr.readlines()): output.append(line) - logger.info("Error while git diff command:\n{}".format("".join(output))) + logger.error("Error while git diff command:\n{}".format("".join(output))) raise Exception("Error while executing git diff command") return migrations From 92a686f13bc152c6c102d495861675a97d420498 Mon Sep 17 00:00:00 2001 From: Felix Date: Sat, 5 Jan 2019 19:50:22 +0100 Subject: [PATCH 37/46] Make imports relative --- django_migration_linter/cache.py | 2 +- django_migration_linter/migration_linter.py | 20 +++++++++----------- django_migration_linter/sql_analyser.py | 2 +- django_migration_linter/utils.py | 2 +- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/django_migration_linter/cache.py b/django_migration_linter/cache.py index 0beedcf..909d45f 100644 --- a/django_migration_linter/cache.py +++ b/django_migration_linter/cache.py @@ -16,7 +16,7 @@ import hashlib import os import pickle -from django_migration_linter.utils import split_path +from .utils import split_path class Cache(dict): diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index dee83f6..21b847a 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -19,10 +19,10 @@ import re from subprocess import Popen, PIPE import sys -from django_migration_linter.cache import Cache -from django_migration_linter.constants import DEFAULT_CACHE_PATH, MIGRATION_FOLDER_NAME -from . import utils +from .cache import Cache +from .constants import DEFAULT_CACHE_PATH, MIGRATION_FOLDER_NAME from .migration import Migration +from .utils import is_directory, is_django_project, is_git_project, clean_bytes_to_str from .sql_analyser import analyse_sql_statements logger = logging.getLogger(__name__) @@ -31,13 +31,13 @@ logger = logging.getLogger(__name__) class MigrationLinter(object): def __init__(self, project_path, **kwargs): # Verify correctness - if not utils.is_directory(project_path): + if not 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): + if not is_django_project(project_path): raise ValueError( ("The given path {0} does not " "seem to be a django project.").format( project_path @@ -136,7 +136,7 @@ class MigrationLinter(object): def lint_all_migrations(self, git_commit_id=None): # Collect migrations if git_commit_id: - if not utils.is_git_project(self.django_path): + if not is_git_project(self.django_path): raise ValueError( ( "The given project {0} does not seem " "to be versioned by git." @@ -190,9 +190,7 @@ class MigrationLinter(object): ) sql_statements = [] - for line in map( - utils.clean_bytes_to_str, sqlmigrate_process.stdout.readlines() - ): + for line in map(clean_bytes_to_str, sqlmigrate_process.stdout.readlines()): sql_statements.append(line) sqlmigrate_process.wait() if sqlmigrate_process.returncode != 0: @@ -211,7 +209,7 @@ class MigrationLinter(object): ).format(self.django_path, git_commit_id) logger.info("Executing {0}".format(git_diff_command)) diff_process = Popen(git_diff_command, shell=True, stdout=PIPE, stderr=PIPE) - for line in map(utils.clean_bytes_to_str, diff_process.stdout.readlines()): + for line in map(clean_bytes_to_str, diff_process.stdout.readlines()): # Only gather lines that include added migrations if ( re.search(r"\/{0}\/.*\.py".format(MIGRATION_FOLDER_NAME), line) @@ -222,7 +220,7 @@ class MigrationLinter(object): if diff_process.returncode != 0: output = [] - for line in map(utils.clean_bytes_to_str, diff_process.stderr.readlines()): + for line in map(clean_bytes_to_str, diff_process.stderr.readlines()): output.append(line) logger.info("Error while git diff command:\n{}".format("".join(output))) raise Exception("Error while executing git diff command") diff --git a/django_migration_linter/sql_analyser.py b/django_migration_linter/sql_analyser.py index de54f43..44b5246 100644 --- a/django_migration_linter/sql_analyser.py +++ b/django_migration_linter/sql_analyser.py @@ -15,7 +15,7 @@ import re import logging -from django_migration_linter.operations import IGNORE_MIGRATION_SQL +from .operations import IGNORE_MIGRATION_SQL IGNORED_MIGRATION = "IGNORED_MIGRATION" diff --git a/django_migration_linter/utils.py b/django_migration_linter/utils.py index 1efc5d7..da5dfdc 100644 --- a/django_migration_linter/utils.py +++ b/django_migration_linter/utils.py @@ -17,7 +17,7 @@ from __future__ import print_function import os import sys -from django_migration_linter.constants import MIGRATION_FOLDER_NAME +from .constants import MIGRATION_FOLDER_NAME def is_django_project(path): From ab5536c8e73ce749597ac0428df6042ba358fac8 Mon Sep 17 00:00:00 2001 From: Felix Date: Mon, 7 Jan 2019 12:25:57 +0100 Subject: [PATCH 38/46] Remove is_git_project from import --- django_migration_linter/migration_linter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index 26a0ffb..032d910 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -22,7 +22,7 @@ import sys from .cache import Cache from .constants import DEFAULT_CACHE_PATH, MIGRATION_FOLDER_NAME from .migration import Migration -from .utils import is_directory, is_django_project, is_git_project, clean_bytes_to_str +from .utils import is_directory, is_django_project, clean_bytes_to_str from .sql_analyser import analyse_sql_statements logger = logging.getLogger(__name__) From f52bb3d9b834fbcacca8fe8820365733a539fcda Mon Sep 17 00:00:00 2001 From: David-Wobrock Date: Mon, 7 Jan 2019 19:37:51 +0100 Subject: [PATCH 39/46] Bump version to 0.1.1 --- django_migration_linter/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_migration_linter/constants.py b/django_migration_linter/constants.py index 052e74e..8180696 100644 --- a/django_migration_linter/constants.py +++ b/django_migration_linter/constants.py @@ -14,7 +14,7 @@ from appdirs import user_cache_dir -__version__ = "0.1.0" +__version__ = "0.1.1" MIGRATION_FOLDER_NAME = "migrations" From c14f9aef37ccabebb6d9e267fde72398332830cb Mon Sep 17 00:00:00 2001 From: Victor Seva Date: Thu, 10 Jan 2019 14:38:48 +0100 Subject: [PATCH 40/46] don't assume git root is django_path > DJANGO_SETTINGS_MODULE=xpbx.test django-migration-linter --no-cache $(pwd)/xpbx origin/2.4.1 > (customers, 0010_auto_20181026_1330)... Traceback (most recent call last): > File "/home/vseva/Env/xpbx/bin/django-migration-linter", line 11, in > sys.exit(_main()) > File "/home/vseva/Env/xpbx/lib/python3.6/site-packages/django_migration_linter/migration_linter.py", line 341, in _main > linter.lint_all_migrations(git_commit_id=args.commit_id) > File "/home/vseva/Env/xpbx/lib/python3.6/site-packages/django_migration_linter/migration_linter.py", line 145, in lint_all_migrations > self.lint_migration(m) > File "/home/vseva/Env/xpbx/lib/python3.6/site-packages/django_migration_linter/migration_linter.py", line 81, in lint_migration > md5hash = self.old_cache.md5(migration.abs_path) > File "/home/vseva/Env/xpbx/lib/python3.6/site-packages/django_migration_linter/cache.py", line 47, in md5 > with open(path, "rb") as f: > FileNotFoundError: [Errno 2] No such file or directory: '/home/vseva/Foehn/xpbx/xpbx/xpbx/customers/migrations/0010_auto_20181026_1330.py' Fix: #37 --- django_migration_linter/migration_linter.py | 22 ++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index 032d910..6e71e53 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -195,8 +195,28 @@ class MigrationLinter(object): logger.info("Found {0} sql migration lines".format(len(sql_statements))) return sql_statements + def _get_git_root(self): + git_root = self.django_path + command = "cd {0} && git rev-parse --show-toplevel".format(self.django_path) + logger.info("Executing {0}".format(command)) + diff_process = Popen(command, shell=True, stdout=PIPE, stderr=PIPE) + for line in map(clean_bytes_to_str, diff_process.stdout.readlines()): + git_root = line + diff_process.wait() + + if diff_process.returncode != 0: + output = [] + for line in map(clean_bytes_to_str, diff_process.stderr.readlines()): + output.append(line) + logger.error( + "Error while git rev-parse command:\n{}".format("".join(output)) + ) + raise Exception("Error while executing git rev-parse command") + return git_root + def _gather_migrations_git(self, git_commit_id): migrations = [] + git_root = self._get_git_root() # Get changes since specified commit git_diff_command = ( "cd {0} && git diff --name-only --diff-filter=A {1}" @@ -209,7 +229,7 @@ class MigrationLinter(object): re.search(r"\/{0}\/.*\.py".format(MIGRATION_FOLDER_NAME), line) and "__init__" not in line ): - migrations.append(Migration(os.path.join(self.django_path, line))) + migrations.append(Migration(os.path.join(git_root, line))) diff_process.wait() if diff_process.returncode != 0: From dca50b3afb3975dcfffd2602cc7b816544838d42 Mon Sep 17 00:00:00 2001 From: Dat Hoang Date: Tue, 15 Jan 2019 20:52:26 +0700 Subject: [PATCH 41/46] Make sure file paths from git diff are relative to django_path instead of git repos root path Currently, the git diff command return file paths which are relative to git repos root path, which will cause invalid migration file path if git repos root path is different from django_path --- django_migration_linter/migration_linter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index 6e71e53..ff2d726 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -219,7 +219,7 @@ class MigrationLinter(object): git_root = self._get_git_root() # Get changes since specified commit git_diff_command = ( - "cd {0} && git diff --name-only --diff-filter=A {1}" + "cd {0} && git diff --relative --name-only --diff-filter=A {1}" ).format(self.django_path, git_commit_id) logger.info("Executing {0}".format(git_diff_command)) diff_process = Popen(git_diff_command, shell=True, stdout=PIPE, stderr=PIPE) From 75c969c6a9ae288660341a392a85cdcdfcc2962b Mon Sep 17 00:00:00 2001 From: David Wobrock Date: Sun, 20 Jan 2019 11:46:07 +0100 Subject: [PATCH 42/46] Revert "don't assume git root is django_path" This reverts commit c14f9aef37ccabebb6d9e267fde72398332830cb. --- django_migration_linter/migration_linter.py | 22 +-------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index ff2d726..808be46 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -195,28 +195,8 @@ class MigrationLinter(object): logger.info("Found {0} sql migration lines".format(len(sql_statements))) return sql_statements - def _get_git_root(self): - git_root = self.django_path - command = "cd {0} && git rev-parse --show-toplevel".format(self.django_path) - logger.info("Executing {0}".format(command)) - diff_process = Popen(command, shell=True, stdout=PIPE, stderr=PIPE) - for line in map(clean_bytes_to_str, diff_process.stdout.readlines()): - git_root = line - diff_process.wait() - - if diff_process.returncode != 0: - output = [] - for line in map(clean_bytes_to_str, diff_process.stderr.readlines()): - output.append(line) - logger.error( - "Error while git rev-parse command:\n{}".format("".join(output)) - ) - raise Exception("Error while executing git rev-parse command") - return git_root - def _gather_migrations_git(self, git_commit_id): migrations = [] - git_root = self._get_git_root() # Get changes since specified commit git_diff_command = ( "cd {0} && git diff --relative --name-only --diff-filter=A {1}" @@ -229,7 +209,7 @@ class MigrationLinter(object): re.search(r"\/{0}\/.*\.py".format(MIGRATION_FOLDER_NAME), line) and "__init__" not in line ): - migrations.append(Migration(os.path.join(git_root, line))) + migrations.append(Migration(os.path.join(self.django_path, line))) diff_process.wait() if diff_process.returncode != 0: From 009153fbf4c99ba2efd7b4d6c039e7e330d7a50e Mon Sep 17 00:00:00 2001 From: David Wobrock Date: Sat, 19 Jan 2019 17:21:12 +0100 Subject: [PATCH 43/46] handle linting a project in pwd When calling "django-migration-linter ." be able to handle it (especially with cache) --- django_migration_linter/cache.py | 2 +- django_migration_linter/migration_linter.py | 2 +- django_migration_linter/utils.py | 2 +- tests/functional/test_cmd_line_call.py | 21 +++++++++++++++++++++ tests/unit/test_utils.py | 6 ++++++ 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/django_migration_linter/cache.py b/django_migration_linter/cache.py index 909d45f..892e2df 100644 --- a/django_migration_linter/cache.py +++ b/django_migration_linter/cache.py @@ -22,7 +22,7 @@ from .utils import split_path class Cache(dict): def __init__(self, django_folder, cache_path): self.django_folder = django_folder - project_name = split_path(django_folder)[-2] + project_name = split_path(django_folder)[-1] self.filename = os.path.join(cache_path, "{0}.pickle".format(project_name)) if not os.path.exists(os.path.dirname(self.filename)): diff --git a/django_migration_linter/migration_linter.py b/django_migration_linter/migration_linter.py index 808be46..f9c0ab1 100644 --- a/django_migration_linter/migration_linter.py +++ b/django_migration_linter/migration_linter.py @@ -45,7 +45,7 @@ class MigrationLinter(object): ) # Store parameters and options - self.django_path = project_path + self.django_path = os.path.abspath(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) diff --git a/django_migration_linter/utils.py b/django_migration_linter/utils.py index 0f50c26..e358593 100644 --- a/django_migration_linter/utils.py +++ b/django_migration_linter/utils.py @@ -49,7 +49,7 @@ def find_project_settings_module(path): def split_path(path): a, b = os.path.split(path) - return (split_path(a) if (len(a) > 0 and a != "/") else []) + [b] + return (split_path(a) if (len(a) > 0 and a != "/") else []) + ([b] if b else []) def split_migration_path(migration_path): diff --git a/tests/functional/test_cmd_line_call.py b/tests/functional/test_cmd_line_call.py index 055d210..1077375 100644 --- a/tests/functional/test_cmd_line_call.py +++ b/tests/functional/test_cmd_line_call.py @@ -220,3 +220,24 @@ class CallLinterFromCommandLineTest(unittest.TestCase): self.assertEqual(len(lines), 3) self.assertTrue(lines[0].endswith('OK')) self.assertTrue(lines[1].startswith('*** Summary')) + + def test_call_from_within_project(self): + cmd = 'cd {0} && {1} --no-cache .'.format( + fixtures.CORRECT_PROJECT, + self.linter_exec) + + process = Popen( + cmd, shell=True, stdout=PIPE, stderr=PIPE) + process.wait() + self.assertEqual(process.returncode, 0) + lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) + self.assertEqual(len(lines), 6) + self.assertEqual( + sorted(lines[:4]), + sorted([ + "(test_app1, 0001_initial)... OK", + "(test_app1, 0002_a_new_null_field)... OK", + "(test_app2, 0001_foo)... OK", + "(test_app3, 0001_initial)... OK", + ]) + ) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index b997288..6998413 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -58,6 +58,12 @@ class UtilityFunctionTest(unittest.TestCase): self.assertEqual(splitted[1], 'bar') self.assertEqual(splitted[2], 'fuz.py') + def test_split_folder_path(self): + splitted = split_path('/foo/bar') + self.assertEqual(len(splitted), 2) + self.assertEqual(splitted[0], 'foo') + self.assertEqual(splitted[1], 'bar') + def test_split_migration_long_path(self): input_path = 'apps/the_app/migrations/0001_stuff.py' app, mig = split_migration_path(input_path) From 1a9024e799fea53ea66120e54e66b92caf012dee Mon Sep 17 00:00:00 2001 From: David Wobrock Date: Sun, 20 Jan 2019 11:44:54 +0100 Subject: [PATCH 44/46] add test for git on non root location --- tests/fixtures.py | 11 +- tests/functional/test_cmd_line_call.py | 33 ++++ .../django_project/manage.py | 22 +++ .../django_project/test_app1/__init__.py | 0 .../django_project/test_app1/admin.py | 3 + .../django_project/test_app1/apps.py | 5 + .../test_app1/migrations/0001_initial.py | 23 +++ .../migrations/0002_a_null_field_2.py | 20 +++ .../migrations/0003_a_non_null_field.py | 20 +++ .../test_app1/migrations/__init__.py | 0 .../django_project/test_app1/models.py | 6 + .../django_project/test_app1/tests.py | 3 + .../django_project/test_app1/views.py | 3 + .../django_project/test_project/__init__.py | 0 .../django_project/test_project/settings.py | 122 +++++++++++++ .../django_project/test_project/urls.py | 21 +++ .../django_project/test_project/wsgi.py | 16 ++ .../git/COMMIT_EDITMSG | 1 + .../test_non_root_git_project/git/HEAD | 1 + .../test_non_root_git_project/git/config | 5 + .../test_non_root_git_project/git/description | 1 + .../git/hooks/applypatch-msg.sample | 15 ++ .../git/hooks/commit-msg.sample | 24 +++ .../git/hooks/fsmonitor-watchman.sample | 114 ++++++++++++ .../git/hooks/post-update.sample | 8 + .../git/hooks/pre-applypatch.sample | 14 ++ .../git/hooks/pre-commit.sample | 49 +++++ .../git/hooks/pre-push.sample | 53 ++++++ .../git/hooks/pre-rebase.sample | 169 ++++++++++++++++++ .../git/hooks/pre-receive.sample | 24 +++ .../git/hooks/prepare-commit-msg.sample | 42 +++++ .../git/hooks/update.sample | 128 +++++++++++++ .../test_non_root_git_project/git/index | Bin 0 -> 1797 bytes .../git/info/exclude | 6 + .../test_non_root_git_project/git/logs/HEAD | 6 + .../git/logs/refs/heads/master | 6 + .../01/40e142724fc58944797f9ddc2ebf964146339a | 2 + .../0e/52dc2380789e401cf385d3376af39f4bb6598f | Bin 0 -> 131 bytes .../15/d47f23b30965a0ef668af1e639a23634e4532a | Bin 0 -> 274 bytes .../16/8e382963b2ef0de835c22c48c2bf3a702e83a0 | Bin 0 -> 149 bytes .../1c/dc834ac63fc5edefbbdcabf264026ccc8cf4a7 | Bin 0 -> 123 bytes .../1d/c36fd270ce97d05f4fa75c3c16917951d71768 | Bin 0 -> 92 bytes .../21/a03f36ba451433c9fefe71d9fd05e80cefe458 | Bin 0 -> 122 bytes .../29/56309db5fd77d3474462f613653d73787f214f | Bin 0 -> 240 bytes .../2c/46f438a89972f9ce2d5151a825a5bfb7f7db4b | 4 + .../2c/5613c15e0a45ad460b4973dcdf942846b77b72 | Bin 0 -> 132 bytes .../2f/3fa93ad05c33e1d0ccda111d881e192bccbc87 | Bin 0 -> 225 bytes .../30/b505e031fc496c88d36be5ed7892337c640223 | 2 + .../33/90cc679ea02b9ad1a57dc6ef711255b4f4d90c | Bin 0 -> 451 bytes .../41/07cf9a6c4633c97bd7ee2a73353c88deca64a4 | Bin 0 -> 122 bytes .../43/4e403e49cc1e2037d29695e0cb6477f20acc2e | Bin 0 -> 56 bytes .../4a/d7b8980d409199892bb78d1c77164fc6446993 | Bin 0 -> 316 bytes .../4b/0dddde301971fc19f1817a22bf0948890ccacc | Bin 0 -> 123 bytes .../4c/f37def1d191c7c4b34e1053fa923e6d649525a | Bin 0 -> 123 bytes .../4d/00aeaddee6c0c490e5018fa1d40b3fb4fd55a4 | Bin 0 -> 240 bytes .../56/26f87a33217c62ed70006bda0735aaa8578ee7 | Bin 0 -> 158 bytes .../57/97dee69dcb584dede25d7c02d34caf96d70236 | Bin 0 -> 109 bytes .../59/e2ac235a403799063c7e7f2a137c462ac17ba2 | Bin 0 -> 583 bytes .../5c/f2c8f1e59f99ac2de981abf2b6adf6279ea3da | Bin 0 -> 239 bytes .../6b/f17bf7104d256ef447c26ecb37124331d29cae | Bin 0 -> 91 bytes .../73/5de5db8bc5e1ed29b3cb7ece62b5e7c8db06ee | Bin 0 -> 391 bytes .../7c/dae90d4510701438cf671bb67772202f825891 | 1 + .../7c/e503c2dd97ba78597f6ff6e4393132753573f6 | Bin 0 -> 72 bytes .../81/45e446f728f9ed6ba04294b4c880be74383949 | Bin 0 -> 1285 bytes .../82/bc1ebd64ca46c16193d8f8407127f0f3e4164c | 2 + .../84/8eee04718d10f32f59ff9ae63afd8478ef68c4 | Bin 0 -> 57 bytes .../88/c4bb3d2e2aa1968f90484f0b6cd77b7c4f7c57 | Bin 0 -> 122 bytes .../89/5be6939590f9199877bf41d9fe2ca0f5f501eb | Bin 0 -> 122 bytes .../8c/38f3f3dad51e4585f3984282c2a4bec5349c1e | Bin 0 -> 77 bytes .../8f/255685e632623f016e1ecf18a4e9d58746e1ce | Bin 0 -> 90 bytes .../8f/be1b0a394f7e215f9cf6d48e8bfdc3b00e1c80 | Bin 0 -> 276 bytes .../91/3c6cc338b8f9d2530f21dd94b939fbc42cf1c4 | Bin 0 -> 56 bytes .../91/ea44a218fbd2f408430959283f0419c921093e | Bin 0 -> 77 bytes .../98/6913f97e6222f14be4db7d10e90430cc43cc4d | Bin 0 -> 1268 bytes .../9b/353778696f76f0288affbc392eeefa4df70e90 | Bin 0 -> 331 bytes .../9b/b51574d23f6566590cf6bb9ce31868466be03f | 5 + .../9d/1dcfdaf1a6857c5f83dc27019c7600e1ffaff8 | Bin 0 -> 56 bytes .../a3/f20003e5407a44000854c41ba29c06a3bbba82 | Bin 0 -> 132 bytes .../aa/66ac1debe2ae5fd39cd8e65f6dfd9786649490 | Bin 0 -> 284 bytes .../b8/da44fc3f868ffbd94e347927f138be8b5b5b2d | Bin 0 -> 56 bytes .../bd/fc3d5864d2ad429269f80d739ef58ded2d4b35 | Bin 0 -> 195 bytes .../bf/a71e37ed417bd9acf5ef4a44975f7bfd10341a | Bin 0 -> 56 bytes .../cb/96855f9aa7aae003123eca1addad870c7b8fde | Bin 0 -> 154 bytes .../e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 | Bin 0 -> 15 bytes .../ea/9048c5860ddad87cb377574bd627fa4a7ccd4c | Bin 0 -> 56 bytes .../f1/22245e6a3ce63b99e0beeb8403031edd08ed50 | 1 + .../f1/ff8f0c6cca456e2e132263fd70ac9593ac1b53 | Bin 0 -> 241 bytes .../f6/7bb4d66d0955079a1365ba91ae91671796ce85 | Bin 0 -> 1373 bytes .../f6/a170bb234332d6ecef3b4348f2da0e758c59a7 | Bin 0 -> 149 bytes .../fb/3701d6fd9f5fa295ab981f9fac08c3156045d2 | Bin 0 -> 82 bytes .../git/refs/heads/master | 1 + 91 files changed, 991 insertions(+), 1 deletion(-) create mode 100644 tests/test_project_fixtures/test_non_root_git_project/django_project/manage.py create mode 100644 tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/__init__.py create mode 100644 tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/admin.py create mode 100644 tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/apps.py create mode 100644 tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/migrations/0001_initial.py create mode 100644 tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/migrations/0002_a_null_field_2.py create mode 100644 tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/migrations/0003_a_non_null_field.py create mode 100644 tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/migrations/__init__.py create mode 100644 tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/models.py create mode 100644 tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/tests.py create mode 100644 tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/views.py create mode 100644 tests/test_project_fixtures/test_non_root_git_project/django_project/test_project/__init__.py create mode 100644 tests/test_project_fixtures/test_non_root_git_project/django_project/test_project/settings.py create mode 100644 tests/test_project_fixtures/test_non_root_git_project/django_project/test_project/urls.py create mode 100644 tests/test_project_fixtures/test_non_root_git_project/django_project/test_project/wsgi.py create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/COMMIT_EDITMSG create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/HEAD create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/config create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/description create mode 100755 tests/test_project_fixtures/test_non_root_git_project/git/hooks/applypatch-msg.sample create mode 100755 tests/test_project_fixtures/test_non_root_git_project/git/hooks/commit-msg.sample create mode 100755 tests/test_project_fixtures/test_non_root_git_project/git/hooks/fsmonitor-watchman.sample create mode 100755 tests/test_project_fixtures/test_non_root_git_project/git/hooks/post-update.sample create mode 100755 tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-applypatch.sample create mode 100755 tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-commit.sample create mode 100755 tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-push.sample create mode 100755 tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-rebase.sample create mode 100755 tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-receive.sample create mode 100755 tests/test_project_fixtures/test_non_root_git_project/git/hooks/prepare-commit-msg.sample create mode 100755 tests/test_project_fixtures/test_non_root_git_project/git/hooks/update.sample create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/index create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/info/exclude create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/logs/HEAD create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/logs/refs/heads/master create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/01/40e142724fc58944797f9ddc2ebf964146339a create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/0e/52dc2380789e401cf385d3376af39f4bb6598f create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/15/d47f23b30965a0ef668af1e639a23634e4532a create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/16/8e382963b2ef0de835c22c48c2bf3a702e83a0 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/1c/dc834ac63fc5edefbbdcabf264026ccc8cf4a7 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/1d/c36fd270ce97d05f4fa75c3c16917951d71768 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/21/a03f36ba451433c9fefe71d9fd05e80cefe458 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/29/56309db5fd77d3474462f613653d73787f214f create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/2c/46f438a89972f9ce2d5151a825a5bfb7f7db4b create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/2c/5613c15e0a45ad460b4973dcdf942846b77b72 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/2f/3fa93ad05c33e1d0ccda111d881e192bccbc87 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/30/b505e031fc496c88d36be5ed7892337c640223 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/33/90cc679ea02b9ad1a57dc6ef711255b4f4d90c create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/41/07cf9a6c4633c97bd7ee2a73353c88deca64a4 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/43/4e403e49cc1e2037d29695e0cb6477f20acc2e create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/4a/d7b8980d409199892bb78d1c77164fc6446993 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/4b/0dddde301971fc19f1817a22bf0948890ccacc create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/4c/f37def1d191c7c4b34e1053fa923e6d649525a create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/4d/00aeaddee6c0c490e5018fa1d40b3fb4fd55a4 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/56/26f87a33217c62ed70006bda0735aaa8578ee7 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/57/97dee69dcb584dede25d7c02d34caf96d70236 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/59/e2ac235a403799063c7e7f2a137c462ac17ba2 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/5c/f2c8f1e59f99ac2de981abf2b6adf6279ea3da create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/6b/f17bf7104d256ef447c26ecb37124331d29cae create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/73/5de5db8bc5e1ed29b3cb7ece62b5e7c8db06ee create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/7c/dae90d4510701438cf671bb67772202f825891 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/7c/e503c2dd97ba78597f6ff6e4393132753573f6 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/81/45e446f728f9ed6ba04294b4c880be74383949 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/82/bc1ebd64ca46c16193d8f8407127f0f3e4164c create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/84/8eee04718d10f32f59ff9ae63afd8478ef68c4 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/88/c4bb3d2e2aa1968f90484f0b6cd77b7c4f7c57 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/89/5be6939590f9199877bf41d9fe2ca0f5f501eb create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/8c/38f3f3dad51e4585f3984282c2a4bec5349c1e create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/8f/255685e632623f016e1ecf18a4e9d58746e1ce create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/8f/be1b0a394f7e215f9cf6d48e8bfdc3b00e1c80 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/91/3c6cc338b8f9d2530f21dd94b939fbc42cf1c4 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/91/ea44a218fbd2f408430959283f0419c921093e create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/98/6913f97e6222f14be4db7d10e90430cc43cc4d create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/9b/353778696f76f0288affbc392eeefa4df70e90 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/9b/b51574d23f6566590cf6bb9ce31868466be03f create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/9d/1dcfdaf1a6857c5f83dc27019c7600e1ffaff8 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/a3/f20003e5407a44000854c41ba29c06a3bbba82 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/aa/66ac1debe2ae5fd39cd8e65f6dfd9786649490 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/b8/da44fc3f868ffbd94e347927f138be8b5b5b2d create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/bd/fc3d5864d2ad429269f80d739ef58ded2d4b35 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/bf/a71e37ed417bd9acf5ef4a44975f7bfd10341a create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/cb/96855f9aa7aae003123eca1addad870c7b8fde create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/ea/9048c5860ddad87cb377574bd627fa4a7ccd4c create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/f1/22245e6a3ce63b99e0beeb8403031edd08ed50 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/f1/ff8f0c6cca456e2e132263fd70ac9593ac1b53 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/f6/7bb4d66d0955079a1365ba91ae91671796ce85 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/f6/a170bb234332d6ecef3b4348f2da0e758c59a7 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/objects/fb/3701d6fd9f5fa295ab981f9fac08c3156045d2 create mode 100644 tests/test_project_fixtures/test_non_root_git_project/git/refs/heads/master diff --git a/tests/fixtures.py b/tests/fixtures.py index 04a8114..81fb577 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -48,10 +48,19 @@ NOT_DJANGO_GIT_PROJECT = os.path.join( NOT_GIT_DJANGO_PROJECT = os.path.join( _FIXTURES_FOLDER, 'test_django_without_git_project/') +NON_GIT_ROOT_GIT_FOLDER = os.path.join( + _FIXTURES_FOLDER, 'test_non_root_git_project/' +) +NON_GIT_ROOT_DJANGO_PROJECT = os.path.join( + NON_GIT_ROOT_GIT_FOLDER, 'django_project/' +) + + ALL_GIT_PROJECTS = ( NOT_DJANGO_GIT_PROJECT, MULTI_COMMIT_PROJECT, - DELETED_MIGRATION_PROJECT + DELETED_MIGRATION_PROJECT, + NON_GIT_ROOT_GIT_FOLDER, ) diff --git a/tests/functional/test_cmd_line_call.py b/tests/functional/test_cmd_line_call.py index 1077375..a2861fa 100644 --- a/tests/functional/test_cmd_line_call.py +++ b/tests/functional/test_cmd_line_call.py @@ -241,3 +241,36 @@ class CallLinterFromCommandLineTest(unittest.TestCase): "(test_app3, 0001_initial)... OK", ]) ) + + def test_call_project_non_git_root(self): + # could use tag: version_ok (but doesn't work in Travis) + cmd = '{0} --no-cache {1} 2c46f438a89972f9ce2d5151a825a5bfb7f7db4b'.format( + self.linter_exec, + fixtures.NON_GIT_ROOT_DJANGO_PROJECT) + fixtures.prepare_git_project(fixtures.NON_GIT_ROOT_GIT_FOLDER) + + process = Popen( + cmd, shell=True, stdout=PIPE, stderr=PIPE) + process.wait() + self.assertEqual(process.returncode, 0) + lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) + self.assertEqual(len(lines), 3) + self.assertTrue(lines[0].endswith('OK')) + self.assertTrue(lines[1].startswith('*** Summary')) + + def test_call_project_non_git_root_ko(self): + # could use tag: version_ko (but doesn't work in Travis) + cmd = '{0} --no-cache {1} 0140e142724fc58944797f9ddc2ebf964146339a'.format( + self.linter_exec, + fixtures.NON_GIT_ROOT_DJANGO_PROJECT) + fixtures.prepare_git_project(fixtures.NON_GIT_ROOT_GIT_FOLDER) + + process = Popen( + cmd, shell=True, stdout=PIPE, stderr=PIPE) + process.wait() + self.assertNotEqual(process.returncode, 0) + lines = list(map(utils.clean_bytes_to_str, process.stdout.readlines())) + self.assertEqual(len(lines), 5) + self.assertTrue(lines[0].endswith('ERR')) + self.assertTrue(lines[2].endswith('OK')) + self.assertTrue(lines[3].startswith('*** Summary')) diff --git a/tests/test_project_fixtures/test_non_root_git_project/django_project/manage.py b/tests/test_project_fixtures/test_non_root_git_project/django_project/manage.py new file mode 100644 index 0000000..3390cc6 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/django_project/manage.py @@ -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) diff --git a/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/__init__.py b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/admin.py b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/apps.py b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/apps.py new file mode 100644 index 0000000..1dc36fd --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TestApp1Config(AppConfig): + name = 'test_app1' diff --git a/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/migrations/0001_initial.py b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/migrations/0001_initial.py new file mode 100644 index 0000000..9bb5157 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/migrations/0001_initial.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2019-01-20 10:31 +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()), + ], + ), + ] diff --git a/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/migrations/0002_a_null_field_2.py b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/migrations/0002_a_null_field_2.py new file mode 100644 index 0000000..15d47f2 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/migrations/0002_a_null_field_2.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2019-01-20 10:32 +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='null_field_2', + field=models.IntegerField(default=1), + ), + ] diff --git a/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/migrations/0003_a_non_null_field.py b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/migrations/0003_a_non_null_field.py new file mode 100644 index 0000000..8fbe1b0 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/migrations/0003_a_non_null_field.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2019-01-20 10:33 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('test_app1', '0002_a_null_field_2'), + ] + + operations = [ + migrations.AddField( + model_name='a', + name='non_null_field', + field=models.IntegerField(null=True), + ), + ] diff --git a/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/migrations/__init__.py b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/models.py b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/models.py new file mode 100644 index 0000000..1cdc834 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/models.py @@ -0,0 +1,6 @@ +from django.db import models + +class A(models.Model): + null_field = models.IntegerField() + null_field_2 = models.IntegerField(default=1) + non_null_field = models.IntegerField(null=True) diff --git a/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/tests.py b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/views.py b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_app1/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/tests/test_project_fixtures/test_non_root_git_project/django_project/test_project/__init__.py b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_project_fixtures/test_non_root_git_project/django_project/test_project/settings.py b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_project/settings.py new file mode 100644 index 0000000..986913f --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_project/settings.py @@ -0,0 +1,122 @@ +""" +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 +import sys + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +sys.path.insert(0, os.path.join(BASE_DIR, 'apps')) + +# 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', +] + +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/' diff --git a/tests/test_project_fixtures/test_non_root_git_project/django_project/test_project/urls.py b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_project/urls.py new file mode 100644 index 0000000..735de5d --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_project/urls.py @@ -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), +] diff --git a/tests/test_project_fixtures/test_non_root_git_project/django_project/test_project/wsgi.py b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_project/wsgi.py new file mode 100644 index 0000000..aa66ac1 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/django_project/test_project/wsgi.py @@ -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() diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/COMMIT_EDITMSG b/tests/test_project_fixtures/test_non_root_git_project/git/COMMIT_EDITMSG new file mode 100644 index 0000000..46d3912 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/COMMIT_EDITMSG @@ -0,0 +1 @@ +Add ok migration diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/HEAD b/tests/test_project_fixtures/test_non_root_git_project/git/HEAD new file mode 100644 index 0000000..cb089cd --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/config b/tests/test_project_fixtures/test_non_root_git_project/git/config new file mode 100644 index 0000000..515f483 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/config @@ -0,0 +1,5 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/description b/tests/test_project_fixtures/test_non_root_git_project/git/description new file mode 100644 index 0000000..498b267 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/hooks/applypatch-msg.sample b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/applypatch-msg.sample new file mode 100755 index 0000000..a5d7b84 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/hooks/commit-msg.sample b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/commit-msg.sample new file mode 100755 index 0000000..b58d118 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/hooks/fsmonitor-watchman.sample b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/fsmonitor-watchman.sample new file mode 100755 index 0000000..e673bb3 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/fsmonitor-watchman.sample @@ -0,0 +1,114 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 1) and a time in nanoseconds +# formatted as a string and outputs to stdout all files that have been +# modified since the given time. Paths must be relative to the root of +# the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $time) = @ARGV; + +# Check the hook interface version + +if ($version == 1) { + # convert nanoseconds to seconds + $time = int $time / 1000000000; +} else { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree; +if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $git_work_tree = Win32::GetCwd(); + $git_work_tree =~ tr/\\/\//; +} else { + require Cwd; + $git_work_tree = Cwd::cwd(); +} + +my $retry = 1; + +launch_watchman(); + +sub launch_watchman { + + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $time but were not transient (ie created after + # $time but no longer exist). + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + # + # The category of transient files that we want to ignore will have a + # creation clock (cclock) newer than $time_t value and will also not + # currently exist. + + my $query = <<" END"; + ["query", "$git_work_tree", { + "since": $time, + "fields": ["name"], + "expression": ["not", ["allof", ["since", $time, "cclock"], ["not", "exists"]]] + }] + END + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + my $json_pkg; + eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; + } or do { + require JSON::PP; + $json_pkg = "JSON::PP"; + }; + + my $o = $json_pkg->new->utf8->decode($response); + + if ($retry > 0 and $o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) { + print STDERR "Adding '$git_work_tree' to watchman's watch list.\n"; + $retry--; + qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + print "/\0"; + eval { launch_watchman() }; + exit 0; + } + + die "Watchman: $o->{error}.\n" . + "Falling back to scanning...\n" if $o->{error}; + + binmode STDOUT, ":utf8"; + local $, = "\0"; + print @{$o->{files}}; +} diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/hooks/post-update.sample b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/post-update.sample new file mode 100755 index 0000000..ec17ec1 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-applypatch.sample b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-applypatch.sample new file mode 100755 index 0000000..4142082 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-commit.sample b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-commit.sample new file mode 100755 index 0000000..68d62d5 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-push.sample b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-push.sample new file mode 100755 index 0000000..6187dbf --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +z40=0000000000000000000000000000000000000000 + +while read local_ref local_sha remote_ref remote_sha +do + if [ "$local_sha" = $z40 ] + then + # Handle delete + : + else + if [ "$remote_sha" = $z40 ] + then + # New branch, examine all commits + range="$local_sha" + else + # Update to existing branch, examine new commits + range="$remote_sha..$local_sha" + fi + + # Check for WIP commit + commit=`git rev-list -n 1 --grep '^WIP' "$range"` + if [ -n "$commit" ] + then + echo >&2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-rebase.sample b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-rebase.sample new file mode 100755 index 0000000..6cbef5c --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-receive.sample b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-receive.sample new file mode 100755 index 0000000..a1fd29e --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/hooks/prepare-commit-msg.sample b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/prepare-commit-msg.sample new file mode 100755 index 0000000..10fa14c --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/prepare-commit-msg.sample @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/hooks/update.sample b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/update.sample new file mode 100755 index 0000000..80ba941 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/hooks/update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to block unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --bool hooks.allowunannotated) +allowdeletebranch=$(git config --bool hooks.allowdeletebranch) +denycreatebranch=$(git config --bool hooks.denycreatebranch) +allowdeletetag=$(git config --bool hooks.allowdeletetag) +allowmodifytag=$(git config --bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero="0000000000000000000000000000000000000000" +if [ "$newrev" = "$zero" ]; then + newrev_type=delete +else + newrev_type=$(git cat-file -t $newrev) +fi + +case "$refname","$newrev_type" in + refs/tags/*,commit) + # un-annotated tag + short_refname=${refname##refs/tags/} + if [ "$allowunannotated" != "true" ]; then + echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/index b/tests/test_project_fixtures/test_non_root_git_project/git/index new file mode 100644 index 0000000000000000000000000000000000000000..dbd8c5de3d828ee73f7220fc8c16c20ddbd133b9 GIT binary patch literal 1797 zcmZ?q402{*U|<4b{umcuedbt36&TIH@RyMxPo|WCp>YWV1M>?g)-s-OCVk!l?O7L> z)*gFbC=|No%S|2ziIl9wy!8C|f};Ga)Z`NV+{C=Z^i;iqN(Kg)c`zDkZaJ#C3=Gfa zKAOGh;x%XQZq0S&Q8x}f=?R|5pn_&@NosLPd}2X?p?-XPW?p7Vd_33=p!;#zQ;%kk zeUHWG&$q71xwd|u;nZ|!$-bi|bL1ElaoUrZlADMy^jE&gD%E0U584L=z%`Ye@hK7?LqafRr2Qe-X8e+b32deo@aw2lfocIL*(^OfO0-$;{6y);BOPFa(EMW?~LJ*ns}>Q_I9D# z%eYhYO1<)C&eR3()4D!Bvs`3m@+4S`!HP(Gjp7sI^Gb7a;?puyb5i1s5Y~h2kCWPV zwG>7}?e9Ue|4{!vX)a6uI>q=o->&p^|2@2cPo{yvhDiI3LH6h8A-MtW0$6;)Xo&st zS*Z5oNT2$|*pHf2f&PrrNlgfc(GdF-ywL31DRZaU>zMt~x9@k~S^X)6Dd$Yjm*osf zxb4eNNzH+zBv5X^l^+zm(d@CQdCGj~?(|(1k@flCo>&?hm6{fR!;>FCIS_0OEWBVe z#2<>SXy)5beC4u8;`gO59L}7P8ul!bClxvE@R(ngnOct9{4!N3H1n~S8|v7?3063v z$A|AN$>^!qfc^l}V1JaUN~7At6EY)H_-9>`(ns$nw`&DnvKX9kKI6-P*Pi0kl9J54 zbZDf&!U;x0?U6yXhnb@|_UY~JqYvL|Za!UiE@|uY6SvvkF(~8nPiYZy#KX*o(NKG2 zQSD)zxhidq?CVGC;xEs+@hm>~@AS5mDHHJ9Q(l~&3C}hRAwjOLz+|?T!O&E}kn7b1 zkE3n8w{FyIE)VyzL%Ahsqr>>$ZEe*`PntkpeT5u&R}Q& zG%DuPiH}d`&s?MXvT^mNZEL@&&s%&8QHn8`C>U_n+2LRnm BZOZ@v literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/info/exclude b/tests/test_project_fixtures/test_non_root_git_project/git/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/info/exclude @@ -0,0 +1,6 @@ +# 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] +# *~ diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/logs/HEAD b/tests/test_project_fixtures/test_non_root_git_project/git/logs/HEAD new file mode 100644 index 0000000..e3682ee --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/logs/HEAD @@ -0,0 +1,6 @@ +0000000000000000000000000000000000000000 30b505e031fc496c88d36be5ed7892337c640223 David Wobrock 1547980206 +0100 commit (initial): init +30b505e031fc496c88d36be5ed7892337c640223 a3f20003e5407a44000854c41ba29c06a3bbba82 David Wobrock 1547980230 +0100 commit (amend): init +a3f20003e5407a44000854c41ba29c06a3bbba82 0e52dc2380789e401cf385d3376af39f4bb6598f David Wobrock 1547980287 +0100 commit (amend): init +0e52dc2380789e401cf385d3376af39f4bb6598f 0140e142724fc58944797f9ddc2ebf964146339a David Wobrock 1547980306 +0100 commit: init migration +0140e142724fc58944797f9ddc2ebf964146339a 2c46f438a89972f9ce2d5151a825a5bfb7f7db4b David Wobrock 1547980356 +0100 commit: add ko migration +2c46f438a89972f9ce2d5151a825a5bfb7f7db4b f122245e6a3ce63b99e0beeb8403031edd08ed50 David Wobrock 1547980398 +0100 commit: Add ok migration diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/logs/refs/heads/master b/tests/test_project_fixtures/test_non_root_git_project/git/logs/refs/heads/master new file mode 100644 index 0000000..e3682ee --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/logs/refs/heads/master @@ -0,0 +1,6 @@ +0000000000000000000000000000000000000000 30b505e031fc496c88d36be5ed7892337c640223 David Wobrock 1547980206 +0100 commit (initial): init +30b505e031fc496c88d36be5ed7892337c640223 a3f20003e5407a44000854c41ba29c06a3bbba82 David Wobrock 1547980230 +0100 commit (amend): init +a3f20003e5407a44000854c41ba29c06a3bbba82 0e52dc2380789e401cf385d3376af39f4bb6598f David Wobrock 1547980287 +0100 commit (amend): init +0e52dc2380789e401cf385d3376af39f4bb6598f 0140e142724fc58944797f9ddc2ebf964146339a David Wobrock 1547980306 +0100 commit: init migration +0140e142724fc58944797f9ddc2ebf964146339a 2c46f438a89972f9ce2d5151a825a5bfb7f7db4b David Wobrock 1547980356 +0100 commit: add ko migration +2c46f438a89972f9ce2d5151a825a5bfb7f7db4b f122245e6a3ce63b99e0beeb8403031edd08ed50 David Wobrock 1547980398 +0100 commit: Add ok migration diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/01/40e142724fc58944797f9ddc2ebf964146339a b/tests/test_project_fixtures/test_non_root_git_project/git/objects/01/40e142724fc58944797f9ddc2ebf964146339a new file mode 100644 index 0000000..3b6c8db --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/objects/01/40e142724fc58944797f9ddc2ebf964146339a @@ -0,0 +1,2 @@ +x¥�Á +!E[ûîƒáéÓ…ˆýCk}ê$åbõû}BËsáî¥ZJî\*±ë-FnÒL„h¼I6H��¤Á*oÑ&OJR¤ØÃµ¸uQË@ ,ÆF‚—Ù¥QPÞÏÚšÄܳ_kãg÷Ê�_ªo•nü¾8½xZ‹Ë÷‰j9r¡Õb Ì|€�t íñËÛ¸ZòÚ\Ïuc�¿M1 \ No newline at end of file diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/0e/52dc2380789e401cf385d3376af39f4bb6598f b/tests/test_project_fixtures/test_non_root_git_project/git/objects/0e/52dc2380789e401cf385d3376af39f4bb6598f new file mode 100644 index 0000000000000000000000000000000000000000..e6f6376d2c0e895625ff49adbafb739bdc921a8b GIT binary patch literal 131 zcmV-}0DS*=0hNtG4#FT106p`Hy_+-(Y@sB^c<>LN>9Q(ONVtd3NOe* l=KH|4>_Ly@!-NJMY*8bs)vn_%|0gE@OQ^i$uHMW)JB~%wK9T?c literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/15/d47f23b30965a0ef668af1e639a23634e4532a b/tests/test_project_fixtures/test_non_root_git_project/git/objects/15/d47f23b30965a0ef668af1e639a23634e4532a new file mode 100644 index 0000000000000000000000000000000000000000..359a96671fcc9d8d5aab8fd846703707c6be9117 GIT binary patch literal 274 zcmV+t0qy>H0bP&HYQ!KEg}e4CF3iGANrI-cmp`quPPB>My=oPccn9yOl7< z56MXeBexA+FRS9mRA0qoO9;eKxS&;N2aOwa@V1L-QBaSY7jjuRErBw|xX@O6Ee$=E zyX`$A+>$O%9sU4*r9LS2q|vD2N>2%UUt#5p9hEdAlb$B4HW(wu^iwqd47=Lplj|4j Yal$ocC>&)lzT@956Yg603wynSsx{|;VgLXD literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/16/8e382963b2ef0de835c22c48c2bf3a702e83a0 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/16/8e382963b2ef0de835c22c48c2bf3a702e83a0 new file mode 100644 index 0000000000000000000000000000000000000000..0835b5a4da36b80c87d2614216de7383976b7b30 GIT binary patch literal 149 zcmV;G0BZku0V^p=O;s>7v0yMXFfcPQQHYPv%*!l^kJl@xWOz3B(d%*C%e@HGaO$Uf?uk%ZY}4B^H*RP-Ue>IbbElu}^Pz zAAR^%bMxuCb4gpDpSaET4yvTQI6V_&%Br+AvacVli@!YQ#f}nA literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/1c/dc834ac63fc5edefbbdcabf264026ccc8cf4a7 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/1c/dc834ac63fc5edefbbdcabf264026ccc8cf4a7 new file mode 100644 index 0000000000000000000000000000000000000000..e0b776b8217c9d5dc2e9929ef2c7b3da498fd364 GIT binary patch literal 123 zcmV->0EGW|0ZYosPf{?nFl9(9%Fk6u$x6&i&(}*yQpn6L$S*2U$jwhl%_-*MO3q0v zE>>{VfbjKvLBg6=T%38OIXUrZnW;G`3bs(idY*YDsp+XjZXi(&O=LCkMmSWZq^2d7 d=9JhPf|Tdy#S=6Zq`)?$s5DiR3jjd>LkdKTIr9Jj literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/1d/c36fd270ce97d05f4fa75c3c16917951d71768 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/1d/c36fd270ce97d05f4fa75c3c16917951d71768 new file mode 100644 index 0000000000000000000000000000000000000000..829448e00b18c14e48fabc40bc16c680fcfa0476 GIT binary patch literal 92 zcmV-i0HgnS0ZYosPg1ZnVn{2>&s9jtO3X{o*GnuYC|1bKEyyn_QE)6MaL&(5%S`9u y;!4g*EG||ENi8k`iW)+MHQ)*~t+*6`ATKdDRl!z4y#%N%9%z!GIu`)2m>|_GKqZ<0 literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/21/a03f36ba451433c9fefe71d9fd05e80cefe458 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/21/a03f36ba451433c9fefe71d9fd05e80cefe458 new file mode 100644 index 0000000000000000000000000000000000000000..8db52c7e55c08fc73dd96d868dd974a3c38cc35e GIT binary patch literal 122 zcmV-=0EPc}0V^p=O;s>7G-5C`FfcPQQOHfqOH5DIE2v~Jo^U38-U97e7njx^dtWFN zy5-AF9uor~P$)?)E{RVpC@^I3y1ruuufxQdo!Z-bWy;0;kGW({Mp9Eyl%JKFT*9Dl cztZYLjPb(@XKo3~cF0L;pV`w60KMWb6^&puqyPW_ literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/29/56309db5fd77d3474462f613653d73787f214f b/tests/test_project_fixtures/test_non_root_git_project/git/objects/29/56309db5fd77d3474462f613653d73787f214f new file mode 100644 index 0000000000000000000000000000000000000000..215b13db2c2b3c125adb950eb97251dc96b898d2 GIT binary patch literal 240 zcmV5HDfR|FfcPQQHYPv%*!l^kJl@xWOz3B(d9WuIY1f&|po&UTi%Y<2YMwG5x;uSWMPz;cwXJY9_vNStRlM(iaYA&PWY=7Ri%}oOS?Y9cmCa&~*F& literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/2c/46f438a89972f9ce2d5151a825a5bfb7f7db4b b/tests/test_project_fixtures/test_non_root_git_project/git/objects/2c/46f438a89972f9ce2d5151a825a5bfb7f7db4b new file mode 100644 index 0000000..a022075 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/objects/2c/46f438a89972f9ce2d5151a825a5bfb7f7db4b @@ -0,0 +1,4 @@ +x¥ŽM!ƒ]sŠ·7™ðó˜�ÄÞÁ5ð`$Ê`êõÅxÓÕ×´MC-%w�¨v½ÅÞ�CLA%3›”P`M¼ \ No newline at end of file diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/2c/5613c15e0a45ad460b4973dcdf942846b77b72 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/2c/5613c15e0a45ad460b4973dcdf942846b77b72 new file mode 100644 index 0000000000000000000000000000000000000000..71c146a30d7c641f5e6f863a9f241172bdaf9fcd GIT binary patch literal 132 zcmV-~0DJ#<0V^p=O;s>7HexU|FfcPQQ7|wtFpSU4%Ph%E%+V{TWSG5GwB(X~YFZ@E zx7~9dOJulZKd?tAHHuG+&nwNziBHQ+%}I$j0_hOFQm?$3Gj+lHw62fOEEk!XJPFo< m>WBxM93Kx-_H6E>*_$q2bN24mTvr}-5Ghr|^FfcPQQHYPv%*!l^kJl@xWOz3B(dsk>|2d8+&GLDdzfmXu`Xr5A&YYjl0$_Fd!W z+w28SQ?{IF*jHj<>4~W?F`Oov5?KK%$NM z+fS0yti}H5U>Mfp&wP)pI;T764s0`Rj_d^3Kfn3WXPIt6 zDT{`bg6nI@l`^QUQu(J0!eL(ogm!ETPts6}eJAsL^<}xa-KvLmS>A7MAJn()>igGq z{tj{>FY4&{k!p!0qV3-2mkB0roT!o9IyTTeqTO>*v!lYG#XT9a0;g8#iRxZRMX9RU z1Vy7dZ7I>{sivdAeX_N7j@@tg49WumRy@!%%-{$u>JD0V;Mowc9LRwq^uZmht?wOy z98khz`bF$>2HN{RV0@Hz-8e~Bsq^^Ng!^C;gaHTE4H)!4X(VE3cE+irHt@(xbBlZHB=(@}Q=s}NHB%U@_=IC}~M t2wB#m2b3^H2>T~Jb}wl+uWP6Miq?|*)a5_7XC4;6K(`+*vtKxd{a_j3=zIVG literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/41/07cf9a6c4633c97bd7ee2a73353c88deca64a4 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/41/07cf9a6c4633c97bd7ee2a73353c88deca64a4 new file mode 100644 index 0000000000000000000000000000000000000000..90e5aac9f9f46d1fb779e31a6951a5445dc93b83 GIT binary patch literal 122 zcmV-=0EPc}0V^p=O;s>7G-5C`FfcPQQOHfqOH5DIE2v~Jo^U38-U97e7njx^dtWFN zy5-AF9uor~P$)?)E{RVpC@^Hu3^SO!^>6uQcbBAZ!l|~!74?e#NNNg-^0QKtOBlW_ cEZD8=Y;^6-duwNpPq+9=dm@(u0L=d|Z&KMbeE4WH2!R0)>>U#Ju$U_=2MRtkmQZhK?h9H)Jq0FfcPQQHYPv%*!l^kJl@xWOz3B(dE~-7BFPrVmDp2BE0W8R9#|9Ze||HtR9QcpKo22b8Y=R z!>Q@el6^-_=E$L`NoLsl$2KD6(pslUnLl`o=Y8#ctLtqFQ&dn;3^GUdaQ>x&bJH)x z`!A2N5t~>UcwIch!~h5sax>G55=%1k^NJaMn=@YfJ3oHW)YUWO=da;7ESlhY32IJm zeoAT%*re=_)!zktRr9{MAIdvzF63-@Y0f%yMac}aP0cGZ^UFSHbp79BsrT-e?{~fl zP*o+V#U)^qYMwG5x;uSWMPz;cwXJY9_vNStRlM(iaYA&PWY= O7Ri%}oOS>cL77k0&6ytn literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/4b/0dddde301971fc19f1817a22bf0948890ccacc b/tests/test_project_fixtures/test_non_root_git_project/git/objects/4b/0dddde301971fc19f1817a22bf0948890ccacc new file mode 100644 index 0000000000000000000000000000000000000000..26178f21e05e0da93adb0d054f800786eb2bf271 GIT binary patch literal 123 zcmV->0EGW|0V^p=O;s>7G-5C`FfcPQQOHfqOH5DIE2v~Jo^U38-U97e7njx^dtWFN zy5-AF9uor~P$)?)E{RVpC@^I3WmvcN-m?QoCOl>AUwDPve#_s`B}i%tit@8klS>%B dEiBlr>}+)H&3kKSk59MwN_!%g0{}`iGZR`0GFAWp literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/4c/f37def1d191c7c4b34e1053fa923e6d649525a b/tests/test_project_fixtures/test_non_root_git_project/git/objects/4c/f37def1d191c7c4b34e1053fa923e6d649525a new file mode 100644 index 0000000000000000000000000000000000000000..e152596e592263793ca8a4db091c005390b84d35 GIT binary patch literal 123 zcmV->0EGW|0V^p=O;s>7G-5C`FfcPQQOHfqOH5DIE2v~Jo^U38-U97e7njx^dtWFN zy5-AF9uor~P$)?)E{RVpC@^G*`E=sr)A=*k=)P=R{b}3UZ|d_F-$GJTP?VpQnq0#0 dZDGM~WoM&nZ{AxwdwjaZSK1S~900tWHB|g%Hqih8 literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/4d/00aeaddee6c0c490e5018fa1d40b3fb4fd55a4 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/4d/00aeaddee6c0c490e5018fa1d40b3fb4fd55a4 new file mode 100644 index 0000000000000000000000000000000000000000..e213d67e7917f920f67124d0b00faad5ebd55486 GIT binary patch literal 240 zcmV5HDfR|FfcPQQHYPv%*!l^kJl@xWOz3B(docPyd&WGu^Aby5UTt@Kcn)e# zZhlH?4%npZkJaA=d{y(lxF5jGj%+zwQnu)Jm7D@cR^o7HjGg8BzMe?K~ryT&ZIBI>o>UR79 literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/56/26f87a33217c62ed70006bda0735aaa8578ee7 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/56/26f87a33217c62ed70006bda0735aaa8578ee7 new file mode 100644 index 0000000000000000000000000000000000000000..fa9f1c093f9bb4da758cb4dc285395208c86917b GIT binary patch literal 158 zcmV;P0Ac@l0ZYosPf{?nG-hD_%*z!g;2e|800oRd+5w1*S%5?e14FO|NW@PANHCxg z#aTcx{fzwFRQ;61vdk3yl&r+O^nBgi%=DtflFa-(-JHz4lGGyolGNgoVlWwBP?VpQ znp_f}mRV6!T9gVEP0lYW0*OLpAxaYq3Jmq*<1_OzOXB183MxxDfY#aM=BJeAq}qWT MRSd)o03!u8+~WC6v;Y7A literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/57/97dee69dcb584dede25d7c02d34caf96d70236 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/57/97dee69dcb584dede25d7c02d34caf96d70236 new file mode 100644 index 0000000000000000000000000000000000000000..b20e73309b43400861e00f8415301b6f4b73510a GIT binary patch literal 109 zcmV-z0FwWB0ZYosPf{>6He*OD%Fk6u$x6&i&(}*yQpn6L$S*2U$jwhl%_-*MO3q0v zE>>{VfbjKvLBg6=T%38OIXUrZnW;G`3bs(idY*YDsp+XjZXi(&O=LCkMmSWZq^2d7 P=9JhPYH|Sp2`VvR_)al; literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/59/e2ac235a403799063c7e7f2a137c462ac17ba2 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/59/e2ac235a403799063c7e7f2a137c462ac17ba2 new file mode 100644 index 0000000000000000000000000000000000000000..1d2d86cfc8e2834df239bee54e88aabe996a876e GIT binary patch literal 583 zcmV-N0=WHn0gY2%ixV*rpY6Ys6}2BB4??$a-Pi+Pii&!8cpyGheJRqt?IvwgHp!A? z&%0;AFYE`f=1+=G+|8MbU-{Zvq zR(MCidQe6nd$8(3bhmsLg|c;U#mv79erf@kksWfzn(UD%Wza<$WK{M%7qu!pye4mmI#0?FR#Fkb{t&>atMOK9(iA$Ov$#TbJ0-N z^aPuswy9^n!;120_v+!8H>KWD%%IJ?}voqLMQyls!Rd z9Yp#$dE_0vCChMpvdoQ9zK?f_HgHexd~rr>mLc*0?N&JMyx_}oj~k(z@B~%(cUgfd z^*yI7zZQ%(>1I;wnjM)nn?(I4*>HFJlx@Y0s||0hg*FDatowS$HH>&#-8x_4E2&vs V@;A>=wtitp(Ks6S{{nbOKCi&5HDfR|FfcPQQHYPv%*!l^kJl@xWOz3B(dGu&zI#;%SuvCcbi6B=P&w7Y=96NDX@y$&-qlb^z7FYk{o=b-n-q literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/6b/f17bf7104d256ef447c26ecb37124331d29cae b/tests/test_project_fixtures/test_non_root_git_project/git/objects/6b/f17bf7104d256ef447c26ecb37124331d29cae new file mode 100644 index 0000000000000000000000000000000000000000..b3da19a60283d744529763eb75d06328228ea2aa GIT binary patch literal 91 zcmV-h0HptT0ZYosPg1b3WJoK@&s9jtO3X{o*GoxK$jmLsFDg;U%}+_qDdyrz&PgmT xR&dmS@b!E_!kSiGoOz`=Iq_+ksW~YMwot`-o_Qsy>8V9-AW;oXE&x@aAn_2%D`5Zt literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/73/5de5db8bc5e1ed29b3cb7ece62b5e7c8db06ee b/tests/test_project_fixtures/test_non_root_git_project/git/objects/73/5de5db8bc5e1ed29b3cb7ece62b5e7c8db06ee new file mode 100644 index 0000000000000000000000000000000000000000..66ccc09794ebf5efe30fa53a069a0084dc53b13f GIT binary patch literal 391 zcmV;20eJp+0gX~!OT#b}^*O)dGK8*B6Xt`1f)64N2R?~DiO#f5w~QvYB&i?&-kYq2 zF;MJN8qPiE+;dWOSiy36bvB#LI&Fh;o1lEy^bYr`4)FZ=0Jmscy&aqk8ZBc_I|c89 z>v|aoZv8uS+6Qnr&}1C&5D-4J`t%%D=)j;;plyrJBns#|CA|W#RLOw8T?oA|g=nz$ zyxB{;#ZjNvXoRw2!50e=u-CO05iLmH3wHM<4Uzehtp;01dk5Xby5MlrG$1W#)1wO| zK6IR}2iNeD+>IS3L^O~N-0Pr{dS0+s_5=3-1~NL0#8UtO literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/7c/dae90d4510701438cf671bb67772202f825891 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/7c/dae90d4510701438cf671bb67772202f825891 new file mode 100644 index 0000000..f03c030 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/objects/7c/dae90d4510701438cf671bb67772202f825891 @@ -0,0 +1 @@ +x+)JMU0´0a040031Q0000ŒÏÌË,ÉLÌÑ+¨d˜½U´ä’}jZ$Ï·ÝsKd¸e?°G(5ŠOŒÏ+ÍɉOËLÍI‰7é½R¯¼™3uÁû´®�Ï,™™< ÖBè0éÈÏCÒÒÓ¿OšËÒ¿N1~η+}Ýoà“i€ê‰»&>¤ìÙÜG³7]¼æìÝ­¹®<êÆ¡'=Áå,JU \ No newline at end of file diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/7c/e503c2dd97ba78597f6ff6e4393132753573f6 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/7c/e503c2dd97ba78597f6ff6e4393132753573f6 new file mode 100644 index 0000000000000000000000000000000000000000..250bef6ac737f97163f7f541ed98f1f4581b0f67 GIT binary patch literal 72 zcmV-O0Jr~m0ZYosPf{>5U`Q*<&s9jtO3X{o*DFaaE>XzLEyyn_Q3wIDofC^wxww=S eoQqNuOHvgo^Gk~qKx&E=GE$3D^|$~d*cUrI-yGBc literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/81/45e446f728f9ed6ba04294b4c880be74383949 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/81/45e446f728f9ed6ba04294b4c880be74383949 new file mode 100644 index 0000000000000000000000000000000000000000..84e0a1301f36029398b81c56f1594ef494f0536c GIT binary patch literal 1285 zcmV+g1^W7U0j*YBbJ|7}?z4Wy+HN9yY!*{bTgNkbkU=JDxD^SzNhZ}S(gF**d3Ob+ z9sl>9)eUeUbvj`fkoN4kf9Em|;xW2vy!rWutDbh(d+r25vO)FTj2WlEauE1< z63OBS!B@^0sAU1Y04u0AlQI!E^|~K>0_RFf6TCRA)2OZ_)@7V9Pt;4h>$!Y6qiB)_ z0ScIqD4uNmB9RZ8DC1~Jf|QDHQpV|IOOpzl#=<1#phA>SVl6c71l^`A@KHkKOn{?; z`BbSbqX>2_)BzJWz>3v1Jdas)dTZFG+p>li2xEMvm9OY+b5>L<64FR+f>}dp zBvK~HIDmLVULiw%>ZyzZyG<#2VHRicO2&1cCPBPbD(c>hdJC02NGM1$o1>xSd_WJz zuxIt!H)ug=k_A*yk5dWnYk(U-5*w3c~0YJ+}2vJ#MW7eHI1R9GJTR30cO;_~zgP9a2HtJUh5fCE#-Y{$=#`F=oGgi{Q8PdSro zY!?sRidG8GTS^tU(mlK-#m-$m$h&nFT2dHCxS9REh(YiPZo<&7HV{SnFPnO0E&bLPMz1eH&Hx;ra4U+(-4IYC5H1f+~fb+TH z6yy?CnG|TCHi-E2IWL6l49Z3|R&}llaEsC(I99)>2pdB(maHoN%0{3E(TcfHLMvm{ z9rY<|u(I?^KAg3N0(v=D{`(SMP9I@V$**wlz+{hhvZ?%i%1ibNCVNDbv}aaqMffQX zDf|Ht9_+TII#tJN8Y;S9fMF%33`2pJ%?^M8-LFMpmJmVV@r9FPXC$Ba1wV}7mf;v` zd@7jbdDWqxk~EMziPp_t+v=HcR;0q6kOg&1L2iY*>LFZ<$3V|e@T5IuW)FxEtC;(0 zr(nRE&%giWfl5^^Ddg1(d4N0^7$fJMJ1}hfp+9W7_eRG87yHA#Wz;jeTP{^iP6>xS zAUD?C7>h@+6&O-YkW^Beg)AVdectEp#JtFQk>X96=-;ZS3j-}o!w!w6a`yej=l!=# z$M=`tOGC<;_Z0j>7Lj?rt6jJD}E`z)BV`*WrriD2_-aw?a^vnt8&Y?40X_HT<#t?A1gZ8YG4WH2!R0)>>U#Ju$U_=2MRtkmQZhR*0`lc!GjDLJEjzvIn+ PIt#vjWqb_)u>%z^_7xhQ literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/88/c4bb3d2e2aa1968f90484f0b6cd77b7c4f7c57 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/88/c4bb3d2e2aa1968f90484f0b6cd77b7c4f7c57 new file mode 100644 index 0000000000000000000000000000000000000000..3f19464b28fa78e38d4fb2818f47c115e506e667 GIT binary patch literal 122 zcmV-=0EPc}0V^p=O;s>7G-5C`FfcPQQOHfqOH5DIE2v~Jo^U38-U97e7njx^dtWFN zy5-AF9uor~P$)?)E{RVpC@^IB_`jbg=ag%np0HB#--0z$C$EtXMp9Eyl%JKFT*B~e cVZm-?XQOLx-dj6+e7eO~+7r1P0Qm$lN=Jk;`v3p{ literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/89/5be6939590f9199877bf41d9fe2ca0f5f501eb b/tests/test_project_fixtures/test_non_root_git_project/git/objects/89/5be6939590f9199877bf41d9fe2ca0f5f501eb new file mode 100644 index 0000000000000000000000000000000000000000..3e08383b6f4070c88ada4e43596780d1166cc1bb GIT binary patch literal 122 zcmV-=0EPc}0V^p=O;s>7G-5C`FfcPQQOHfqOH5DIE2v~Jo^U38-U97e7njx^dtWFN zy5-AF9uor~P$)?)E{RVpC@^IB_`jbg=ag%np0HB#--0z$C$EtXMp9Eyl%JKFT*4sM cXQ7$A={@fY(?dEQhxS_)=ru0@0NrgbgFT)yU;qFB literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/8c/38f3f3dad51e4585f3984282c2a4bec5349c1e b/tests/test_project_fixtures/test_non_root_git_project/git/objects/8c/38f3f3dad51e4585f3984282c2a4bec5349c1e new file mode 100644 index 0000000000000000000000000000000000000000..e68f806e4b9f3596d3c9fcc6aa1f59610147f291 GIT binary patch literal 77 zcmV-T0J8sh0R_Q94uBvG1VGQcViWINOnitx0I6C-SyS-f@14n&8S63~PD8mo literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/8f/255685e632623f016e1ecf18a4e9d58746e1ce b/tests/test_project_fixtures/test_non_root_git_project/git/objects/8f/255685e632623f016e1ecf18a4e9d58746e1ce new file mode 100644 index 0000000000000000000000000000000000000000..2299381e58688912d0b188e2eebfdf26b3fc2dbc GIT binary patch literal 90 zcmV-g0HyzU0V^p=O;xZkVlXr?Ff%bxFfcGMjL*!=EXhpF(JQEAn7viBZYak1wC$DnlIOP&B1LgY#-nw(RO zMQ2pb56R;GSV_K*hsP>^I9K5h#yrKZ`z<5}EwvTt4oj~2ii~=kRV7bX!T&5vnR2~7 ad!c|W4r@y6TD}bv{`8E%t@1bB8HSHG6NXg) literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/91/3c6cc338b8f9d2530f21dd94b939fbc42cf1c4 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/91/3c6cc338b8f9d2530f21dd94b939fbc42cf1c4 new file mode 100644 index 0000000000000000000000000000000000000000..e62ddc5d1d1d9b6ce50c986ff10fbd7844285d35 GIT binary patch literal 56 zcmV-80LTA$0V^p=O;s>4WH2!R0)>>U#Ju$U_=2MRtkmQZ25;WG_Y5Qp|44pptWw&~ O>Cwq^>I?vrbP_(xt{VUV literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/91/ea44a218fbd2f408430959283f0419c921093e b/tests/test_project_fixtures/test_non_root_git_project/git/objects/91/ea44a218fbd2f408430959283f0419c921093e new file mode 100644 index 0000000000000000000000000000000000000000..263fc8c87e325bcaa3c19492252655b82f498d2d GIT binary patch literal 77 zcmV-T0J8sh0ZYosPf{>5W=Jc_&s9jtO3X{o*DKD*FDgkcEh$#W%q;*i6^c^xQc{b! jxRe!~i&7IyQWYxmON$iBGE>Wo6*5wbQuVk1gVGqx_PZe6 literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/98/6913f97e6222f14be4db7d10e90430cc43cc4d b/tests/test_project_fixtures/test_non_root_git_project/git/objects/98/6913f97e6222f14be4db7d10e90430cc43cc4d new file mode 100644 index 0000000000000000000000000000000000000000..c2660a4b63c592c2f67a9a3e996dddd3a6188200 GIT binary patch literal 1268 zcmVT3K0a1Da~w|-#EB3zo^muv7!rgF zFJ;M`_yU#xnxb?`Oc)jXp|-lOz`T7ueqX}pqq?_0ZOr$ zaX2Wa0g+~_D1u)Lxj^|fKsC8dnzKm}q-oljQoW*g^|pYr7)Ru^f<1l5 zV=mR5dY+7>=bfFMDGE4M7y(xZ6Q`FKwZ122Rh*_=m6#LsM@IccBhM)c$dZIf8j)CR zg4sY>%rh$RID~jZULiw4=F5x%+#v)#GmEoiC6Z=9(lA*|6HR|c{Dn*&Bos6m?a|P5 zKcahmXqi^`8Z8J(^MG>VGa}%94Rixa0tq)xfTvP=q=15<;d&p8j}S?9D#qtgGUKnN zlgsmy3A_4jx%!p7J|RK0eD`p5M3?9B>$%4hyjuJ$7V|fMsr$VMl2{dpWieV&F@qS5 zvndi8i@_0K^o}n_O2@bvb-}*NGNLGYuV>#I9q+yExDKp-{-Q`DdYXpRFNz`{6B^Sz znu=*TE|&h~eqakHoI_p@i6ao> z>0z{hBl(h%wSv660NYw%5YFB=JDr{Z2{vRfb`B8HAPC6{GonF4Ge*T)bION)E&Gn& z+LjilILkro)>h~>Q4(wI{O?%+&L&zLe(oq)9R$?*f}Cu7ro%fVqGZ5#;Z@YPRrHHY z9K=R-#55nMwwPmo0exEg^Oiwhu~W%6zJrGB3~k%>M#Em)wr*t!ZI1>mV5oD@c@vFh;a7I6&f{>hfAKFx*mH@g>&+ozVdmpT86g z&)1+HWXF{2va+@$_P{l5OA0oQS90)Xloq|LjrRGkIx(lJG1sADDf~`JG!gOxhQE?!k(uA*{5XwuziLvL`HO z_+vf*#!jUQamTV>=Dm0p&&;%$9i5I&ApY9xKObB_&0)P_{$B8L0|v!gfCeZ9Oa%xP z0SYK0SO`!td;)v`JWgX!9hf9t_Q=0^C4w#LF_cE3cI5fxuM}6TxMKzc9~mPkI11Aa zhZNK9AEgK5NgKDrUR9%Yvc(2%Ge}uwB}z&CKsafmnU0wO?K0d}giEwHIci1T|G_pG zZu>xT5%hi5D}0q3DPaXR;8sl2l_@b**-NfcRbYSuo{H~!9d dIyh|#4%WX`9ma9i4&dCsz(ouP(GN&8nubQ^k@)}s literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/9b/b51574d23f6566590cf6bb9ce31868466be03f b/tests/test_project_fixtures/test_non_root_git_project/git/objects/9b/b51574d23f6566590cf6bb9ce31868466be03f new file mode 100644 index 0000000..d07a118 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/objects/9b/b51574d23f6566590cf6bb9ce31868466be03f @@ -0,0 +1,5 @@ +x]PËjÃ0ìY_±�ƒ�;m! øRrÈ­·„m­�ZY2zÒ¯¯d%MSa„™™�™U#TO�Ë»d÷´ŠqÙ¯ÁÙ.[„Ìà%êÚ"ƒæÛ�Zö +ʼôß +”„eQ>gE™- (‹õCI:­ ´sÖi¤ø0*mÁIîÝ‘ +n½�0$ +Ùd˜³æ¢xïÓ¸’fƒJÒŠÚ8\¨ô*ÊÁùšð‡Kny- ‚7í0b G” eËÑxâ}#§Æ°^üe‚ÍŸˆ�~ÿC(“N“�GÖVÉ&YÜ GÁLC¢0ÞiÂYrÙ*ß8«vAšÖþ�¶S«B錚µ>ÑO<�ƒÚoÅ¿±Úù×ó’/Ô�2Hc‰ý6™Ïo{„Ð4‘N:UºFï¥ÅuLÿ?v¼ºœ �äÓØŸº \ No newline at end of file diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/9d/1dcfdaf1a6857c5f83dc27019c7600e1ffaff8 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/9d/1dcfdaf1a6857c5f83dc27019c7600e1ffaff8 new file mode 100644 index 0000000000000000000000000000000000000000..0f6ee3b48f56b1a9c6c8fb48434b7c9ec77cd87d GIT binary patch literal 56 zcmV-80LTA$0V^p=O;s?qWH2-^Ff%bxh>y?A%PfhH*DI)GcsBRZ>`fQ1IeT|&t}Bna Oap*};@I(NM#S`0rff(EX literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/a3/f20003e5407a44000854c41ba29c06a3bbba82 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/a3/f20003e5407a44000854c41ba29c06a3bbba82 new file mode 100644 index 0000000000000000000000000000000000000000..95bd2eb04938d0aebbe9d2b74a542dc2593fb175 GIT binary patch literal 132 zcmV-~0DJ#<0hNtG4#FT106p`Hy_+;kp|DMi@!%gkBkU?sN3i@ zO=i;@Gms@_M(Z^tjbcWbW|29tGeHtrPb`k2RpHUEwZSnwbAnSnw;FG-OCKCwUp!n& m$oC=EvIjj`KNF7VV2c`2t#%!E{y$;xmr!}hUA+M$SUd~0x;Rb% literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/aa/66ac1debe2ae5fd39cd8e65f6dfd9786649490 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/aa/66ac1debe2ae5fd39cd8e65f6dfd9786649490 new file mode 100644 index 0000000000000000000000000000000000000000..6ec0e9951184206e817bd8034e723211bbb6f9cf GIT binary patch literal 284 zcmV+%0ptF70c}vtP69Cy_S~nK^aKgEl6cX?g9%753I{MAcgybVQqp#sb^v{QcL5U) zoFg1CI|+cT!66y_(ITqR`kD8cN`7U`z#L<{~EAVa0HnN{BfF{zS@{fwu@} zv2o699D>PUz*D_4WH2!R0)>>U#Ju$U_=2MRtkmQZ21oYuvvS;wPgY-lr&Vlf O({b-q$`SyT+!FZEjvJc* literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/bd/fc3d5864d2ad429269f80d739ef58ded2d4b35 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/bd/fc3d5864d2ad429269f80d739ef58ded2d4b35 new file mode 100644 index 0000000000000000000000000000000000000000..7bfac1550118bcdc7812d9a85636683c09cd8964 GIT binary patch literal 195 zcmV;!06hPA0acFA3c@f9##2QQLGj=Nc-&=H1#cpPcMp0KQM$Fx%G!=L6Mb$UK(jv= zL;6emC81xEyJWgt&7u1l4ld)}IfXVlRJU}(gdiyy3HM+igZgQ7m}vYF50lPk*hAlVz?pmFG}-`2o&0L^vagT|NK+ literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/bf/a71e37ed417bd9acf5ef4a44975f7bfd10341a b/tests/test_project_fixtures/test_non_root_git_project/git/objects/bf/a71e37ed417bd9acf5ef4a44975f7bfd10341a new file mode 100644 index 0000000000000000000000000000000000000000..b4d68db3e486312a87edccc00daf94879fa2eef6 GIT binary patch literal 56 zcmV-80LTA$0V^p=O;s>4WH2!R0)>>U#Ju$U_=2MRtkmQZ2E_&TX1iQPj8FdiS9tR; O>kFRuPa*)B0TaK*ry5!S literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/cb/96855f9aa7aae003123eca1addad870c7b8fde b/tests/test_project_fixtures/test_non_root_git_project/git/objects/cb/96855f9aa7aae003123eca1addad870c7b8fde new file mode 100644 index 0000000000000000000000000000000000000000..df8c6f3f439224ad8174587b2148e41a3912a98f GIT binary patch literal 154 zcmV;L0A>Gp0ZYosPf{?nG-F`?%*z!g;2e|800oRd+5w1*S%5?e14FO|NW@PANHCxg z#koK+{fzwFRQ;61vdk3yl&r+O^nBgi%=DtflFa-(-JHz4lGGyolGNgoVlWwBP?VpQ znp_f}mRV6!T9gVEP0lYW0*OLpQIzV($7kkcmc+;F6;zgR0By9%%}*)KNwovHs~CtG I0OqwfXBq@d;Q#;t literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 new file mode 100644 index 0000000000000000000000000000000000000000..711223894375fe1186ac5bfffdc48fb1fa1e65cc GIT binary patch literal 15 Wcmb4WH2!R0)>>U#Ju$U_=2MRtkmQZ2A|Kh?`0)rYP?Mzvf8gy Oes;|>C<*|P3K6ZrD;Y}w literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/f1/22245e6a3ce63b99e0beeb8403031edd08ed50 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/f1/22245e6a3ce63b99e0beeb8403031edd08ed50 new file mode 100644 index 0000000..343ce14 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/objects/f1/22245e6a3ce63b99e0beeb8403031edd08ed50 @@ -0,0 +1 @@ +x¥ŽAj1 E»ö)´�GÙPJ½Cײd§&u\ŒÓ^¿Sr„,߇÷yÒ[«®/sä ™£Å >lV•5�¤•È&ÝF&E1ß<òm·‚kà#¹%;õ‹_88Ï>•D…4a2|ŸŸ}À;ÿT…�žF—+¼ê?xº4®_Géí �ƒ]c€ƒ]¬5ûº‡ÎüÄ…9«B¿B«—Á³ö›ùÖ‚Nˆ \ No newline at end of file diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/f1/ff8f0c6cca456e2e132263fd70ac9593ac1b53 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/f1/ff8f0c6cca456e2e132263fd70ac9593ac1b53 new file mode 100644 index 0000000000000000000000000000000000000000..19affa31b60b919aca4e6e4894f76c0be31cf5b1 GIT binary patch literal 241 zcmV5HDfR|FfcPQQHYPv%*!l^kJl@xWOz3B(d~awt%+~Gqs};|jN#$`^*^BI ztL=@oye^j^_xEb?^Lphk55~U&K7YqQ z?_~f8fQ@4YNEZAI*f>e&S+KJ}Gce4-aOu?K9MC*`%ZzV(8E95Xo(Gbf2p51Z16hJ^ zS$Sv3Ik1aBFMz!QeYCm?Bo727P^SQ-2;>5g6(C4ogMI<*H6Y&sSqE|v$R!|`fm{J{ z75d9yBmElK>%f+PTnBOk%7{pALK)%7EhsaL;Wm)(f!qP|1N2esM<^2wsMP@;8@wZ}v$ zZn-32p~i(9i?QTmE#U~IjghzY?QOk`>hBRbh+?X-5RC+`U{R>yEd~tDGM_#}FKBTf z#EAb~Dv`+JdTM0if)@oP8kQttNkk*&@zTWaQi?AIRO=S^+4QOj5TZqr8UPRUT=GBwf=e#rn6f_w5*ur`jk!VOJ9j2$v+8(pHSVo<9M&+Z~ ztg0HX*7huM9*M^8t|J$GV*#qF#INe?@G!0@Mts^$`uyqt6OQQ@Y)!#8XW}RFz5rkD z2Y-25ewynAIuo9_gnFm>D)`oJyVh!2{0@qoNa+#pGabtv_d{(te>OGl^xOmL5LdWe zms4k&CVzVd)CLP#kOXxa_Qha|#eX{kMA~q>W0z+bzn=l4 znFKUu-f56?2tvcHgo_-`I_(OV68kV)++N(OB zjCu|EEo@=wQr&3obc`L-skEwQieGD(j@`n7lu>su=-T$1=5$fte%@3mkes%iM&3r- z73t*$s=#dE0WM2}D4-?c9x_r&-ve6#>*L1*7Vnmi$)j;eb`@9SBK|~UtzP*6fc@Kwtb|tftdz!tM&*$=495N@b!qWV@kjv+C fnPqk5vMJ|EKGj+)AO~ra|LS?fORxODGY~H}PsF2j literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/objects/f6/a170bb234332d6ecef3b4348f2da0e758c59a7 b/tests/test_project_fixtures/test_non_root_git_project/git/objects/f6/a170bb234332d6ecef3b4348f2da0e758c59a7 new file mode 100644 index 0000000000000000000000000000000000000000..ea64db9d016d3f9e768b06c72e37464fe6702881 GIT binary patch literal 149 zcmV;G0BZku0V^p=O;s>7v0yMXFfcPQQHYPv%*!l^kJl@xWOz3B(dKnZiHol9WDrKe=5i@RG&gjPn^^sItx%qV6xumVnPuymE2USvDoSq3XWmVc5+1HQO#b2It<5_&}-|1~BQzifa%y?A%PfhH*DI)GcsBRZ>`fQ1IeT|&t}Bna oap*};@I*{y$qY?<%pidNdN!< literal 0 HcmV?d00001 diff --git a/tests/test_project_fixtures/test_non_root_git_project/git/refs/heads/master b/tests/test_project_fixtures/test_non_root_git_project/git/refs/heads/master new file mode 100644 index 0000000..7ceae76 --- /dev/null +++ b/tests/test_project_fixtures/test_non_root_git_project/git/refs/heads/master @@ -0,0 +1 @@ +f122245e6a3ce63b99e0beeb8403031edd08ed50 From 4c5d12800e4cfc9528f3ee70ba31a6dbffc00055 Mon Sep 17 00:00:00 2001 From: David Wobrock Date: Mon, 21 Jan 2019 22:02:17 +0100 Subject: [PATCH 45/46] Add in the readme what is looked for by the linter --- README.rst | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index fa7554f..996762f 100644 --- a/README.rst +++ b/README.rst @@ -54,6 +54,19 @@ Examples 3YOURMIND is running the linter on every build getting pushed through CI. Checkout the ``examples/`` folder to see how you could integrate the linter in your test suite. +Backward incompatible migrations +-------------------------------- + +The linter analyses your migrations and checks the SQL for: + +- Added ``NOT NULL`` columns, which don't have a DEFAULT value +- Dropping columns +- Renaming columns +- Renaming tables +- Altering columns (which can be backward compatible and eventually ignored) + +Those are the most important and frequent backward incompatible migrations. We are happy to add more if you have some. + Ignoring migrations ------------------- @@ -97,6 +110,5 @@ License *django-migration-linter* is released under the `Apache 2.0 License`_. -.. _`Bitbucket Pipelines`: https://bitbucket.org/product/features/pipelines .. _`tox`: https://pypi.python.org/pypi/tox .. _`Apache 2.0 License`: https://github.com/3YOURMIND/django-migration-linter/blob/master/LICENSE From 4d68f7bac84e5fb17e306a3caf56ceb22a0f6c45 Mon Sep 17 00:00:00 2001 From: David Wobrock Date: Mon, 21 Jan 2019 22:08:57 +0100 Subject: [PATCH 46/46] Bump version to 0.1.2 --- django_migration_linter/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_migration_linter/constants.py b/django_migration_linter/constants.py index 8180696..3260131 100644 --- a/django_migration_linter/constants.py +++ b/django_migration_linter/constants.py @@ -14,7 +14,7 @@ from appdirs import user_cache_dir -__version__ = "0.1.1" +__version__ = "0.1.2" MIGRATION_FOLDER_NAME = "migrations"