hookmeup

A Git hook to automate your Pipenv and Django workflow
git clone git://git.danielmoch.com/hookmeup.git
Log | Files | Refs | README | LICENSE

commit 7fa87f53e182f9c1455fe94ccc2c96785e78cdaf
parent a7d6b2c92005726e6d9fe178e8a0c8473e138373
Author: Daniel Moch <daniel@danielmoch.com>
Date:   Fri, 24 Aug 2018 16:02:21 -0400

Add remove subcommand

Diffstat:
MMakefile | 6+++---
Mhookmeup/__init__.py | 16+++++++++++++---
Mhookmeup/hookmeup.py | 51+++++++++++++++++++++++++++++++++++++++++++++------
Mpytest.ini | 2+-
Atests/coveragerc | 5+++++
Mtests/test_hookmeup.py | 118++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
6 files changed, 184 insertions(+), 14 deletions(-)

diff --git a/Makefile b/Makefile @@ -52,17 +52,17 @@ clean-test: ## remove test and coverage artifacts rm -fr .pytest_cache rm -fr .tox -lint: ## check style with flake8 +lint: ## check style with pylint $(PIPENV) pylint --rcfile tests/pylintrc hookmeup tests test: ## run tests quickly with the default Python - $(PIPENV) python -m pytest + -$(PIPENV) python -m pytest test-all: ## run tests on every Python version with tox $(PIPENV) tox coverage: ## check code coverage quickly with the default Python - $(PIPENV) python -m pytest + -$(PIPENV) python -m pytest $(PIPENV) coverage report -m $(PIPENV) coverage html $(BROWSER) htmlcov/index.html diff --git a/hookmeup/__init__.py b/hookmeup/__init__.py @@ -14,18 +14,28 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument( '-v', action='version', - version='%(prog)s {}'.format(__version__)) + version='%(prog)s {}'.format(__version__) + ) subparsers = parser.add_subparsers( title='subcommands', description='Valid %(prog)s subcommands. See more \ information on a subcommand by typing hookmeup \ - {subcommand} {-h,--help}') + {subcommand} {-h,--help}' + ) install_parser = subparsers.add_parser( 'install', description='Run inside a repository to install the hook. \ Fails if the current directory is not inside a Git \ - repository.') + repository.' + ) install_parser.set_defaults(func=hookmeup.install) + remove_parser = subparsers.add_parser( + 'remove', + description="Run inside a repository to uninstall the hook. \ + Fails if the current directory is not inside a Git \ + repository." + ) + remove_parser.set_defaults(func=hookmeup.remove) post_commit_parser = subparsers.add_parser( 'post-checkout', description='Run post-checkout hook. This should normally \ diff --git a/hookmeup/hookmeup.py b/hookmeup/hookmeup.py @@ -107,12 +107,12 @@ def pipfile_changed(args): 'Not in a Git repository' ) - return stdout.startswith('M') + return stdout[0] in ['M', 'A'] def post_checkout(args): """Run post-checkout hook""" - migrator = DjangoMigrator(args) if args['branch_checkout'] == 1: + migrator = DjangoMigrator(args) if migrator.migrations_changed(): migrator.migrate() if pipfile_changed(args): @@ -140,11 +140,50 @@ def install(args): with open(hook_path, 'r') as hook_file: already_installed = 'hookmeup' in hook_file.read() - with open(hook_path, 'a') as hook_file: - if already_installed: - print('hookmeup already installed') - else: + if already_installed: + print('hookmeup: already installed') + else: + print('hookmeup: installing to existing hook') + with open(hook_path, 'a') as hook_file: hook_file.write('hookmeup post-checkout "$@"\n') else: + print('hookmeup: creating hook') with open(hook_path, 'w') as hook_file: hook_file.write('#!/bin/sh\nhookmeup post-checkout "$@"\n') + +def remove(args): + """Remove the hook from the repository""" + if len(args) is not 0: + raise HookMeUpError( + "Argument passed to 'remove', but expected none" + ) + + stdout = call_checked_subprocess( + ['git', 'rev-parse', '--git-dir'], + 'Not in a Git repository' + ) + + hook_path = os.path.join( + stdout.strip(), + 'hooks', + 'post-checkout' + ) + + if os.path.exists(hook_path): + with open(hook_path, 'r') as hook_file: + hook_lines = hook_file.read() + installed = 'hookmeup' in hook_lines + hook_lines = hook_lines.splitlines() + + if installed: + hook_lines = \ + ['{}\n'.format(line) + for line in hook_lines + if line.find('hookmeup') == -1] + with open(hook_path, 'w') as hook_file: + hook_file.writelines(hook_lines) + else: + print('hookmeup: hookmeup not installed. nothing to do.') + + else: + print('hookmeup: no hook to remove') diff --git a/pytest.ini b/pytest.ini @@ -1,2 +1,2 @@ [pytest] -addopts= --cov=hookmeup --cov-report=term-missing --pylint --pylint-rcfile=tests/pylintrc +addopts= --cov=hookmeup --cov-config tests/coveragerc --cov-report=term-missing --pylint --pylint-rcfile=tests/pylintrc diff --git a/tests/coveragerc b/tests/coveragerc @@ -0,0 +1,5 @@ +[run] +branch = True + +[report] +fail_under = 100 diff --git a/tests/test_hookmeup.py b/tests/test_hookmeup.py @@ -82,6 +82,23 @@ def test_error(): except HookMeUpError as error: assert str(error) == 'hookmeup: test error' +def test_post_checkout_non_branch(mocker): + """Test post_checkout call for non-branch checkout""" + mocker.patch( + 'hookmeup.hookmeup.adjust_pipenv' + ) + mocker.patch.object( + DjangoMigrator, + 'migrations_changed' + ) + hookmeup.hookmeup.post_checkout( + {'old': 'old', + 'new': 'new', + 'branch_checkout': 0} + ) + hookmeup.hookmeup.adjust_pipenv.assert_not_called() + DjangoMigrator.migrations_changed.assert_not_called() + def test_post_checkout(mocker): """Test nominal post_checkout""" mocker.patch( @@ -174,7 +191,7 @@ def test_migrate_down(mocker): migrator._migrate_command + ['app2', '0002'] ) -def test__migrate_to_zero(mocker): +def test_migrate_to_zero(mocker): """Test a Django migration upgrade with an intervening squash""" mocker.patch( 'subprocess.check_output', @@ -198,3 +215,102 @@ def test__migrate_to_zero(mocker): subprocess.check_output.assert_any_call( migrator._migrate_command + ['app1', 'app2', 'app3'] ) + +def test_remove(mocker): + """Test removing the hook (nominal case)""" + mocker.patch( + 'subprocess.check_output', + new=mocker.MagicMock(return_value=b'.git\n') + ) + mocker.patch( + 'os.path.exists', + new=mocker.MagicMock(return_value=True) + ) + mock_file = mocker.mock_open( + read_data='#!/bin/sh\nfoo\nhookmeup post-checkout "$@"' + ) + mocker.patch('hookmeup.hookmeup.open', new=mock_file) + hookmeup.hookmeup.remove({}) + assert subprocess.check_output.call_count == 1 + assert os.path.exists.call_count == 1 + assert mock_file.call_count == 2 + assert mock_file().read.call_count == 1 + mock_file().writelines.assert_called_with(['#!/bin/sh\n', 'foo\n']) + +def test_remove_no_repo(mocker): + """Test removing the hook (nominal case)""" + mocker.patch( + 'subprocess.check_output', + new=mocker.Mock( + side_effect=CalledProcessError(128, 'cmd')) + ) + mocker.patch( + 'os.path.exists', + new=mocker.MagicMock(return_value=False) + ) + mock_file = mocker.mock_open( + read_data='#!/bin/sh\nfoo\nhookmeup post-checkout "$@"' + ) + mocker.patch('hookmeup.hookmeup.open', new=mock_file) + with pytest.raises(HookMeUpError): + hookmeup.hookmeup.remove({}) + assert subprocess.check_output.call_count == 1 + assert os.path.exists.call_count == 0 + assert mock_file.call_count == 0 + assert mock_file().read.call_count == 0 + assert mock_file().writelines.call_count == 0 + +def test_remove_no_hook_file(mocker): + """Test remove when no hook file""" + mocker.patch( + 'subprocess.check_output', + new=mocker.MagicMock(return_value=b'.git\n') + ) + mocker.patch( + 'os.path.exists', + new=mocker.MagicMock(return_value=False) + ) + mock_file = mocker.mock_open( + read_data='#!/bin/sh\nfoo\nhookmeup post-checkout "$@"' + ) + mocker.patch('hookmeup.hookmeup.open', new=mock_file) + mocker.patch('hookmeup.hookmeup.print') + hookmeup.hookmeup.remove({}) + assert subprocess.check_output.call_count == 1 + assert os.path.exists.call_count == 1 + assert mock_file.call_count == 0 + assert mock_file().read.call_count == 0 + hookmeup.hookmeup.print.assert_called_with( + 'hookmeup: no hook to remove' + ) + assert mock_file().writelines.call_count == 0 + +def test_remove_not_installed(mocker): + """Test remove when hook not installed""" + mocker.patch( + 'subprocess.check_output', + new=mocker.MagicMock(return_value=b'.git\n') + ) + mocker.patch( + 'os.path.exists', + new=mocker.MagicMock(return_value=True) + ) + mock_file = mocker.mock_open( + read_data='#!/bin/sh\nfoo' + ) + mocker.patch('hookmeup.hookmeup.open', new=mock_file) + mocker.patch('hookmeup.hookmeup.print') + hookmeup.hookmeup.remove({}) + assert subprocess.check_output.call_count == 1 + assert os.path.exists.call_count == 1 + assert mock_file.call_count == 1 + assert mock_file().read.call_count == 1 + hookmeup.hookmeup.print.assert_called_with( + 'hookmeup: hookmeup not installed. nothing to do.' + ) + assert mock_file().writelines.call_count == 0 + +def test_remove_unexpected_arg(mocker): + """Test remove when hook not installed""" + with pytest.raises(HookMeUpError): + hookmeup.hookmeup.remove({'this': 'that'})