Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions pre_commit/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def _hook_msg_start(hook, verbose):
NO_FILES = '(no files to check)'


def _run_single_hook(classifier, hook, args, skips, cols):
def _run_single_hook(classifier, hook, args, skips, cols, use_color):
filenames = classifier.filenames_for_hook(hook)

if hook.language == 'pcre':
Expand Down Expand Up @@ -118,7 +118,8 @@ def _run_single_hook(classifier, hook, args, skips, cols):
sys.stdout.flush()

diff_before = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None)
retcode, out = hook.run(tuple(filenames) if hook.pass_filenames else ())
filenames = tuple(filenames) if hook.pass_filenames else ()
retcode, out = hook.run(filenames, use_color)
diff_after = cmd_output_b('git', 'diff', '--no-ext-diff', retcode=None)

file_modifications = diff_before != diff_after
Expand Down Expand Up @@ -203,7 +204,9 @@ def _run_hooks(config, hooks, args, environ):
classifier = Classifier(filenames)
retval = 0
for hook in hooks:
retval |= _run_single_hook(classifier, hook, args, skips, cols)
retval |= _run_single_hook(
classifier, hook, args, skips, cols, args.color,
)
if retval and config['fail_fast']:
break
if retval and args.show_diff_on_failure and git.has_diff():
Expand Down
5 changes: 3 additions & 2 deletions pre_commit/languages/all.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,17 @@
# version - A version specified in the hook configuration or 'default'.
# """
#
# def run_hook(hook, file_args):
# def run_hook(hook, file_args, color):
# """Runs a hook and returns the returncode and output of running that
# hook.
#
# Args:
# hook - `Hook`
# file_args - The files to be run
# color - whether the hook should be given a pty (when supported)
#
# Returns:
# (returncode, stdout, stderr)
# (returncode, output)
# """

languages = {
Expand Down
8 changes: 4 additions & 4 deletions pre_commit/languages/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,15 @@ def docker_cmd(): # pragma: windows no cover
)


def run_hook(hook, file_args): # pragma: windows no cover
def run_hook(hook, file_args, color): # pragma: windows no cover
assert_docker_available()
# Rebuild the docker image in case it has gone missing, as many people do
# automated cleanup of docker images.
build_docker_image(hook.prefix, pull=False)

hook_cmd = helpers.to_cmd(hook)
entry_exe, cmd_rest = hook_cmd[0], hook_cmd[1:]
hook_cmd = hook.cmd
entry_exe, cmd_rest = hook.cmd[0], hook_cmd[1:]

entry_tag = ('--entrypoint', entry_exe, docker_tag(hook.prefix))
cmd = docker_cmd() + entry_tag + cmd_rest
return helpers.run_xargs(hook, cmd, file_args)
return helpers.run_xargs(hook, cmd, file_args, color=color)
6 changes: 3 additions & 3 deletions pre_commit/languages/docker_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
install_environment = helpers.no_install


def run_hook(hook, file_args): # pragma: windows no cover
def run_hook(hook, file_args, color): # pragma: windows no cover
assert_docker_available()
cmd = docker_cmd() + helpers.to_cmd(hook)
return helpers.run_xargs(hook, cmd, file_args)
cmd = docker_cmd() + hook.cmd
return helpers.run_xargs(hook, cmd, file_args, color=color)
2 changes: 1 addition & 1 deletion pre_commit/languages/fail.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
install_environment = helpers.no_install


def run_hook(hook, file_args):
def run_hook(hook, file_args, color):
out = hook.entry.encode('UTF-8') + b'\n\n'
out += b'\n'.join(f.encode('UTF-8') for f in file_args) + b'\n'
return 1, out
4 changes: 2 additions & 2 deletions pre_commit/languages/golang.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,6 @@ def install_environment(prefix, version, additional_dependencies):
rmtree(pkgdir)


def run_hook(hook, file_args):
def run_hook(hook, file_args, color):
with in_env(hook.prefix):
return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args)
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
10 changes: 3 additions & 7 deletions pre_commit/languages/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import multiprocessing
import os
import random
import shlex

import six

Expand All @@ -25,10 +24,6 @@ def environment_dir(ENVIRONMENT_DIR, language_version):
return '{}-{}'.format(ENVIRONMENT_DIR, language_version)


def to_cmd(hook):
return tuple(shlex.split(hook.entry)) + tuple(hook.args)


