Repository: pre-commit/pre-commit

Issue:
`check-useless-excludes` meta hook doesn't seem to work well with broken symlinks
Repro steps
1. Add a submodule
2. Add a symlink to somewhere inside this submodule
3. Use `check-symlinks`
4. Deinit submodule (to simulate the case when people clone without `--recursive`)
4. Notice that it now complains about the symlink created on 2
5. Add an exclude entry with the path to that symlink
6. Use `check-useless-excludes`
7. Notice that it complains that the added exclude entry doesn't apply.

For a working example, check out https://github.com/pkoch/test-check-useless-excludes (without `--recursive`, or run `git submodule deinit --all`) and run `pre-commit run --all`.


Discussion:
hmm indeed -- the "test" hook gets initialized with `types: [file]` which filters out the symlink -- the fix needs to happen [here](https://github.com/pre-commit/pre-commit/blob/509e4e20e86b5db48bc827753dcdfcdbbd706812/pre_commit/meta_hooks/check_useless_excludes.py#L48) -- notably it should probably have a `hook.setdefault('types', [])` ? something like that?
Unsure, can't really reason about the code. However, I can add that adding `types: []` to `check-useless-excludes` doesn't help.

Resolving patch:
diff --git a/pre_commit/meta_hooks/check_useless_excludes.py b/pre_commit/meta_hooks/check_useless_excludes.py
--- a/pre_commit/meta_hooks/check_useless_excludes.py
+++ b/pre_commit/meta_hooks/check_useless_excludes.py
@@ -43,6 +43,9 @@ def check_useless_excludes(config_file: str) -> int:
 
     for repo in config['repos']:
         for hook in repo['hooks']:
+            # the default of manifest hooks is `types: [file]` but we may
+            # be configuring a symlink hook while there's a broken symlink
+            hook.setdefault('types', [])
             # Not actually a manifest dict, but this more accurately reflects
             # the defaults applied during runtime
             hook = apply_defaults(hook, MANIFEST_HOOK_DICT)


Test patch:
diff --git a/tests/meta_hooks/check_useless_excludes_test.py b/tests/meta_hooks/check_useless_excludes_test.py
--- a/tests/meta_hooks/check_useless_excludes_test.py
+++ b/tests/meta_hooks/check_useless_excludes_test.py
@@ -1,5 +1,10 @@
+from pre_commit import git
 from pre_commit.meta_hooks import check_useless_excludes
+from pre_commit.util import cmd_output
 from testing.fixtures import add_config_to_repo
+from testing.fixtures import make_config_from_repo
+from testing.fixtures import make_repo
+from testing.util import xfailif_windows
 
 
 def test_useless_exclude_global(capsys, in_git_dir):
@@ -113,3 +118,20 @@ def test_valid_exclude(capsys, in_git_dir):
 
     out, _ = capsys.readouterr()
     assert out == ''
+
+
+@xfailif_windows  # pragma: win32 no cover
+def test_useless_excludes_broken_symlink(capsys, in_git_dir, tempdir_factory):
+    path = make_repo(tempdir_factory, 'script_hooks_repo')
+    config = make_config_from_repo(path)
+    config['hooks'][0]['exclude'] = 'broken-symlink'
+    add_config_to_repo(in_git_dir.strpath, config)
+
+    in_git_dir.join('broken-symlink').mksymlinkto('DNE')
+    cmd_output('git', 'add', 'broken-symlink')
+    git.commit()
+
+    assert check_useless_excludes.main(('.pre-commit-config.yaml',)) == 0
+
+    out, _ = capsys.readouterr()
+    assert out == ''