From 4ed9120ae9322af57706cdf8801b3a140a89dde6 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 5 Apr 2014 18:41:49 -0700 Subject: [PATCH 1/4] Add staged_files_only context manager. --- pre_commit/prefixed_command_runner.py | 21 +-- pre_commit/staged_files_only.py | 47 ++++++ testing/resources/img1.jpg | Bin 0 -> 843 bytes testing/resources/img2.jpg | Bin 0 -> 891 bytes testing/resources/img3.jpg | Bin 0 -> 859 bytes tests/prefixed_command_runner_test.py | 10 ++ tests/staged_files_only_test.py | 218 ++++++++++++++++++++++++++ 7 files changed, 287 insertions(+), 9 deletions(-) create mode 100644 pre_commit/staged_files_only.py create mode 100644 testing/resources/img1.jpg create mode 100644 testing/resources/img2.jpg create mode 100644 testing/resources/img3.jpg create mode 100644 tests/staged_files_only_test.py diff --git a/pre_commit/prefixed_command_runner.py b/pre_commit/prefixed_command_runner.py index bee77016..ed88f219 100644 --- a/pre_commit/prefixed_command_runner.py +++ b/pre_commit/prefixed_command_runner.py @@ -6,6 +6,9 @@ import subprocess class CalledProcessError(RuntimeError): def __init__(self, returncode, cmd, expected_returncode, output=None): + super(CalledProcessError, self).__init__( + returncode, cmd, expected_returncode, output, + ) self.returncode = returncode self.cmd = cmd self.expected_returncode = expected_returncode @@ -15,13 +18,13 @@ class CalledProcessError(RuntimeError): return ( 'Command: {0!r}\n' 'Return code: {1}\n' - 'Expected return code {2}\n', + 'Expected return code: {2}\n' 'Output: {3!r}\n'.format( self.cmd, self.returncode, self.expected_returncode, self.output, - ), + ) ) @@ -48,15 +51,15 @@ class PrefixedCommandRunner(object): self.__makedirs(self.prefix_dir) def run(self, cmd, retcode=0, stdin=None, **kwargs): + popen_kwargs = { + 'stdin': subprocess.PIPE, + 'stdout': subprocess.PIPE, + 'stderr': subprocess.PIPE, + } + popen_kwargs.update(kwargs) self._create_path_if_not_exists() replaced_cmd = _replace_cmd(cmd, prefix=self.prefix_dir) - proc = self.__popen( - replaced_cmd, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - **kwargs - ) + proc = self.__popen(replaced_cmd, **popen_kwargs) stdout, stderr = proc.communicate(stdin) returncode = proc.returncode diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py new file mode 100644 index 00000000..859dd25b --- /dev/null +++ b/pre_commit/staged_files_only.py @@ -0,0 +1,47 @@ + +import contextlib +import time + +from pre_commit.prefixed_command_runner import CalledProcessError + + +@contextlib.contextmanager +def staged_files_only(cmd_runner): + """Clear any unstaged changes from the git working directory inside this + context. + + Args: + cmd_runner - PrefixedCommandRunner + """ + # Determine if there are unstaged files + retcode, _, _ = cmd_runner.run( + ['git', 'diff-files', '--quiet'], + retcode=None, + ) + if retcode: + # TODO: print a warning message that unstaged things are being stashed + # Save the current unstaged changes as a patch + # TODO: use a more unique patch filename + patch_filename = cmd_runner.path('patch{0}'.format(time.time())) + with open(patch_filename, 'w') as patch_file: + cmd_runner.run(['git', 'diff', '--binary'], stdout=patch_file) + + # Clear the working directory of unstaged changes + cmd_runner.run(['git', 'checkout', '--', '.']) + try: + yield + finally: + # Try to apply the patch we saved + try: + cmd_runner.run(['git', 'apply', patch_filename]) + except CalledProcessError: + # TOOD: print a warning about rolling back changes made by hooks + # We failed to apply the patch, presumably due to fixes made + # by hooks. + # Roll back the changes made by hooks. + cmd_runner.run(['git', 'checkout', '--', '.']) + cmd_runner.run(['git', 'apply', patch_filename]) + else: + # There weren't any staged files so we don't need to do anything + # special + yield diff --git a/testing/resources/img1.jpg b/testing/resources/img1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dea42627ad9f5eb8061bf5068117faa3ba22591c GIT binary patch literal 843 zcmex=_1P|rX?qqI0P zFI~aY%U!`Mz|~!$%*;qrN1?DZF(6Oj-S5fuR$!pIEN!@|nR z%E~Fi%grl7GWdUhL6Cz%fI)znQHg;`kdaxC@&6G9d7vj*8Nq-73K*GyZe(NU;N;>4 zD%dK(z{JSR%*4VBay3wOEl{3;MUYiU(a@1iI53f2sZhkIapFP_Wv7h?MT0JWP%%y_ zYU1P)6PJ*bQdLve(9|+9H8Z!cv~qTFb#wRd^a>6M4GWKmj7m;PO-s+n%qlJ^Ei136 ztZHs)ZENr7?3y%r%G7DoXUv?nXz`Mz%a*TLxoXqqEnBy3-?4Mop~FXx9y@;G&P778mFHFAhJO^@zmKAh{eZ~f|M{x9gm=v-l>-n?(>?Mmen~E6EX;=IIShdgcqTAxqLYrGVE+%f;HR<7| pm^B+2ZXP)2P=A_1P|rX?qqI0P zFI~aY%U!`Mz|~!$%*;qrN1?DZF(6Oj-S5fuR$!pIEN!@|nR z%E~Fi%grl7GWdUhL6Cz%fI)znQHg;`kdaxC@&6G9d7vj*8Nq-73K*GyZe(NU;N;>4 zD%dK(z{JSR%*4VBay3wOEl{3;MUYiU(a@1iI53f2sZhkIapFP_Wv7h?MT0JWP%%y_ zYU1P)6PJ*bQdLve(9|+9H8Z!cv~qTFb#wRd^a>6M4GWKmj7m;PO-s+n%qlJ^Ei136 ztZHs)ZENr7?3y%r%G7DoXUv?nXz`Mz%a*TLxoXqqEnBy3-?4Mop~FXx9y@;G&P778mFHFAhJO?e(whs7_!gH(4Ta?h)hPo9fEnntq5sTHhvT{?TpsLwUYeb_$nl zoHehfd}N)x_|+`YTc^ys(>|$8?|3rf`1P4{{jSE9?BlNy`?&sye%CMmj;*%pNA~f4 paGm<5POIg$xc9f)cU|vnUbEs%T8F`*EirCp8v}nIVqpJ&697dxJ@Nnm literal 0 HcmV?d00001 diff --git a/testing/resources/img3.jpg b/testing/resources/img3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..392d2cf22882b17530452c023ea6f12e885a895b GIT binary patch literal 859 zcmex=_1P|rX?qqI0P zFI~aY%U!`Mz|~!$%*;qrN1?DZF(6Oj-S5fuR$!pIEN!@|nR z%E~Fi%grl7GWdUhL6Cz%fI)znQHg;`kdaxC@&6G9d7vj*8Nq-73K*GyZe(NU;N;>4 zD%dK(z{JSR%*4VBay3wOEl{3;MUYiU(a@1iI53f2sZhkIapFP_Wv7h?MT0JWP%%y_ zYU1P)6PJ*bQdLve(9|+9H8Z!cv~qTFb#wRd^a>6M4GWKmj7m;PO-s+n%qlJ^Ei136 ztZHs)ZENr7?3y%r%G7DoXUv?nXz`Mz%a*TLxoXqqEnBy3-?4Mop~FXx9y@;G&P778mFHFAhJO`IkyR&u>X z7d%cWx@S5q&{v*&*Wm;+v&^LXZL{XBUR$I0F>LbsUZ;o`IAe2n(aS3P?2jo!Y3TzV{??RthC`e^g(U% zBkj4&N3Ko2VUzSoQe|6PIor!)Hzr83c|1Rr#9&io9X?Sqq)+wWUdj7GBFh_tdrIB^ G-vj_m Date: Sat, 5 Apr 2014 21:50:20 -0700 Subject: [PATCH 2/4] Add logging handler. --- pre_commit/color.py | 2 ++ pre_commit/logging_handler.py | 32 ++++++++++++++++++++++++++++++++ pre_commit/repository.py | 12 +++++++----- pre_commit/run.py | 8 ++++++++ tests/repository_test.py | 20 +++++++++++--------- 5 files changed, 60 insertions(+), 14 deletions(-) create mode 100644 pre_commit/logging_handler.py diff --git a/pre_commit/color.py b/pre_commit/color.py index 641e719e..b9808ad4 100644 --- a/pre_commit/color.py +++ b/pre_commit/color.py @@ -3,6 +3,8 @@ import sys RED = '\033[41m' GREEN = '\033[42m' +YELLOW = '\033[43;30m' +TURQUOISE = '\033[46;30m' NORMAL = '\033[0m' diff --git a/pre_commit/logging_handler.py b/pre_commit/logging_handler.py new file mode 100644 index 00000000..11736d1b --- /dev/null +++ b/pre_commit/logging_handler.py @@ -0,0 +1,32 @@ + +from __future__ import print_function + +import logging + +from pre_commit import color + + +LOG_LEVEL_COLORS = { + 'DEBUG': '', + 'INFO': '', + 'WARNING': color.YELLOW, + 'ERROR': color.RED, +} + + +class LoggingHandler(logging.Handler): + def __init__(self, use_color): + super(LoggingHandler, self).__init__() + self.use_color = use_color + + def emit(self, record): + print( + u'{0}{1}'.format( + color.format_color( + '[{0}]'.format(record.levelname), + LOG_LEVEL_COLORS[record.levelname], + self.use_color, + ) + ' ' if record.levelno >= logging.WARNING else '', + record.getMessage(), + ) + ) diff --git a/pre_commit/repository.py b/pre_commit/repository.py index 46764957..a8611f79 100644 --- a/pre_commit/repository.py +++ b/pre_commit/repository.py @@ -1,7 +1,6 @@ -from __future__ import print_function - import contextlib +import logging from plumbum import local import pre_commit.constants as C @@ -14,6 +13,9 @@ from pre_commit.util import cached_property from pre_commit.util import clean_path_on_failure +logger = logging.getLogger('pre_commit') + + class Repository(object): def __init__(self, repo_config): self.repo_config = repo_config @@ -66,9 +68,9 @@ class Repository(object): return # Checking out environment for the first time - print('Installing environment for {0}.'.format(self.repo_url)) - print('Once installed this environment will be reused.') - print('This may take a few minutes...') + logger.info('Installing environment for {0}.'.format(self.repo_url)) + logger.info('Once installed this environment will be reused.') + logger.info('This may take a few minutes...') with clean_path_on_failure(unicode(local.path(self.sha))): local['git']['clone', '--no-checkout', self.repo_url, self.sha]() with self.in_checkout(): diff --git a/pre_commit/run.py b/pre_commit/run.py index 7f1cb6b5..a5792fab 100644 --- a/pre_commit/run.py +++ b/pre_commit/run.py @@ -2,16 +2,20 @@ from __future__ import print_function import argparse +import logging import subprocess import sys from pre_commit import color from pre_commit import commands from pre_commit import git +from pre_commit.logging_handler import LoggingHandler from pre_commit.runner import Runner from pre_commit.util import entry +logger = logging.getLogger('pre_commit') + COLS = int(subprocess.Popen(['tput', 'cols'], stdout=subprocess.PIPE).communicate()[0]) PASS_FAIL_LENGTH = 6 @@ -81,6 +85,10 @@ def run_single_hook(runner, hook_id, args): def _run(runner, args): + # Set up our logging handler + logger.addHandler(LoggingHandler(args.color)) + logger.setLevel(logging.INFO) + if args.hook: return run_single_hook(runner, args.hook, args) else: diff --git a/tests/repository_test.py b/tests/repository_test.py index 925dbbdd..eb840c2c 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,10 +1,10 @@ -import __builtin__ import mock import os import pytest import pre_commit.constants as C from pre_commit import git +from pre_commit import repository from pre_commit.clientlib.validate_config import CONFIG_JSON_SCHEMA from pre_commit.clientlib.validate_config import validate_config_extra from pre_commit.jsonschema_extensions import apply_defaults @@ -135,23 +135,25 @@ def test_languages(config_for_python_hooks_repo): @pytest.yield_fixture -def print_mock(): - with mock.patch.object(__builtin__, 'print', autospec=True) as print_mock: - yield print_mock +def logger_mock(): + with mock.patch.object( + repository.logger, 'info', autospec=True, + ) as info_mock: + yield info_mock -def test_prints_while_creating(config_for_python_hooks_repo, print_mock): +def test_prints_while_creating(config_for_python_hooks_repo, logger_mock): repo = Repository(config_for_python_hooks_repo) repo.require_created() - print_mock.assert_called_with('This may take a few minutes...') - print_mock.reset_mock() + logger_mock.assert_called_with('This may take a few minutes...') + logger_mock.reset_mock() # Reinstall with same repo should not trigger another install repo.require_created() - assert print_mock.call_count == 0 + assert logger_mock.call_count == 0 # Reinstall on another run should not trigger another install repo = Repository(config_for_python_hooks_repo) repo.require_created() - assert print_mock.call_count == 0 + assert logger_mock.call_count == 0 def test_reinstall(config_for_python_hooks_repo): From 158a3a6d8b5d840051009aab624f71e6ea85025e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 5 Apr 2014 22:01:29 -0700 Subject: [PATCH 3/4] Pre-commit stashes unstaged changes on run. Closes #30. --- pre_commit/run.py | 10 ++++++---- pre_commit/staged_files_only.py | 17 ++++++++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/pre_commit/run.py b/pre_commit/run.py index a5792fab..17aa2aa9 100644 --- a/pre_commit/run.py +++ b/pre_commit/run.py @@ -11,6 +11,7 @@ from pre_commit import commands from pre_commit import git from pre_commit.logging_handler import LoggingHandler from pre_commit.runner import Runner +from pre_commit.staged_files_only import staged_files_only from pre_commit.util import entry @@ -89,10 +90,11 @@ def _run(runner, args): logger.addHandler(LoggingHandler(args.color)) logger.setLevel(logging.INFO) - if args.hook: - return run_single_hook(runner, args.hook, args) - else: - return run_hooks(runner, args) + with staged_files_only(runner.cmd_runner): + if args.hook: + return run_single_hook(runner, args.hook, args) + else: + return run_hooks(runner, args) @entry diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index 859dd25b..fdb42d67 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -1,10 +1,14 @@ import contextlib +import logging import time from pre_commit.prefixed_command_runner import CalledProcessError +logger = logging.getLogger('pre_commit') + + @contextlib.contextmanager def staged_files_only(cmd_runner): """Clear any unstaged changes from the git working directory inside this @@ -19,10 +23,12 @@ def staged_files_only(cmd_runner): retcode=None, ) if retcode: - # TODO: print a warning message that unstaged things are being stashed + patch_filename = cmd_runner.path('patch{0}'.format(int(time.time()))) + logger.warning('Unstaged files detected.') + logger.info( + 'Stashing unstaged files to {0}.'.format(patch_filename), + ) # Save the current unstaged changes as a patch - # TODO: use a more unique patch filename - patch_filename = cmd_runner.path('patch{0}'.format(time.time())) with open(patch_filename, 'w') as patch_file: cmd_runner.run(['git', 'diff', '--binary'], stdout=patch_file) @@ -35,12 +41,17 @@ def staged_files_only(cmd_runner): try: cmd_runner.run(['git', 'apply', patch_filename]) except CalledProcessError: + logger.warning( + 'Stashed changes conflicted with hook auto-fixes... ' + 'Rolling back fixes...' + ) # TOOD: print a warning about rolling back changes made by hooks # We failed to apply the patch, presumably due to fixes made # by hooks. # Roll back the changes made by hooks. cmd_runner.run(['git', 'checkout', '--', '.']) cmd_runner.run(['git', 'apply', patch_filename]) + logger.info('Restored changes from {0}.'.format(patch_filename)) else: # There weren't any staged files so we don't need to do anything # special From 514a6d66cf42cd86fe604993dc5cf9139a17bb79 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 5 Apr 2014 22:05:18 -0700 Subject: [PATCH 4/4] Remove incorrect todo comment. --- pre_commit/staged_files_only.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py index fdb42d67..8a38ae11 100644 --- a/pre_commit/staged_files_only.py +++ b/pre_commit/staged_files_only.py @@ -45,7 +45,6 @@ def staged_files_only(cmd_runner): 'Stashed changes conflicted with hook auto-fixes... ' 'Rolling back fixes...' ) - # TOOD: print a warning about rolling back changes made by hooks # We failed to apply the patch, presumably due to fixes made # by hooks. # Roll back the changes made by hooks.