def assert_version_default(binary, version):
if version != C.DEFAULT:
raise AssertionError(
Expand Down Expand Up @@ -83,8 +78,9 @@ def _shuffled(seq):
return seq


def run_xargs(hook, cmd, file_args):
def run_xargs(hook, cmd, file_args, **kwargs):
# Shuffle the files so that they more evenly fill out the xargs partitions,
# but do it deterministically in case a hook cares about ordering.
file_args = _shuffled(file_args)
return xargs(cmd, file_args, target_concurrency=target_concurrency(hook))
kwargs['target_concurrency'] = target_concurrency(hook)
return xargs(cmd, file_args, **kwargs)
4 changes: 2 additions & 2 deletions pre_commit/languages/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,6 @@ def install_environment(
)


def run_hook(hook, file_args): # pragma: windows no cover
def run_hook(hook, file_args, color): # pragma: windows no cover
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args)
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
4 changes: 2 additions & 2 deletions pre_commit/languages/pcre.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
install_environment = helpers.no_install


def run_hook(hook, file_args):
def run_hook(hook, file_args, color):
# For PCRE the entry is the regular expression to match
cmd = (GREP, '-H', '-n', '-P') + tuple(hook.args) + (hook.entry,)

# Grep usually returns 0 for matches, and nonzero for non-matches so we
# negate it here.
return xargs(cmd, file_args, negate=True)
return xargs(cmd, file_args, negate=True, color=color)
4 changes: 2 additions & 2 deletions pre_commit/languages/pygrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ def _process_filename_at_once(pattern, filename):
return retv


def run_hook(hook, file_args):
def run_hook(hook, file_args, color):
exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,)
return xargs(exe, file_args)
return xargs(exe, file_args, color=color)


def main(argv=None):
Expand Down
4 changes: 2 additions & 2 deletions pre_commit/languages/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,9 @@ def healthy(prefix, language_version):
)
return retcode == 0

def run_hook(hook, file_args):
def run_hook(hook, file_args, color):
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args)
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

def install_environment(prefix, version, additional_dependencies):
additional_dependencies = tuple(additional_dependencies)
Expand Down
4 changes: 2 additions & 2 deletions pre_commit/languages/ruby.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,6 @@ def install_environment(
)


def run_hook(hook, file_args): # pragma: windows no cover
def run_hook(hook, file_args, color): # pragma: windows no cover
with in_env(hook.prefix, hook.language_version):
return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args)
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
4 changes: 2 additions & 2 deletions pre_commit/languages/rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,6 @@ def install_environment(prefix, version, additional_dependencies):
)


def run_hook(hook, file_args):
def run_hook(hook, file_args, color):
with in_env(hook.prefix):
return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args)
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
6 changes: 3 additions & 3 deletions pre_commit/languages/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
install_environment = helpers.no_install


def run_hook(hook, file_args):
cmd = helpers.to_cmd(hook)
def run_hook(hook, file_args, color):
cmd = hook.cmd
cmd = (hook.prefix.path(cmd[0]),) + cmd[1:]
return helpers.run_xargs(hook, cmd, file_args)
return helpers.run_xargs(hook, cmd, file_args, color=color)
4 changes: 2 additions & 2 deletions pre_commit/languages/swift.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ def install_environment(
)


def run_hook(hook, file_args): # pragma: windows no cover
def run_hook(hook, file_args, color): # pragma: windows no cover
with in_env(hook.prefix):
return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args)
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
4 changes: 2 additions & 2 deletions pre_commit/languages/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
install_environment = helpers.no_install


def run_hook(hook, file_args):
return helpers.run_xargs(hook, helpers.to_cmd(hook), file_args)
def run_hook(hook, file_args, color):
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
9 changes: 7 additions & 2 deletions pre_commit/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import json
import logging
import os
import shlex

import pre_commit.constants as C
from pre_commit import five
Expand Down Expand Up @@ -54,6 +55,10 @@ def _write_state(prefix, venv, state):
class Hook(collections.namedtuple('Hook', ('src', 'prefix') + _KEYS)):
__slots__ = ()

@property
def cmd(self):
return tuple(shlex.split(self.entry)) + tuple(self.args)

