Repository: pre-commit/pre-commit

Issue:
Fix lots of files problem
https://github.com/pre-commit/pre-commit/blob/master/pre_commit/languages/helpers.py#L4

Fix is prefix with xargs and push filenames into stdin



Discussion:

Resolving patch:
diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py
--- a/pre_commit/languages/helpers.py
+++ b/pre_commit/languages/helpers.py
@@ -1,6 +1,37 @@
 
+import subprocess
+
+
 def run_hook(env, hook, file_args):
     return env.run(
-        ' '.join([hook['entry']] + hook.get('args', []) + list(file_args)),
-        retcode=None,
-    )
\ No newline at end of file
+        ' '.join(['xargs', hook['entry']] + hook.get('args', [])),
+        stdin='\n'.join(list(file_args) + ['']),
+    )
+
+
+class Environment(object):
+    @property
+    def env_prefix(self):
+        """env_prefix is a value that is prefixed to the command that is run.
+
+        Usually this is to source a virtualenv, etc.
+
+        Commands basically end up looking like:
+
+        bash -c '{env_prefix} {cmd}'
+
+        so you'll often want to end your prefix with &&
+        """
+        raise NotImplementedError
+
+    def run(self, cmd, stdin=None, **kwargs):
+        """Returns (returncode, stdout, stderr)."""
+        proc = subprocess.Popen(
+            ['bash', '-c', ' '.join([self.env_prefix, cmd])],
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+        )
+        stdout, stderr = proc.communicate(stdin)
+
+        return proc.returncode, stdout, stderr
diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py
--- a/pre_commit/languages/node.py
+++ b/pre_commit/languages/node.py
@@ -8,37 +8,42 @@
 NODE_ENV = 'node_env'
 
 
-class NodeEnv(object):
-    def __init__(self, py_env):
-        self.py_env = py_env
-        self.env_prefix = '. {0}/bin/activate &&'.format(NODE_ENV)
-
-    def run(self, cmd, **kwargs):
-        return self.py_env.run(' '.join([self.env_prefix, cmd]), **kwargs)
+class NodeEnv(python.PythonEnv):
+    @property
+    def env_prefix(self):
+        base = super(NodeEnv, self).env_prefix
+        return ' '.join([base, '. {0}/bin/activate &&'.format(NODE_ENV)])
 
 
 @contextlib.contextmanager
-def in_env(py_env):
-    yield NodeEnv(py_env)
+def in_env():
+    yield NodeEnv()
 
 
 def install_environment():
     assert local.path('package.json').exists()
 
-    if local.path('node_env').exists():
+    if local.path(NODE_ENV).exists():
         return
 
     local['virtualenv'][python.PY_ENV]()
 
     with python.in_env() as python_env:
         python_env.run('pip install nodeenv')
-        python_env.run('nodeenv --jobs 4 {0}'.format(NODE_ENV))
 
-        with in_env(python_env) as node_env:
+        try:
+            # Try and use the system level node executable first
+            python_env.run('nodeenv -n system {0}'.format(NODE_ENV))
+        except Exception:
+            # TODO: log exception here
+            # cleanup
+            local.path(NODE_ENV).remove()
+            python_env.run('nodeenv --jobs 4 {0}'.format(NODE_ENV))
+
+        with in_env() as node_env:
             node_env.run('npm install -g')
 
 
 def run_hook(hook, file_args):
-    with python.in_env() as py_env:
-        with in_env(py_env) as node_env:
-            return helpers.run_hook(node_env, hook, file_args)
\ No newline at end of file
+    with in_env() as node_env:
+        return helpers.run_hook(node_env, hook, file_args)
diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py
--- a/pre_commit/languages/python.py
+++ b/pre_commit/languages/python.py
@@ -6,12 +6,10 @@
 PY_ENV = 'py_env'
 
 
-class PythonEnv(object):
-    def __init__(self):
-        self.env_prefix = '. {0}/bin/activate &&'.format(PY_ENV)
-
-    def run(self, cmd, **kwargs):
-        return local['bash']['-c', ' '.join([self.env_prefix, cmd])].run(**kwargs)
+class PythonEnv(helpers.Environment):
+    @property
+    def env_prefix(self):
+        return '. {0}/bin/activate &&'.format(PY_ENV)
 
 
 @contextlib.contextmanager


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -67,6 +67,8 @@ def python_pre_commit_git_repo(dummy_git_repo):
         local.path('__init__.py').write('')
         local.path('main.py').write("""
 def func():
+    import sys
+    print repr(sys.argv[1:])
     print 'Hello World'
     return 0
 """)
@@ -142,4 +144,4 @@ def config_for_python_pre_commit_git_repo(python_pre_commit_git_repo):
 
     jsonschema.validate([config], CONFIG_JSON_SCHEMA)
 
-    return config
\ No newline at end of file
+    return config
diff --git a/tests/repository_test.py b/tests/repository_test.py
--- a/tests/repository_test.py
+++ b/tests/repository_test.py
@@ -46,13 +46,25 @@ def test_install_python_repo_in_env(python_pre_commit_git_repo, config_for_pytho
 def test_run_a_python_hook(config_for_python_pre_commit_git_repo):
     repo = Repository(config_for_python_pre_commit_git_repo)
     repo.install()
-    ret = repo.run_hook('foo', [])
+    ret = repo.run_hook('foo', ['/dev/null'])
+
+    assert ret[0] == 0
+    assert ret[1] == "['/dev/null']\nHello World\n"
+
+
+@pytest.mark.integration
+def test_run_a_hook_lots_of_files(config_for_python_pre_commit_git_repo):
+    repo = Repository(config_for_python_pre_commit_git_repo)
+    repo.install()
+    ret = repo.run_hook('foo', ['/dev/null'] * 15000)
 
     assert ret[0] == 0
-    assert ret[1] == 'Hello World\n'
 
 
-@pytest.mark.skipif(True, reason="TODO: make this test not super slow")
+@pytest.mark.skipif(
+    os.environ.get('slowtests', None) == 'false',
+    reason="TODO: make this test not super slow",
+)
 def test_run_a_node_hook(config_for_node_pre_commit_git_repo):
     repo = Repository(config_for_node_pre_commit_git_repo)
     repo.install()