Repository: pre-commit/pre-commit

Issue:
make `files` available for use at top-level
We have a codebase that we are trying to add pre-commit to, but not everyone on the team wants or trusts the workflow. For this reason, we wanted to make it an opt-in system where by default all directories are excluded from pre-commit, except certain ones.

Ideally, we could accomplish this by being able to use the `files:` option at the top-level yaml. Because we cannot do this at the top level, we are forced to either add it for every single repo, or invert the value of the regular expression used in the `exclude:` option at the top-level.

Ergonomically, neither of these choices are good ones - the former requires many repeated `files:` options and is brittle, and the latter is synactically unintelligible for non-regex experts:
```regex
(?x)^(?!( 
    parent\/opt-in-dir1|
    opt-in-dir2
)(\/.*)?$).+
```

Could you allow us to use `files:` at the top level?


Discussion:
sure, would you like to work on a patch for this?
First, thanks for your rapid response and your project! I can already say after using it for a few days that I really like it. I can try to submit a patch, that being said, it probably won't be something I can get around to just yet since its a hectic time at work rn.
cool, it shouldn't be too difficult -- here's the patch that adds global `exclude` for instance: https://github.com/pre-commit/pre-commit/pull/617/files

Resolving patch:
diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py
--- a/pre_commit/clientlib.py
+++ b/pre_commit/clientlib.py
@@ -18,6 +18,8 @@
 
 logger = logging.getLogger('pre_commit')
 
+check_string_regex = cfgv.check_and(cfgv.check_string, cfgv.check_regex)
+
 
 def check_type_tag(tag):
     if tag not in ALL_TAGS:
@@ -53,12 +55,8 @@ def _make_argparser(filenames_help):
     cfgv.Required('language', cfgv.check_one_of(all_languages)),
     cfgv.Optional('alias', cfgv.check_string, ''),
 
-    cfgv.Optional(
-        'files', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '',
-    ),
-    cfgv.Optional(
-        'exclude', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '^$',
-    ),
+    cfgv.Optional('files', check_string_regex, ''),
+    cfgv.Optional('exclude', check_string_regex, '^$'),
     cfgv.Optional('types', cfgv.check_array(check_type_tag), ['file']),
     cfgv.Optional('exclude_types', cfgv.check_array(check_type_tag), []),
 
@@ -260,7 +258,8 @@ def warn_unknown_keys_repo(extra, orig_keys, dct):
         cfgv.check_array(cfgv.check_one_of(C.STAGES)),
         C.STAGES,
     ),
-    cfgv.Optional('exclude', cfgv.check_regex, '^$'),
+    cfgv.Optional('files', check_string_regex, ''),
+    cfgv.Optional('exclude', check_string_regex, '^$'),
     cfgv.Optional('fail_fast', cfgv.check_bool, False),
     cfgv.Optional(
         'minimum_pre_commit_version',
@@ -272,6 +271,7 @@ def warn_unknown_keys_repo(extra, orig_keys, dct):
             'repos',
             'default_language_version',
             'default_stages',
+            'files',
             'exclude',
             'fail_fast',
             'minimum_pre_commit_version',
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
@@ -206,7 +206,9 @@ def _run_hooks(config, hooks, args, environ):
     skips = _get_skips(environ)
     cols = _compute_cols(hooks, args.verbose)
     filenames = _all_filenames(args)
-    filenames = filter_by_include_exclude(filenames, '', config['exclude'])
+    filenames = filter_by_include_exclude(
+        filenames, config['files'], config['exclude'],
+    )
     classifier = Classifier(filenames)
     retval = 0
     for hook in hooks:


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
@@ -180,6 +180,22 @@ def test_global_exclude(cap_out, store, tempdir_factory):
         assert printed.endswith(expected)
 
 
+def test_global_files(cap_out, store, tempdir_factory):
+    git_path = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
+    with cwd(git_path):
+        with modify_config() as config:
+            config['files'] = '^bar.py$'
+        open('foo.py', 'a').close()
+        open('bar.py', 'a').close()
+        cmd_output('git', 'add', '.')
+        opts = run_opts(verbose=True)
+        ret, printed = _do_run(cap_out, store, git_path, opts)
+        assert ret == 0
+        # Does not contain foo.py since it was not included
+        expected = b'hookid: bash_hook\n\nbar.py\nHello World\n\n'
+        assert printed.endswith(expected)
+
+
 @pytest.mark.parametrize(
     ('args', 'expected_out'),
     [