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:

Patch:
diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py
index 14a22b9..c4768ff 100644
--- a/pre_commit/clientlib.py
+++ b/pre_commit/clientlib.py
@@ -18,6 +18,8 @@ from pre_commit.util import parse_version
 
 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 @@ MANIFEST_HOOK_DICT = cfgv.Map(
     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 @@ CONFIG_SCHEMA = cfgv.Map(
         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 @@ CONFIG_SCHEMA = cfgv.Map(
             '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
index 0b1f7b7..f5a5b1e 100644
--- 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
index 4221134..63d0925 100644
--- 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'),
     [