diff --git a/pre_commit/commands/install.py b/pre_commit/commands/install.py index d208ff7f..606cfd25 100644 --- a/pre_commit/commands/install.py +++ b/pre_commit/commands/install.py @@ -1,24 +1,44 @@ from __future__ import print_function from __future__ import unicode_literals +import io import os +import os.path import pkg_resources import stat +# This is used to identify the hook file we install +IDENTIFYING_HASH = 'd8ee923c46731b42cd95cc869add4062' + + +def is_our_pre_commit(filename): + return IDENTIFYING_HASH in io.open(filename).read() + + +def make_executable(filename): + original_mode = os.stat(filename).st_mode + os.chmod( + filename, + original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, + ) + def install(runner): """Install the pre-commit hooks.""" pre_commit_file = pkg_resources.resource_filename( 'pre_commit', 'resources/pre-commit-hook', ) + + # If we have an existing hook, move it to pre-commit.legacy + if ( + os.path.exists(runner.pre_commit_path) and + not is_our_pre_commit(runner.pre_commit_path) + ): + os.rename(runner.pre_commit_path, runner.pre_commit_path + '.legacy') + with open(runner.pre_commit_path, 'w') as pre_commit_file_obj: pre_commit_file_obj.write(open(pre_commit_file).read()) - - original_mode = os.stat(runner.pre_commit_path).st_mode - os.chmod( - runner.pre_commit_path, - original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH, - ) + make_executable(runner.pre_commit_path) print('pre-commit installed at {0}'.format(runner.pre_commit_path)) return 0 diff --git a/pre_commit/resources/pre-commit-hook b/pre_commit/resources/pre-commit-hook index 1a93b653..60c620cf 100755 --- a/pre_commit/resources/pre-commit-hook +++ b/pre_commit/resources/pre-commit-hook @@ -1,4 +1,10 @@ #!/usr/bin/env bash +# This is a randomish md5 to identify this script +# d8ee923c46731b42cd95cc869add4062 + +HERE=$(dirname $(readlink -f "$0")) + +retv=0 which pre-commit > /dev/null if [ $? -ne 0 ]; then @@ -6,4 +12,20 @@ if [ $? -ne 0 ]; then exit 1 fi + +# Run the legacy pre-commit if it exists +if [ -x "$HERE"/pre-commit.legacy ]; then + "$HERE"/pre-commit.legacy + if [ $? -ne 0 ]; then + retv=1 + fi +fi + + +# Run pre-commit pre-commit +if [ $? -ne 0 ]; then + retv=1 +fi + +exit $retv diff --git a/tests/commands/install_test.py b/tests/commands/install_test.py index d87356e6..00bca826 100644 --- a/tests/commands/install_test.py +++ b/tests/commands/install_test.py @@ -11,14 +11,44 @@ import stat from plumbum import local from pre_commit.commands.install import install +from pre_commit.commands.install import is_our_pre_commit +from pre_commit.commands.install import make_executable from pre_commit.runner import Runner from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo -def _get_commit_output(tmpdir_factory): - local['touch']('foo') - local['git']('add', 'foo') +def test_is_not_our_pre_commit(): + assert is_our_pre_commit('setup.py') is False + + +def test_is_our_pre_commit(): + assert is_our_pre_commit( + pkg_resources.resource_filename( + 'pre_commit', 'resources/pre-commit-hook', + ) + ) is True + + +def test_install_pre_commit(tmpdir_factory): + path = git_dir(tmpdir_factory) + runner = Runner(path) + ret = install(runner) + assert ret == 0 + assert os.path.exists(runner.pre_commit_path) + pre_commit_contents = io.open(runner.pre_commit_path).read() + pre_commit_script = pkg_resources.resource_filename( + 'pre_commit', 'resources/pre-commit-hook', + ) + expected_contents = io.open(pre_commit_script).read() + assert pre_commit_contents == expected_contents + stat_result = os.stat(runner.pre_commit_path) + assert stat_result.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + + +def _get_commit_output(tmpdir_factory, touch_file='foo'): + local['touch'](touch_file) + local['git']('add', touch_file) # Don't want to write to home directory env = dict(os.environ, **{'PRE_COMMIT_HOME': tmpdir_factory.get()}) return local['git'].run( @@ -41,34 +71,6 @@ NORMAL_PRE_COMMIT_RUN = re.compile( ) -FAILING_PRE_COMMIT_RUN = re.compile( - r'^\[INFO\] Installing environment for .+\.\n' - r'\[INFO\] Once installed this environment will be reused\.\n' - r'\[INFO\] This may take a few minutes\.\.\.\n' - r'Failing hook\.+Failed\n' - r'\n' - r'Fail\n' - r'foo\n' - r'\n$' -) - - -def test_install_pre_commit(tmpdir_factory): - path = git_dir(tmpdir_factory) - runner = Runner(path) - ret = install(runner) - assert ret == 0 - assert os.path.exists(runner.pre_commit_path) - pre_commit_contents = io.open(runner.pre_commit_path).read() - pre_commit_sh = pkg_resources.resource_filename( - 'pre_commit', 'resources/pre-commit-hook', - ) - expected_contents = io.open(pre_commit_sh).read() - assert pre_commit_contents == expected_contents - stat_result = os.stat(runner.pre_commit_path) - assert stat_result.st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) - - def test_install_pre_commit_and_run(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') with local.cwd(path): @@ -108,6 +110,18 @@ def test_environment_not_sourced(tmpdir_factory): ) +FAILING_PRE_COMMIT_RUN = re.compile( + r'^\[INFO\] Installing environment for .+\.\n' + r'\[INFO\] Once installed this environment will be reused\.\n' + r'\[INFO\] This may take a few minutes\.\.\.\n' + r'Failing hook\.+Failed\n' + r'\n' + r'Fail\n' + r'foo\n' + r'\n$' +) + + def test_failing_hooks_returns_nonzero(tmpdir_factory): path = make_consuming_repo(tmpdir_factory, 'failing_hook_repo') with local.cwd(path): @@ -116,3 +130,63 @@ def test_failing_hooks_returns_nonzero(tmpdir_factory): ret, output = _get_commit_output(tmpdir_factory) assert ret == 1 assert FAILING_PRE_COMMIT_RUN.match(output) + + +EXISTING_COMMIT_RUN = re.compile( + r'^legacy hook\n' + r'\[master [a-f0-9]{7}\] Commit!\n' + r' 0 files changed\n' + r' create mode 100644 baz\n$' +) + + +def test_install_existing_hooks_no_overwrite(tmpdir_factory): + path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + with local.cwd(path): + runner = Runner(path) + + # Write out an "old" hook + with io.open(runner.pre_commit_path, 'w') as hook_file: + hook_file.write('#!/usr/bin/env bash\necho "legacy hook"\n') + make_executable(runner.pre_commit_path) + + # Make sure we installed the "old" hook correctly + ret, output = _get_commit_output(tmpdir_factory, touch_file='baz') + assert ret == 0 + assert EXISTING_COMMIT_RUN.match(output) + + # Now install pre-commit (no-overwrite) + assert install(Runner(path)) == 0 + + # We should run both the legacy and pre-commit hooks + ret, output = _get_commit_output(tmpdir_factory) + assert ret == 0 + assert output.startswith('legacy hook\n') + assert NORMAL_PRE_COMMIT_RUN.match(output[len('legacy hook\n'):]) + + +FAIL_OLD_HOOK = re.compile( + r'fail!\n' + r'\[INFO\] Installing environment for .+\.\n' + r'\[INFO\] Once installed this environment will be reused\.\n' + r'\[INFO\] This may take a few minutes\.\.\.\n' + r'Bash hook\.+Passed\n' +) + + +def test_failing_existing_hook_returns_1(tmpdir_factory): + path = make_consuming_repo(tmpdir_factory, 'script_hooks_repo') + with local.cwd(path): + runner = Runner(path) + + # Write out a failing "old" hook + with io.open(runner.pre_commit_path, 'w') as hook_file: + hook_file.write('#!/usr/bin/env bash\necho "fail!"\nexit 1\n') + make_executable(runner.pre_commit_path) + + assert install(Runner(path)) == 0 + + # We should get a failure from the legacy hook + ret, output = _get_commit_output(tmpdir_factory) + assert ret == 1 + assert FAIL_OLD_HOOK.match(output)