Repository: pre-commit/pre-commit
Issue: Occasional flakiness of staged file stasher

It appears `git diff-files` is returning incorrectly in some case that I haven't been able to pinpoint.

It results in something like this (you can see however that all the files are staged):

```
$ pre-commit 
[WARNING] Unstaged files detected.
Stashing unstaged files to /home/anthony/workspace/pre-commit/.pre-commit-files/patch1397370090.
Trim Trailing Whitespace............................................Passed
Fix End of Files....................................................Passed
Check Yaml..........................................................Passed
Debug Statements (Python)...........................................Passed
Tests should end in _test.py........................................Passed
Pyflakes............................................................Passed
Validate Pre-Commit Config..........................................Passed
Validate Pre-Commit Manifest........................................Passed
[WARNING] Stashed changes conflicted with hook auto-fixes... Rolling back fixes...
Traceback (most recent call last):
  File "/home/anthony/workspace/pre-commit/venv-pre_commit/bin/pre-commit", line 9, in <module>
    load_entry_point('pre-commit==0.0.0', 'console_scripts', 'pre-commit')()
  File "/home/anthony/workspace/pre-commit/pre_commit/util.py", line 52, in wrapper
    return func(argv)
  File "/home/anthony/workspace/pre-commit/pre_commit/run.py", line 143, in run
    return _run(runner, args)
  File "/home/anthony/workspace/pre-commit/pre_commit/run.py", line 95, in _run
    return run_hooks(runner, args)
  File "/usr/lib/python2.7/contextlib.py", line 24, in __exit__
    self.gen.next()
  File "/home/anthony/workspace/pre-commit/pre_commit/staged_files_only.py", line 51, in staged_files_only
    cmd_runner.run(['git', 'apply', patch_filename])
  File "/home/anthony/workspace/pre-commit/pre_commit/prefixed_command_runner.py", line 67, in run
    returncode, replaced_cmd, retcode, output=(stdout, stderr),
pre_commit.prefixed_command_runner.CalledProcessError: Command: ['git', 'apply', '/home/anthony/workspace/pre-commit/.pre-commit-files/patch1397370090']
Return code: 128
Expected return code: 0
Output: ('', 'fatal: unrecognized input\\n')
$ git status
# On branch rebuild_venv
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   modified:   .gitignore
#   modified:   Makefile
#
```

The "stashed diff" is an empty file.  I think the "fix" is to check if the diff contains anything before printing the warning message and entering the branch that isn't a noop context manager.


Changed files:
pre_commit/staged_files_only.py, tests/staged_files_only_test.py

diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py
index f3e3482..8d27d9d 100644
--- a/pre_commit/staged_files_only.py
+++ b/pre_commit/staged_files_only.py
@@ -17,11 +17,11 @@ def staged_files_only(cmd_runner):
         cmd_runner - PrefixedCommandRunner
     """
     # Determine if there are unstaged files
-    retcode, _, _ = cmd_runner.run(
-        ['git', 'diff-files', '--quiet'],
+    retcode, diff_stdout, _ = cmd_runner.run(
+        ['git', 'diff', '--ignore-submodules', '--binary', '--exit-code'],
         retcode=None,
     )
-    if retcode:
+    if retcode and diff_stdout.strip():
         patch_filename = cmd_runner.path('patch{0}'.format(int(time.time())))
         logger.warning('Unstaged files detected.')
         logger.info(
@@ -29,7 +29,7 @@ def staged_files_only(cmd_runner):
         )
         # Save the current unstaged changes as a patch
         with open(patch_filename, 'w') as patch_file:
-            cmd_runner.run(['git', 'diff', '--binary'], stdout=patch_file)
+            patch_file.write(diff_stdout)
 
         # Clear the working directory of unstaged changes
         cmd_runner.run(['git', 'checkout', '--', '.'])
diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py
index 5722af3..adb0b01 100644
--- a/tests/staged_files_only_test.py
+++ b/tests/staged_files_only_test.py
@@ -1,3 +1,5 @@
+import logging
+import mock
 import os.path
 import pytest
 import shutil
@@ -215,3 +217,32 @@ def test_sub_something_unstaged(sub_staged, cmd_runner):
         _test_sub_state(sub_staged, 'sha2', 'AM')
 
     _test_sub_state(sub_staged, 'sha2', 'AM')
+
+
+@pytest.yield_fixture
+def fake_logging_handler():
+    class FakeHandler(logging.Handler):
+        def __init__(self):
+            logging.Handler.__init__(self)
+            self.logs = []
+
+        def emit(self, record):
+            self.logs.append(record)
+
+    pre_commit_logger = logging.getLogger('pre_commit')
+    original_level = pre_commit_logger.getEffectiveLevel()
+    handler = FakeHandler()
+    pre_commit_logger.addHandler(handler)
+    pre_commit_logger.setLevel(logging.WARNING)
+    yield handler
+    pre_commit_logger.setLevel(original_level)
+    pre_commit_logger.removeHandler(handler)
+
+
+def test_diff_returns_1_no_diff_though(fake_logging_handler, foo_staged):
+    cmd_runner = mock.Mock()
+    cmd_runner.run.return_value = (1, '', '')
+    cmd_runner.path.return_value = '.pre-commit-files_patch'
+    with staged_files_only(cmd_runner):
+        pass
+    assert not fake_logging_handler.logs