Repository: pre-commit/pre-commit

Issue:
Add option to print diff if hooks failed and files were changed.
I'm currently debugging a pre-commit failure in only one Python version that happens on Travis.

Not sure if it is out of scope of the project, but it would be handy to have some kind of `--print-diff` option that simply prints a diff of the files on failure. It would generally only be useful in CI when you can't just go back and inspect what changed.

Currently all I see in Travis logs is

```
Reordering imports in pre_commit/main.py
Reordering imports in pre_commit/clientlib/validate_base.py
Reordering imports in tests/main_test.py
```

which doesn't really tell me what changed or give me useful hints on tracking down the problem.

As an example, here's the actual diff. It's clear now what actually happened and why this only happened on Python 2.6:

```
diff --git a/pre_commit/clientlib/validate_base.py b/pre_commit/clientlib/validate_base.py
index 707bdde..af18725 100644
--- a/pre_commit/clientlib/validate_base.py
+++ b/pre_commit/clientlib/validate_base.py
@@ -1,11 +1,11 @@
 from __future__ import print_function
 from __future__ import unicode_literals

-import argparse
 import os.path
 import re
 import sys

+import argparse
 import jsonschema
 import jsonschema.exceptions
 import yaml
```



Discussion:
Would running `git diff` after all the hooks satisfy this?


Resolving patch:
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
@@ -3,6 +3,7 @@
 
 import logging
 import os
+import subprocess
 import sys
 
 from pre_commit import color
@@ -152,6 +153,13 @@ def _run_hooks(repo_hooks, args, environ):
     retval = 0
     for repo, hook in repo_hooks:
         retval |= _run_single_hook(hook, repo, args, skips, cols)
+    if (
+            retval and
+            args.show_diff_on_failure and
+            subprocess.call(('git', 'diff', '--quiet')) != 0
+    ):
+        print('All changes made by hooks:')
+        subprocess.call(('git', 'diff'))
     return retval
 
 
diff --git a/pre_commit/main.py b/pre_commit/main.py
--- a/pre_commit/main.py
+++ b/pre_commit/main.py
@@ -149,6 +149,10 @@ def main(argv=None):
         '--hook-stage', choices=('commit', 'push'), default='commit',
         help='The stage during which the hook is fired e.g. commit or push.',
     )
+    run_parser.add_argument(
+        '--show-diff-on-failure', action='store_true',
+        help='When hooks fail, run `git diff` directly afterward.',
+    )
     run_mutex_group = run_parser.add_mutually_exclusive_group(required=False)
     run_mutex_group.add_argument(
         '--all-files', '-a', action='store_true', default=False,


Test patch:
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
@@ -58,6 +58,7 @@ def _get_opts(
         source='',
         allow_unstaged_config=False,
         hook_stage='commit',
+        show_diff_on_failure=False,
 ):
     # These are mutually exclusive
     assert not (all_files and files)
@@ -67,11 +68,12 @@ def _get_opts(
         color=color,
         verbose=verbose,
         hook=hook,
-        hook_stage=hook_stage,
         no_stash=no_stash,
         origin=origin,
         source=source,
         allow_unstaged_config=allow_unstaged_config,
+        hook_stage=hook_stage,
+        show_diff_on_failure=show_diff_on_failure,
     )
 
 
@@ -151,6 +153,23 @@ def test_hook_that_modifies_but_returns_zero(
         )
 
 
+def test_show_diff_on_failure(
+        capfd, cap_out, tempdir_factory, mock_out_store_directory,
+):
+    git_path = make_consuming_repo(
+        tempdir_factory, 'modified_file_returns_zero_repo',
+    )
+    with cwd(git_path):
+        stage_a_file('bar.py')
+        _test_run(
+            cap_out, git_path, {'show_diff_on_failure': True},
+            # we're only testing the output after running
+            (), 1, True,
+        )
+    out, _ = capfd.readouterr()
+    assert 'diff --git' in out
+
+
 @pytest.mark.parametrize(
     ('options', 'outputs', 'expected_ret', 'stage'),
     (