diff --git a/pre_commit/git.py b/pre_commit/git.py index ccdd1856..f0b50404 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 1d0c3648..7af319d7 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 diff --git a/tests/git_test.py b/tests/git_test.py index a78b7458..43f1c156 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 619d739b..2410bffe 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']