Repository: pre-commit/pre-commit

Issue:
A config option to store hook output into a file
Hi, this is a feature request and if people would like it then I will submit a pull request.

What I would like is to be able to specify in the config an output file on  a per hook basis. When the hook is run, it will output to that file (as well as to STDOUT as currently).

The reason I want this is because I want to run pre-commit on Jenkins and I want the reports of certain tools to be stored in files so that they can be easily consumed by Jenkins. 


Discussion:
This sounds reasonable.

You _can_ hack it into the current scheme but it's not pretty nor very portable (without first-class support):

```yaml
-   repo: https://github.com/pre-commit/pre-commit-hooks
    sha: v0.7.1
    hooks:
    -   id: flake8
        entry: bash -c -o pipefail 'exec flake8 $@ |& tee -a flake8.log' --
```

To verify:

```
# important, since we use `-a` to append to the log (since pre-commit will invoke 1..many times)
rm flake8.log
pre-commit run flake8 --all-files || true  # if you want to demonstrate a failure writing to the log
cat flake8.log
```

For me (after breaking a file) this looks like this:
```
$ rm flake8.log 
$ pre-commit run flake8 --all-files 
Flake8...................................................................Failed
hookid: flake8

setup.py:1:1: F401 'os' imported but unused

$ cat flake8.log 
setup.py:1:1: F401 'os' imported but unused
```


As for first class, I think if you want to implement this I'd accept something like this:

```yaml
    -   id: flake8
        log_file: flake8.log
```

Note that pre-commit combines stdout and stderr, so you'd get combined stdout / stderr into that log file.


Resolving patch:
diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py
--- a/pre_commit/clientlib.py
+++ b/pre_commit/clientlib.py
@@ -53,6 +53,7 @@ def _make_argparser(filenames_help):
         '^$',
     ),
     schema.Optional('language_version', schema.check_string, 'default'),
+    schema.OptionalNoDefault('log_file', schema.check_string),
     schema.Optional('minimum_pre_commit_version', schema.check_string, '0'),
     schema.Optional('stages', schema.check_array(schema.check_string), []),
 )
diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py
--- a/pre_commit/commands/run.py
+++ b/pre_commit/commands/run.py
@@ -121,7 +121,10 @@ def _run_single_hook(hook, repo, args, skips, cols):
         for out in (stdout, stderr):
             assert type(out) is bytes, type(out)
             if out.strip():
-                output.write_line(out.strip())
+                output.write_line(
+                    out.strip(),
+                    logfile_name=hook.get('log_file'),
+                )
         output.write_line()
 
     return retcode
diff --git a/pre_commit/output.py b/pre_commit/output.py
--- a/pre_commit/output.py
+++ b/pre_commit/output.py
@@ -71,8 +71,17 @@ def write(s, stream=stdout_byte_stream):
     stream.flush()
 
 
-def write_line(s=None, stream=stdout_byte_stream):
-    if s is not None:
-        stream.write(five.to_bytes(s))
-    stream.write(b'\n')
-    stream.flush()
+def write_line(s=None, stream=stdout_byte_stream, logfile_name=None):
+    def output_streams():
+        yield stream
+        try:
+            with open(logfile_name, 'ab') as logfile:
+                yield logfile
+        except (TypeError, IOError):
+            pass
+
+    for output_stream in output_streams():
+        if s is not None:
+            output_stream.write(five.to_bytes(s))
+        output_stream.write(b'\n')
+        output_stream.flush()


Test patch:
diff --git a/testing/resources/logfile_repo/.pre-commit-hooks.yaml b/testing/resources/logfile_repo/.pre-commit-hooks.yaml
new file mode 100644
--- /dev/null
+++ b/testing/resources/logfile_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,6 @@
+-   id: logfile test hook
+    name: Logfile test hook
+    entry: bin/hook.sh
+    language: script
+    files: .
+    log_file: test.log
diff --git a/testing/resources/logfile_repo/bin/hook.sh b/testing/resources/logfile_repo/bin/hook.sh
new file mode 100755
--- /dev/null
+++ b/testing/resources/logfile_repo/bin/hook.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+echo "This is STDOUT output"
+echo "This is STDERR output" 1>&2
+
+exit 1
diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py
--- a/tests/commands/run_test.py
+++ b/tests/commands/run_test.py
@@ -211,6 +211,35 @@ def test_run(
     )
 
 
+def test_run_output_logfile(
+        cap_out,
+        tempdir_factory,
+        mock_out_store_directory,
+):
+
+    expected_output = (
+        b'This is STDOUT output\n',
+        b'This is STDERR output\n',
+    )
+
+    git_path = make_consuming_repo(tempdir_factory, 'logfile_repo')
+    with cwd(git_path):
+        _test_run(
+            cap_out,
+            git_path, {},
+            expected_output,
+            expected_ret=1,
+            stage=True
+        )
+    logfile_path = os.path.join(git_path, 'test.log')
+    assert os.path.exists(logfile_path)
+    with open(logfile_path, 'rb') as logfile:
+        logfile_content = logfile.readlines()
+
+    for expected_output_part in expected_output:
+        assert expected_output_part in logfile_content
+
+
 def test_always_run(
         cap_out, repo_with_passing_hook, mock_out_store_directory,
 ):