Repository: pre-commit/pre-commit

Issue:
"git add -N" prevents restoring stashed changes.
To reproduce, start with this simple `pre-commit-config.yaml` in an otherwise empty repo:

```yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: master
  hooks: 
    - id: end-of-file-fixer
```

The hook used doesn't really matter. end-of-file-fixer is just an example. 

Run the following:
```bash
echo "new" > newfile
echo "\n\n\n" > needs-fixing
git add -N newfile
# newfile is now staged as an empty file
git add needs-fixing
git commit -m "fix"
```

The following output is generated: 

```
[WARNING] Unstaged files detected.
[INFO] Stashing unstaged files to /home/henniss/.cache/pre-commit/patch1544663784.
Fix End of Files.........................................................Failed
hookid: end-of-file-fixer

Files were modified by this hook. Additional output:

Fixing needs-fixing

[WARNING] Stashed changes conflicted with hook auto-fixes... Rolling back fixes...
An unexpected error has occurred: CalledProcessError: Command: ('/usr/lib/git-core/git', '-c', 'core.autocrlf=false', 'apply', '--whitespace=nowarn', '/home/henniss/.cache/pre-commit/patch1544663784')
Return code: 1
Expected return code: 0
Output: (none)
Errors:
    error: newfile: already exists in working directory


Check the log at /home/henniss/.cache/pre-commit/pre-commit.log
```

`cat newfile` now shows that it is empty. The unstaged changes aren't restored.

Discussion:

Patch:
diff --git a/pre_commit/git.py b/pre_commit/git.py
index ccdd185..f0b5040 100644
--- a/pre_commit/git.py
+++ b/pre_commit/git.py
@@ -97,6 +97,20 @@ def get_staged_files():
     )[1])
 
 
+def intent_to_add_files():
+    _, stdout_binary, _ = cmd_output('git', 'status', '--porcelain', '-z')
+    parts = list(reversed(zsplit(stdout_binary)))
+    intent_to_add = []
+    while parts:
+        line = parts.pop()
+        status, filename = line[:3], line[3:]
+        if status[0] in {'C', 'R'}:  # renames / moves have an additional arg
+            parts.pop()
+        if status[1] == 'A':
+            intent_to_add.append(filename)
+    return intent_to_add
+
+
 def get_all_files():
     return zsplit(cmd_output('git', 'ls-files', '-z')[1])
 
diff --git a/pre_commit/staged_files_only.py b/pre_commit/staged_files_only.py
index 1d0c364..7af319d 100644
--- a/pre_commit/staged_files_only.py
+++ b/pre_commit/staged_files_only.py
@@ -6,9 +6,11 @@ import logging
 import os.path
 import time
 
+from pre_commit import git
 from pre_commit.util import CalledProcessError
 from pre_commit.util import cmd_output
 from pre_commit.util import mkdirp
+from pre_commit.xargs import xargs
 
 
 logger = logging.getLogger('pre_commit')
@@ -24,11 +26,22 @@ def _git_apply(patch):
 
 
 @contextlib.contextmanager
-def staged_files_only(patch_dir):
-    """Clear any unstaged changes from the git working directory inside this
-    context.
-    """
-    # Determine if there are unstaged files
+def _intent_to_add_cleared():
+    intent_to_add = git.intent_to_add_files()
+    if intent_to_add:
+        logger.warning('Unstaged intent-to-add files detected.')
+
+        xargs(('git', 'rm', '--cached', '--'), intent_to_add)
+        try:
+            yield
+        finally:
+            xargs(('git', 'add', '--intent-to-add', '--'), intent_to_add)
+    else:
+        yield
+
+
+@contextlib.contextmanager
+def _unstaged_changes_cleared(patch_dir):
     tree = cmd_output('git', 'write-tree')[1].strip()
     retcode, diff_stdout_binary, _ = cmd_output(
         'git', 'diff-index', '--ignore-submodules', '--binary',
@@ -71,3 +84,12 @@ def staged_files_only(patch_dir):
         # There weren't any staged files so we don't need to do anything
         # special
         yield
+
+
+@contextlib.contextmanager
+def staged_files_only(patch_dir):
+    """Clear any unstaged changes from the git working directory inside this
+    context.
+    """
+    with _intent_to_add_cleared(), _unstaged_changes_cleared(patch_dir):
+        yield


Test patch:
diff --git a/tests/git_test.py b/tests/git_test.py
index a78b745..43f1c15 100644
--- a/tests/git_test.py
+++ b/tests/git_test.py
@@ -155,3 +155,21 @@ def test_get_conflicted_files_non_ascii(in_merge_conflict):
     cmd_output('git', 'add', '.')
     ret = git.get_conflicted_files()
     assert ret == {'conflict_file', 'интервью'}
+
+
+def test_intent_to_add(in_git_dir):
+    in_git_dir.join('a').ensure()
+    cmd_output('git', 'add', '--intent-to-add', 'a')
+
+    assert git.intent_to_add_files() == ['a']
+
+
+def test_status_output_with_rename(in_git_dir):
+    in_git_dir.join('a').write('1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n')
+    cmd_output('git', 'add', 'a')
+    git_commit()
+    cmd_output('git', 'mv', 'a', 'b')
+    in_git_dir.join('c').ensure()
+    cmd_output('git', 'add', '--intent-to-add', 'c')
+
+    assert git.intent_to_add_files() == ['c']
diff --git a/tests/staged_files_only_test.py b/tests/staged_files_only_test.py
index 619d739..2410bff 100644
--- a/tests/staged_files_only_test.py
+++ b/tests/staged_files_only_test.py
@@ -9,6 +9,7 @@ import shutil
 
 import pytest
 
+from pre_commit import git
 from pre_commit.staged_files_only import staged_files_only
 from pre_commit.util import cmd_output
 from testing.auto_namedtuple import auto_namedtuple
@@ -339,3 +340,14 @@ def test_autocrlf_commited_crlf(in_git_dir, patch_dir):
 
     with staged_files_only(patch_dir):
         assert_no_diff()
+
+
+def test_intent_to_add(in_git_dir, patch_dir):
+    """Regression test for #881"""
+    _write(b'hello\nworld\n')
+    cmd_output('git', 'add', '--intent-to-add', 'foo')
+
+    assert git.intent_to_add_files() == ['foo']
+    with staged_files_only(patch_dir):
+        assert_no_diff()
+    assert git.intent_to_add_files() == ['foo']