@property
def install_key(self):
return (
Expand Down Expand Up @@ -95,9 +100,9 @@ def install(self):
# Write our state to indicate we're installed
_write_state(self.prefix, venv, _state(self.additional_dependencies))

def run(self, file_args):
def run(self, file_args, color):
lang = languages[self.language]
return lang.run_hook(self, file_args)
return lang.run_hook(self, file_args, color)

@classmethod
def create(cls, src, prefix, dct):
Expand Down
87 changes: 76 additions & 11 deletions pre_commit/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,29 +117,28 @@ def to_text(self):
__str__ = to_text


def cmd_output_b(*cmd, **kwargs):
retcode = kwargs.pop('retcode', 0)

popen_kwargs = {
'stdin': subprocess.PIPE,
'stdout': subprocess.PIPE,
'stderr': subprocess.PIPE,
}

def _cmd_kwargs(*cmd, **kwargs):
# py2/py3 on windows are more strict about the types here
cmd = tuple(five.n(arg) for arg in cmd)
kwargs['env'] = {
five.n(key): five.n(value)
for key, value in kwargs.pop('env', {}).items()
} or None
popen_kwargs.update(kwargs)
for arg in ('stdin', 'stdout', 'stderr'):
kwargs.setdefault(arg, subprocess.PIPE)
return cmd, kwargs


def cmd_output_b(*cmd, **kwargs):
retcode = kwargs.pop('retcode', 0)
cmd, kwargs = _cmd_kwargs(*cmd, **kwargs)

try:
cmd = parse_shebang.normalize_cmd(cmd)
except parse_shebang.ExecutableNotFoundError as e:
returncode, stdout_b, stderr_b = e.to_output()
else:
proc = subprocess.Popen(cmd, **popen_kwargs)
proc = subprocess.Popen(cmd, **kwargs)
stdout_b, stderr_b = proc.communicate()
returncode = proc.returncode

Expand All @@ -158,6 +157,72 @@ def cmd_output(*cmd, **kwargs):
return returncode, stdout, stderr


if os.name != 'nt': # pragma: windows no cover
from os import openpty
import termios

class Pty(object):
def __init__(self):
self.r = self.w = None

def __enter__(self):
self.r, self.w = openpty()

# tty flags normally change \n to \r\n
attrs = termios.tcgetattr(self.r)
attrs[1] &= ~(termios.ONLCR | termios.OPOST)
termios.tcsetattr(self.r, termios.TCSANOW, attrs)

return self

def close_w(self):
if self.w is not None:
os.close(self.w)
self.w = None

def close_r(self):
assert self.r is not None
os.close(self.r)
self.r = None

def __exit__(self, exc_type, exc_value, traceback):
self.close_w()
self.close_r()

def cmd_output_p(*cmd, **kwargs):
assert kwargs.pop('retcode') is None
assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr']
cmd, kwargs = _cmd_kwargs(*cmd, **kwargs)

try:
cmd = parse_shebang.normalize_cmd(cmd)
except parse_shebang.ExecutableNotFoundError as e:
return e.to_output()

with open(os.devnull) as devnull, Pty() as pty:
kwargs.update({'stdin': devnull, 'stdout': pty.w, 'stderr': pty.w})
proc = subprocess.Popen(cmd, **kwargs)
pty.close_w()

buf = b''
while True:
try:
bts = os.read(pty.r, 4096)
except OSError as e:
if e.errno == errno.EIO:
bts = b''
else:
raise
else:
buf += bts
if not bts:
break

return proc.wait(), buf, None
else: # pragma: no cover
cmd_output_p = cmd_output_b


def rmtree(path):
"""On windows, rmtree fails for readonly dirs."""
def handle_remove_readonly(func, path, exc):
Expand Down
5 changes: 4 additions & 1 deletion pre_commit/xargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from pre_commit import parse_shebang
from pre_commit.util import cmd_output_b
from pre_commit.util import cmd_output_p


def _environ_size(_env=None):
Expand Down Expand Up @@ -108,9 +109,11 @@ def xargs(cmd, varargs, **kwargs):
negate: Make nonzero successful and zero a failure
target_concurrency: Target number of partitions to run concurrently
"""
color = kwargs.pop('color', False)
negate = kwargs.pop('negate', False)
target_concurrency = kwargs.pop('target_concurrency', 1)
max_length = kwargs.pop('_max_length', _get_platform_max_length())
cmd_fn = cmd_output_p if color else cmd_output_b
retcode = 0
stdout = b''

Expand All @@ -122,7 +125,7 @@ def xargs(cmd, varargs, **kwargs):
partitions = partition(cmd, varargs, target_concurrency, max_length)

def run_cmd_partition(run_cmd):
return cmd_output_b(
return cmd_fn(
*run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs
)

Expand Down
Loading