forbid-new-submodules: fix triggering failure when only a submodule is committed (without any other file); support --from-ref and --to-ref; fixes #609
diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml
index 91dbdf0..646a0bb 100644
--- a/.pre-commit-hooks.yaml
+++ b/.pre-commit-hooks.yaml
@@ -154,6 +154,7 @@
     language: python
     entry: forbid-new-submodules
     description: Prevent addition of new git submodules
+    types: [directory]
 -   id: mixed-line-ending
     name: Mixed line ending
     description: Replaces or checks mixed line ending
diff --git a/pre_commit_hooks/forbid_new_submodules.py b/pre_commit_hooks/forbid_new_submodules.py
index c144d72..9d1aa2f 100644
--- a/pre_commit_hooks/forbid_new_submodules.py
+++ b/pre_commit_hooks/forbid_new_submodules.py
@@ -1,3 +1,5 @@
+import argparse
+import os
 from typing import Optional
 from typing import Sequence
 
@@ -5,10 +7,23 @@
 
 
 def main(argv: Optional[Sequence[str]] = None) -> int:
-    # `argv` is ignored, pre-commit will send us a list of files that we
-    # don't care about
+    parser = argparse.ArgumentParser()
+    parser.add_argument('filenames', nargs='*')
+    args = parser.parse_args(argv)
+
+    if (
+        'PRE_COMMIT_FROM_REF' in os.environ and
+        'PRE_COMMIT_TO_REF' in os.environ
+    ):
+        diff_arg = '...'.join((
+            os.environ['PRE_COMMIT_FROM_REF'],
+            os.environ['PRE_COMMIT_TO_REF'],
+        ))
+    else:
+        diff_arg = '--staged'
     added_diff = cmd_output(
-        'git', 'diff', '--staged', '--diff-filter=A', '--raw',
+        'git', 'diff', '--diff-filter=A', '--raw', diff_arg, '--',
+        *args.filenames,
     )
     retv = 0
     for line in added_diff.splitlines():
diff --git a/testing/util.py b/testing/util.py
index 8e468d6..5043754 100644
--- a/testing/util.py
+++ b/testing/util.py
@@ -1,4 +1,5 @@
 import os.path
+import subprocess
 
 
 TESTING_DIR = os.path.abspath(os.path.dirname(__file__))
@@ -6,3 +7,8 @@
 
 def get_resource_path(path):
     return os.path.join(TESTING_DIR, 'resources', path)
+
+
+def git_commit(*args, **kwargs):
+    cmd = ('git', 'commit', '--no-gpg-sign', '--no-verify', '--no-edit', *args)
+    subprocess.check_call(cmd, **kwargs)
diff --git a/tests/check_added_large_files_test.py b/tests/check_added_large_files_test.py
index d98d99b..d38c4f6 100644
--- a/tests/check_added_large_files_test.py
+++ b/tests/check_added_large_files_test.py
@@ -5,6 +5,7 @@
 from pre_commit_hooks.check_added_large_files import find_large_added_files
 from pre_commit_hooks.check_added_large_files import main
 from pre_commit_hooks.util import cmd_output
+from testing.util import git_commit
 
 
 def test_nothing_added(temp_git_dir):
@@ -104,7 +105,7 @@
         # First add the file we're going to move
         temp_git_dir.join('a.bin').write('a' * 10000)
         cmd_output('git', 'add', '--', '.')
-        cmd_output('git', 'commit', '--no-gpg-sign', '-am', 'foo')
+        git_commit('-am', 'foo')
         # Now move it and make sure the hook still succeeds
         cmd_output('git', 'mv', 'a.bin', 'b.bin')
         assert main(('--maxkb', '9', 'b.bin')) == 0
diff --git a/tests/check_case_conflict_test.py b/tests/check_case_conflict_test.py
index c8c9d12..d9211b5 100644
--- a/tests/check_case_conflict_test.py
+++ b/tests/check_case_conflict_test.py
@@ -6,6 +6,7 @@
 from pre_commit_hooks.check_case_conflict import main
 from pre_commit_hooks.check_case_conflict import parents
 from pre_commit_hooks.util import cmd_output
