Repository: pre-commit/pre-commit

Issue:
issue a warning if a mutable ref is used (`HEAD` / `stable` / `master` / etc.)
Perhaps nudging towards https://pre-commit.com/#using-the-latest-version-for-a-repository or `pre-commit autoupdate`

Discussion:

Patch:
diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py
index 916c5ff..5dfaf7a 100644
--- a/pre_commit/clientlib.py
+++ b/pre_commit/clientlib.py
@@ -1,6 +1,7 @@
 import argparse
 import functools
 import logging
+import re
 import shlex
 import sys
 from typing import Any
@@ -112,6 +113,25 @@ LOCAL = 'local'
 META = 'meta'
 
 
+# should inherit from cfgv.Conditional if sha support is dropped
+class WarnMutableRev(cfgv.ConditionalOptional):
+    def check(self, dct: Dict[str, Any]) -> None:
+        super().check(dct)
+
+        if self.key in dct:
+            rev = dct[self.key]
+
+            if '.' not in rev and not re.match(r'^[a-fA-F0-9]+$', rev):
+                logger.warning(
+                    f'The {self.key!r} field of repo {dct["repo"]!r} '
+                    f'appears to be a mutable reference '
+                    f'(moving tag / branch).  Mutable references are never '
+                    f'updated after first install and are not supported.  '
+                    f'See https://pre-commit.com/#using-the-latest-version-for-a-repository '  # noqa: E501
+                    f'for more details.',
+                )
+
+
 class OptionalSensibleRegex(cfgv.OptionalNoDefault):
     def check(self, dct: Dict[str, Any]) -> None:
         super().check(dct)
@@ -261,6 +281,14 @@ CONFIG_REPO_DICT = cfgv.Map(
     ),
 
     MigrateShaToRev(),
+    WarnMutableRev(
+        'rev',
+        cfgv.check_string,
+        '',
+        'repo',
+        cfgv.NotIn(LOCAL, META),
+        True,
+    ),
     cfgv.WarnAdditionalKeys(('repo', 'rev', 'hooks'), warn_unknown_keys_repo),
 )
 DEFAULT_LANGUAGE_VERSION = cfgv.Map(


Test patch:
diff --git a/tests/clientlib_test.py b/tests/clientlib_test.py
index ba60236..d08ecdf 100644
--- a/tests/clientlib_test.py
+++ b/tests/clientlib_test.py
@@ -180,6 +180,70 @@ def test_ci_key_must_be_map():
         cfgv.validate({'ci': 'invalid', 'repos': []}, CONFIG_SCHEMA)
 
 
+@pytest.mark.parametrize(
+    'rev',
+    (
+        'v0.12.4',
+        'b27f281',
+        'b27f281eb9398fc8504415d7fbdabf119ea8c5e1',
+        '19.10b0',
+        '4.3.21-2',
+    ),
+)
+def test_warn_mutable_rev_ok(caplog, rev):
+    config_obj = {
+        'repo': 'https://gitlab.com/pycqa/flake8',
+        'rev': rev,
+        'hooks': [{'id': 'flake8'}],
+    }
+    cfgv.validate(config_obj, CONFIG_REPO_DICT)
+
+    assert caplog.record_tuples == []
+
+
+@pytest.mark.parametrize(
+    'rev',
+    (
+        '',
+        'HEAD',
+        'stable',
+        'master',
+        'some_branch_name',
+    ),
+)
+def test_warn_mutable_rev_invalid(caplog, rev):
+    config_obj = {
+        'repo': 'https://gitlab.com/pycqa/flake8',
+        'rev': rev,
+        'hooks': [{'id': 'flake8'}],
+    }
+    cfgv.validate(config_obj, CONFIG_REPO_DICT)
+
+    assert caplog.record_tuples == [
+        (
+            'pre_commit',
+            logging.WARNING,
+            "The 'rev' field of repo 'https://gitlab.com/pycqa/flake8' "
+            'appears to be a mutable reference (moving tag / branch).  '
+            'Mutable references are never updated after first install and are '
+            'not supported.  '
+            'See https://pre-commit.com/#using-the-latest-version-for-a-repository '  # noqa: E501
+            'for more details.',
+        ),
+    ]
+
+
+def test_warn_mutable_rev_conditional():
+    config_obj = {
+        'repo': 'meta',
+        'rev': '3.7.7',
+        'hooks': [{'id': 'flake8'}],
+    }
+
+    with pytest.raises(cfgv.ValidationError):
+        cfgv.validate(config_obj, CONFIG_REPO_DICT)
+
+
 def test_validate_optional_sensible_regex(caplog):
     config_obj = {
         'id': 'flake8',