Repository: pre-commit/pre-commit

Issue:
Cannot run --files on Windows
(pre-commit version 2.4.0) We're running in to an odd issue on Windows

```
$ pre-commit run --files my_file.py
black................................................(no files to check)Skipped
```

However, running `pre-commit run --all-files` _will_ correctly format the file. Equally, running `pre-commit run --files my_file.py` in the same repository on a Mac works fine.

It's like the file is somehow not being read in Windows... Has anybody else encountered this?


Discussion:
please share your configuration -- I suspect you've got an `exclude:` or `types:` which is causing that file to be skipped
Hey @asottile ,

Here are the configuration files. The file we were trying to format was `./trase/supplychain/test/test_supplychain.py`.

However, I thought that we had ruled out this being a configuration issue, for two reasons:

1. Running `pre-commit run --all-files` _does_ format the file, so it must be correct in the configuration
2. Running the same command on a Mac works

Thank you for any help!
 - .pre-commit-config.yaml:

    ```yaml
    exclude: '^(?!trase/).*$'
    repos:
    -   repo: https://github.com/ambv/black
        rev: 19.10b0
        hooks:
        - id: black
          language_version: python3.6
    -   repo: https://github.com/timothycrosley/isort
        rev: 4.3.21
        hooks:
        -   id: isort
    ```

 - .gitignore:

    ```
    __pycache__/
    .ipynb_checkpoints/
    .vscode/
    .venv/
    ORI/
    IN/
    PRE/
    OUT/
    POST/
    *.pyc
    *.mps
    *.lp
    *.txt
    .DS_Store
    poetry.toml
    .pytest_cache
    Tutorial_1/
    Tutorial_2/
    Tutorial_3/
    result.log
    main.py
    **/*.idea*
    .idea/*
    **/*~
    **/*#
    **/.#*
    ```
can you show the exact command you ran?
On Windows:

```
$ pre-commit run --files trase/supplychain/test/test_supplychain.py
black................................................(no files to check)Skipped
isort................................................(no files to check)Skipped
```

On Mac:

```bash
$ pre-commit run --files trase/supplychain/test/test_supplychain.py
black....................................................................Passed
isort....................................................................Passed
```

I'm wondering if it is something odd about passing the file path on Windows. I've noticed that I get "skipped" on a Mac with `pre-commit run --files this-file-does-not-exist`. We did try changing to a backslash but still no joy...
ok, I can now reproduce this, it seems that when I switched everything to use forward slashes for the regexes, I forgot to handle global `files` / `exclude` (by the way, your configuration can be a little bit simpler by doing `files: ^trase/` (this still doesn't work until I fix this bug though!))

thanks for the report! (I'm working on this on stream right now https://twitch.tv/anthonywritescode)

Resolving patch:
diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py
--- a/pre_commit/commands/run.py
+++ b/pre_commit/commands/run.py
@@ -72,13 +72,7 @@ def filter_by_include_exclude(
 
 
 class Classifier:
-    def __init__(self, filenames: Sequence[str]) -> None:
-        # on windows we normalize all filenames to use forward slashes
-        # this makes it easier to filter using the `files:` regex
-        # this also makes improperly quoted shell-based hooks work better
-        # see #1173
-        if os.altsep == '/' and os.sep == '\\':
-            filenames = [f.replace(os.sep, os.altsep) for f in filenames]
+    def __init__(self, filenames: Collection[str]) -> None:
         self.filenames = [f for f in filenames if os.path.lexists(f)]
 
     @functools.lru_cache(maxsize=None)
@@ -105,6 +99,22 @@ def filenames_for_hook(self, hook: Hook) -> Tuple[str, ...]:
         names = self.by_types(names, hook.types, hook.exclude_types)
         return tuple(names)
 
+    @classmethod
+    def from_config(
+            cls,
+            filenames: Collection[str],
+            include: str,
+            exclude: str,
+    ) -> 'Classifier':
+        # on windows we normalize all filenames to use forward slashes
+        # this makes it easier to filter using the `files:` regex
+        # this also makes improperly quoted shell-based hooks work better
+        # see #1173
+        if os.altsep == '/' and os.sep == '\\':
+            filenames = [f.replace(os.sep, os.altsep) for f in filenames]
+        filenames = filter_by_include_exclude(filenames, include, exclude)
+        return Classifier(filenames)
+
 
 def _get_skips(environ: EnvironT) -> Set[str]:
     skips = environ.get('SKIP', '')
@@ -247,10 +257,9 @@ def _run_hooks(
     """Actually run the hooks."""
     skips = _get_skips(environ)
     cols = _compute_cols(hooks)
-    filenames = filter_by_include_exclude(
+    classifier = Classifier.from_config(
         _all_filenames(args), config['files'], config['exclude'],
     )
-    classifier = Classifier(filenames)
     retval = 0
     for hook in hooks:
         retval |= _run_single_hook(
diff --git a/pre_commit/meta_hooks/check_hooks_apply.py b/pre_commit/meta_hooks/check_hooks_apply.py
--- a/pre_commit/meta_hooks/check_hooks_apply.py
+++ b/pre_commit/meta_hooks/check_hooks_apply.py
@@ -11,10 +11,13 @@
 
 
 def check_all_hooks_match_files(config_file: str) -> int:
-    classifier = Classifier(git.get_all_files())
+    config = load_config(config_file)
+    classifier = Classifier.from_config(
+        git.get_all_files(), config['files'], config['exclude'],
+    )
     retv = 0
 
-    for hook in all_hooks(load_config(config_file), Store()):
+    for hook in all_hooks(config, Store()):
         if hook.always_run or hook.language == 'fail':
             continue
         elif not classifier.filenames_for_hook(hook):
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
@@ -28,11 +28,14 @@ def exclude_matches_any(
 
 def check_useless_excludes(config_file: str) -> int:
     config = load_config(config_file)
-    classifier = Classifier(git.get_all_files())
+    filenames = git.get_all_files()
+    classifier = Classifier.from_config(
+        filenames, config['files'], config['exclude'],
+    )
     retv = 0
 
     exclude = config['exclude']
-    if not exclude_matches_any(classifier.filenames, '', exclude):
+    if not exclude_matches_any(filenames, '', exclude):
         print(
             f'The global exclude pattern {exclude!r} does not match any files',
         )


Test patch:
diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py
--- a/tests/commands/run_test.py
+++ b/tests/commands/run_test.py
@@ -939,7 +939,7 @@ def test_classifier_normalizes_filenames_on_windows_to_forward_slashes(tmpdir):
         tmpdir.join('a/b/c').ensure()
         with mock.patch.object(os, 'altsep', '/'):
             with mock.patch.object(os, 'sep', '\\'):
-                classifier = Classifier((r'a\b\c',))
+                classifier = Classifier.from_config((r'a\b\c',), '', '^$')
                 assert classifier.filenames == ['a/b/c']
 
 
@@ -947,7 +947,7 @@ def test_classifier_does_not_normalize_backslashes_non_windows(tmpdir):
     with mock.patch.object(os.path, 'lexists', return_value=True):
         with mock.patch.object(os, 'altsep', None):
             with mock.patch.object(os, 'sep', '/'):
-                classifier = Classifier((r'a/b\c',))
+                classifier = Classifier.from_config((r'a/b\c',), '', '^$')
                 assert classifier.filenames == [r'a/b\c']