+from testing.util import git_commit
 
 skip_win32 = pytest.mark.skipif(
     sys.platform == 'win32',
@@ -85,7 +86,7 @@
     with temp_git_dir.as_cwd():
         temp_git_dir.join('f.py').write("print('hello world')")
         cmd_output('git', 'add', 'f.py')
-        cmd_output('git', 'commit', '--no-gpg-sign', '-n', '-m', 'Add f.py')
+        git_commit('-m', 'Add f.py')
 
         temp_git_dir.join('F.py').write("print('hello world')")
         cmd_output('git', 'add', 'F.py')
@@ -98,7 +99,7 @@
     with temp_git_dir.as_cwd():
         temp_git_dir.mkdir('dir').join('x').write('foo')
         cmd_output('git', 'add', '-A')
-        cmd_output('git', 'commit', '--no-gpg-sign', '-n', '-m', 'Add f.py')
+        git_commit('-m', 'Add f.py')
 
         temp_git_dir.join('DIR').write('foo')
         cmd_output('git', 'add', '-A')
diff --git a/tests/check_merge_conflict_test.py b/tests/check_merge_conflict_test.py
index fccf41f..de07bc1 100644
--- a/tests/check_merge_conflict_test.py
+++ b/tests/check_merge_conflict_test.py
@@ -6,6 +6,7 @@
 from pre_commit_hooks.check_merge_conflict import main
 from pre_commit_hooks.util import cmd_output
 from testing.util import get_resource_path
+from testing.util import git_commit
 
 
 @pytest.fixture
@@ -20,19 +21,19 @@
     with repo1.as_cwd():
         repo1_f1.ensure()
         cmd_output('git', 'add', '.')
-        cmd_output('git', 'commit', '--no-gpg-sign', '-m', 'commit1')
+        git_commit('-m', 'commit1')
 
     cmd_output('git', 'clone', str(repo1), str(repo2))
 
     # Commit in master
     with repo1.as_cwd():
         repo1_f1.write('parent\n')
-        cmd_output('git', 'commit', '--no-gpg-sign', '-am', 'master commit2')
+        git_commit('-am', 'master commit2')
 
     # Commit in clone and pull
     with repo2.as_cwd():
         repo2_f1.write('child\n')
-        cmd_output('git', 'commit', '--no-gpg-sign', '-am', 'clone commit2')
+        git_commit('-am', 'clone commit2')
         cmd_output('git', 'pull', '--no-rebase', retcode=None)
         # We should end up in a merge conflict!
         f1 = repo2_f1.read()
@@ -75,20 +76,20 @@
     with repo1.as_cwd():
         repo1_f1.ensure()
         cmd_output('git', 'add', '.')
-        cmd_output('git', 'commit', '--no-gpg-sign', '-m', 'commit1')
+        git_commit('-m', 'commit1')
 
     cmd_output('git', 'clone', str(repo1), str(repo2))
 
     # Commit in master
     with repo1.as_cwd():
         repo1_f1.write('parent\n')
-        cmd_output('git', 'commit', '--no-gpg-sign', '-am', 'master commit2')
+        git_commit('-am', 'master commit2')
 
     # Commit in clone and pull without committing
     with repo2.as_cwd():
         repo2_f2.write('child\n')
         cmd_output('git', 'add', '.')
-        cmd_output('git', 'commit', '--no-gpg-sign', '-m', 'clone commit2')
+        git_commit('-m', 'clone commit2')
         cmd_output('git', 'pull', '--no-commit', '--no-rebase')
         # We should end up in a pending merge
         assert repo2_f1.read() == 'parent\n'
diff --git a/tests/destroyed_symlinks_test.py b/tests/destroyed_symlinks_test.py
index d2c9031..cde06cf 100644
--- a/tests/destroyed_symlinks_test.py
+++ b/tests/destroyed_symlinks_test.py
@@ -5,6 +5,7 @@
 
 from pre_commit_hooks.destroyed_symlinks import find_destroyed_symlinks
 from pre_commit_hooks.destroyed_symlinks import main
+from testing.util import git_commit
 
 TEST_SYMLINK = 'test_symlink'
 TEST_SYMLINK_TARGET = '/doesnt/really/matters'
@@ -23,9 +24,7 @@
         with open(TEST_FILE, 'w') as f:
             print('some random content', file=f)
         subprocess.check_call(('git', 'add', '.'))
-        subprocess.check_call(
-            ('git', 'commit', '--no-gpg-sign', '-m', 'initial'),
-        )
+        git_commit('-m', 'initial')
         assert b'120000 ' in subprocess.check_output(
             ('git', 'cat-file', '-p', 'HEAD^{tree}'),
         )
diff --git a/tests/forbid_new_submodules_test.py b/tests/forbid_new_submodules_test.py
index 4871ae7..0326d94 100644
--- a/tests/forbid_new_submodules_test.py
+++ b/tests/forbid_new_submodules_test.py
@@ -1,22 +1,20 @@
+import os
 import subprocess
+from unittest import mock
 
 import pytest
 
 from pre_commit_hooks.forbid_new_submodules import main
+from testing.util import git_commit
 
 
 @pytest.fixture
 def git_dir_with_git_dir(tmpdir):
     with tmpdir.as_cwd():
         subprocess.check_call(('git', 'init', '.'))
-        subprocess.check_call((
-            'git', 'commit', '-m', 'init', '--allow-empty', '--no-gpg-sign',
-        ))
+        git_commit('--allow-empty', '-m', 'init')
         subprocess.check_call(('git', 'init', 'foo'))
-        subprocess.check_call(
-            ('git', 'commit', '-m', 'init', '--allow-empty', '--no-gpg-sign'),
-            cwd=str(tmpdir.join('foo')),
-        )
+        git_commit('--allow-empty', '-m', 'init', cwd=str(tmpdir.join('foo')))
         yield
 
 
@@ -31,7 +29,24 @@
 )
 def test_main_new_submodule(git_dir_with_git_dir, capsys, cmd):
     subprocess.check_call(cmd)
