Repository: pre-commit/pre-commit

Issue:
pre-commit crashes when running during unresolved merge conflict
I intentionally forced the following by making two branches conflict and then editing a file on that branch.  `pre-commit` should fail-fast in a merge conflict situation.

```
$ git diff --exit-code
diff --cc foo.txt
index 8ff26e7,c148433..0000000
--- a/foo.txt
+++ b/foo.txt
@@@ -1,4 -1,5 +1,11 @@@
  asdf
++<<<<<<< HEAD
 +fdsa
 +yeah
 +yeah
++=======
+ asdf
+ asdf
+ asdf
+ 
++>>>>>>> derp
diff --git a/git_code_debt/generate.py b/git_code_debt/generate.py
index 12ceec6..967506e 100644
--- a/git_code_debt/generate.py
+++ b/git_code_debt/generate.py
@@ -12,6 +12,7 @@ from git_code_debt.logic import get_previous_sha
 from git_code_debt.logic import insert_metric_values
 from git_code_debt.repo_parser import RepoParser

+
 def get_metrics(diff, metric_parsers):
     def get_all_metrics(file_diff_stats):
         for metric_parser_cls in metric_parsers:
(py_env)[anthony@anthony-VirtualBox git-code-debt (herp|MERGING)]$ echo $?
1
(py_env)[anthony@anthony-VirtualBox git-code-debt (herp|MERGING)]$ pre-commit 
[WARNING] Unstaged files detected.
[INFO] Stashing unstaged files to /tmp/git-code-debt/.pre-commit-files/patch1397455577.
Traceback (most recent call last):
  File "/tmp/git-code-debt/py_env/bin/pre-commit", line 9, in <module>
    load_entry_point('pre-commit==0.0.0', 'console_scripts', 'pre-commit')()
  File "/tmp/git-code-debt/py_env/local/lib/python2.7/site-packages/pre_commit/util.py", line 52, in wrapper
    return func(argv)
  File "/tmp/git-code-debt/py_env/local/lib/python2.7/site-packages/pre_commit/run.py", line 59, in run
    return commands.run(runner, args)
  File "/tmp/git-code-debt/py_env/local/lib/python2.7/site-packages/pre_commit/commands.py", line 242, in run
    with ctx:
  File "/usr/lib/python2.7/contextlib.py", line 17, in __enter__
    return self.gen.next()
  File "/tmp/git-code-debt/py_env/local/lib/python2.7/site-packages/pre_commit/staged_files_only.py", line 35, in staged_files_only
    cmd_runner.run(['git', 'checkout', '--', '.'])
  File "/tmp/git-code-debt/py_env/local/lib/python2.7/site-packages/pre_commit/prefixed_command_runner.py", line 77, in run
    returncode, replaced_cmd, retcode, output=(stdout, stderr),
pre_commit.prefixed_command_runner.CalledProcessError: Command: ['git', 'checkout', '--', '.']
Return code: 1
Expected return code: 0
Output: (u'', u"error: path 'foo.txt' is unmerged\n")

(py_env)[anthony@anthony-VirtualBox git-code-debt (herp|MERGING)]$ 
```



Discussion:

Resolving patch:
diff --git a/pre_commit/commands.py b/pre_commit/commands.py
--- a/pre_commit/commands.py
+++ b/pre_commit/commands.py
@@ -229,11 +229,21 @@ def _run_hook(runner, hook_id, args, write):
         return 1
 
 
+def _has_unmerged_paths(runner):
+    _, stdout, _ = runner.cmd_runner.run(['git', 'ls-files', '--unmerged'])
+    return bool(stdout.strip())
+
+
 def run(runner, args, write=sys.stdout.write):
     # Set up our logging handler
     logger.addHandler(LoggingHandler(args.color, write=write))
     logger.setLevel(logging.INFO)
 
+    # Check if we have unresolved merge conflict files and fail fast.
+    if _has_unmerged_paths(runner):
+        logger.error('Unmerged files.  Resolve before committing.')
+        return 1
+
     if args.no_stash or args.all_files:
         ctx = noop_context()
     else:


Test patch:
diff --git a/tests/commands_test.py b/tests/commands_test.py
--- a/tests/commands_test.py
+++ b/tests/commands_test.py
@@ -275,3 +275,47 @@ def test_no_stash(repo_with_passing_hook, no_stash, all_files, expect_stash):
         assert warning_msg in printed
     else:
         assert warning_msg not in printed
+
+
+@pytest.mark.parametrize(('output', 'expected'), (('some', True), ('', False)))
+def test_has_unmerged_paths(output, expected):
+    mock_runner = mock.Mock()
+    mock_runner.cmd_runner.run.return_value = (1, output, '')
+    assert commands._has_unmerged_paths(mock_runner) is expected
+
+
+@pytest.yield_fixture
+def in_merge_conflict(repo_with_passing_hook):
+    local['git']['add', C.CONFIG_FILE]()
+    local['git']['commit', '-m' 'add hooks file']()
+    local['git']['clone', '.', 'foo']()
+    with local.cwd('foo'):
+        local['git']['checkout', 'origin/master', '-b', 'foo']()
+        with open('conflict_file', 'w') as conflict_file:
+            conflict_file.write('herp\nderp\n')
+        local['git']['add', 'conflict_file']()
+        local['git']['commit', '-m', 'conflict_file']()
+        local['git']['checkout', 'origin/master', '-b', 'bar']()
+        with open('conflict_file', 'w') as conflict_file:
+            conflict_file.write('harp\nddrp\n')
+        local['git']['add', 'conflict_file']()
+        local['git']['commit', '-m', 'conflict_file']()
+        local['git']['merge', 'foo'](retcode=None)
+        yield os.path.join(repo_with_passing_hook, 'foo')
+
+
+def test_merge_conflict(in_merge_conflict):
+    ret, printed = _do_run(in_merge_conflict, _get_opts())
+    assert ret == 1
+    assert 'Unmerged files.  Resolve before committing.' in printed
+
+
+def test_merge_conflict_modified(in_merge_conflict):
+    # Touch another file so we have unstaged non-conflicting things
+    assert os.path.exists('dummy')
+    with open('dummy', 'w') as dummy_file:
+        dummy_file.write('bar\nbaz\n')
+
+    ret, printed = _do_run(in_merge_conflict, _get_opts())
+    assert ret == 1
+    assert 'Unmerged files.  Resolve before committing.' in printed
diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py
--- a/tests/staged_files_only_test.py
+++ b/tests/staged_files_only_test.py
@@ -227,7 +227,7 @@ def __init__(self):
             self.logs = []
 
         def emit(self, record):
-            self.logs.append(record)
+            self.logs.append(record)  # pragma: no cover (only hit in failure)
 
     pre_commit_logger = logging.getLogger('pre_commit')
     original_level = pre_commit_logger.getEffectiveLevel()