-    assert main() == 1
+    assert main(('random_non-related_file',)) == 0
+    assert main(('foo',)) == 1
+    out, _ = capsys.readouterr()
+    assert out.startswith('foo: new submodule introduced\n')
+
+
+def test_main_new_submodule_committed(git_dir_with_git_dir, capsys):
+    rev_parse_cmd = ('git', 'rev-parse', 'HEAD')
+    from_ref = subprocess.check_output(rev_parse_cmd).decode().strip()
+    subprocess.check_call(('git', 'submodule', 'add', './foo'))
+    git_commit('-m', 'new submodule')
+    to_ref = subprocess.check_output(rev_parse_cmd).decode().strip()
+    with mock.patch.dict(
+        os.environ,
+        {'PRE_COMMIT_FROM_REF': from_ref, 'PRE_COMMIT_TO_REF': to_ref},
+    ):
+        assert main(('random_non-related_file',)) == 0
+        assert main(('foo',)) == 1
     out, _ = capsys.readouterr()
     assert out.startswith('foo: new submodule introduced\n')
 
@@ -39,4 +54,4 @@
 def test_main_no_new_submodule(git_dir_with_git_dir):
     open('test.py', 'a+').close()
     subprocess.check_call(('git', 'add', 'test.py'))
-    assert main() == 0
+    assert main(('test.py',)) == 0
diff --git a/tests/no_commit_to_branch_test.py b/tests/no_commit_to_branch_test.py
index 610e660..9fcb580 100644
--- a/tests/no_commit_to_branch_test.py
+++ b/tests/no_commit_to_branch_test.py
@@ -3,6 +3,7 @@
 from pre_commit_hooks.no_commit_to_branch import is_on_branch
 from pre_commit_hooks.no_commit_to_branch import main
 from pre_commit_hooks.util import cmd_output
+from testing.util import git_commit
 
 
 def test_other_branch(temp_git_dir):
@@ -62,7 +63,7 @@
 
 def test_not_on_a_branch(temp_git_dir):
     with temp_git_dir.as_cwd():
-        cmd_output('git', 'commit', '--no-gpg-sign', '--allow-empty', '-m1')
+        git_commit('--allow-empty', '-m1')
         head = cmd_output('git', 'rev-parse', 'HEAD').strip()
         cmd_output('git', 'checkout', head)
         # we're not on a branch!