[binutils-gdb] [pre-commit] Don't allow pre-release revisions

Tom de Vries via Binutils-cvs <[email protected]>
Newsgroups gmane.comp.gnu.binutils.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;h=55eed3660724f6927d5202b29316df1b483b3af5

commit 55eed3660724f6927d5202b29316df1b483b3af5
Author: Tom de Vries <[email protected]>
Date:   Thu Aug 27 11:19:51 2026 +0200

    [pre-commit] Don't allow pre-release revisions
    
    Pre-commit has a convenient autoupdate command.  The default behavior is:
    update to the latest tagged version.
    
    For most tools this is fine, and not too frequent.
    
    Isort however also tags pre-releases, so that has resulted in more frequent
    updates:
    ...
    $ git log .pre-commit-config.yaml | grep "Bump isort"
        [pre-commit] Bump isort to 9.0.0b1
        [pre-commit] Bump isort to 9.0.0a3
        [pre-commit] Bump isort to 9.0.0a2
        [pre-commit] Bump isort to 8.0.1
        [pre-commit] Bump isort to 8.0.0
    ...
    
    Add a script gdb/contrib/pre-commit.py that for --config-check checks the
    revision numbers of all repos, and errors out when finding something like
    9.0.0b1:
    ...
    $ ./gdb/contrib/pre-commit.py --config-check
    Revision 9.0.0b1 for repo https://github.com/pycqa/isort not allowed.
    ...
    
    Add a corresponding pre-commit check, and revert isort back to the last stable
    release: 8.0.1, to make the check pass.
    
    Consequently, pre-commit autoupdate now breaks the pre-commit check:
    ...
    $ pre-commit autoupdate
    [https://github.com/psf/black-pre-commit-mirror] already up to date!
    [https://github.com/pycqa/flake8] already up to date!
    [https://github.com/pycqa/isort] updating 8.0.1 -> 9.0.0b2
    [https://github.com/codespell-project/codespell] already up to date!
    [https://github.com/nmoroze/tclint] already up to date!
    [https://github.com/adrienverge/yamllint.git] already up to date!
    $ ./gdb/contrib/pre-commit.py --config-check
    Revision 9.0.0b2 for repo https://github.com/pycqa/isort not allowed.
    ...
    so add gdb/contrib/pre-commit.py --autoupdate that can be used instead:
    ...
    $ ./gdb/contrib/pre-commit.py --autoupdate
    [https://github.com/psf/black-pre-commit-mirror] already up to date!
    [https://github.com/pycqa/flake8] already up to date!
    [https://github.com/pycqa/isort] updating 8.0.1 -> 9.0.0b2
    Revision 9.0.0b2 for repo https://github.com/pycqa/isort not allowed.
    [https://github.com/codespell-project/codespell] already up to date!
    [https://github.com/nmoroze/tclint] already up to date!
    [https://github.com/adrienverge/yamllint.git] already up to date!
    ...
    
    The script could be extended to check that frozen revs stay frozen, but that's
    currently not needed.
    
    Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=34393

Diff:
---
 .pre-commit-config.yaml   |  11 ++-
 gdb/contrib/pre-commit.py | 169 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 178 insertions(+), 2 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index fcf839caa6b..998dbe6b820 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -64,7 +64,7 @@ repos:
         files: *gdb_python_files
         args: [--config, gdb/setup.cfg]
   - repo: https://github.com/pycqa/isort
-    rev: 9.0.0b1
+    rev: 8.0.1
     hooks:
       - id: isort
         files: *gdb_python_files
@@ -107,7 +107,7 @@ repos:
     rev: v1.38.0
     hooks:
       - id: yamllint
-        files: '^\.pre-commit-config.yaml$'
+        files: &pre_commit_config_file '^\.pre-commit-config.yaml$'
         # By default warnings don't cause yamllint to return non-zero, so they:
         # - don't cause the hook to fail, and
         # - are hidden unless pre-commit runs in verbose mode.
@@ -162,6 +162,13 @@ repos:
         # provide the dependency, which may be non-trivial.
         entry: gdb/contrib/shellcheck.sh
         stages: [manual]
+      - id: &id6 pre-commit-config-check
+        name: *id6
+        language: python
+        entry: gdb/contrib/pre-commit.py
+        args: [--config-check]
+        additional_dependencies: ["pyyaml"]
+        files: *pre_commit_config_file
 
 # Local Variables:
 # indent-tabs-mode: nil
diff --git a/gdb/contrib/pre-commit.py b/gdb/contrib/pre-commit.py
new file mode 100755
index 00000000000..f3ad6f803b0
--- /dev/null
+++ b/gdb/contrib/pre-commit.py
@@ -0,0 +1,169 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2026 Free Software Foundation, Inc.
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import subprocess
+import sys
+
+import yaml
+
+
+def get_repos(cfg):
+    with open(cfg, "r") as f:
+        data = yaml.safe_load(f)
+        repos = data.get("repos")
+        if not repos:
+            raise RuntimeError("repos missing")
+        return repos
+
+
+def config_check_repo(repo):
+    name = repo.get("repo")
+    if not name:
+        raise RuntimeError("no repo")
+    if name == "local":
+        # Skip local repo, there's no revision to check.
+        return True
+
+    # Get the revision.
+    rev = repo.get("rev")
+    if not rev:
+        raise RuntimeError("empty revision")
+
+    # Normalize revision: skip 'v' prefix.
+    if rev[0].lower() == "v":
+        rev = rev[1:]
+
+    # Check version number.  Don't allow pre-releases like 9.0.0b1.
+    # We currently only need to support x.y.z, but that could change.
+    if not re.fullmatch(r"\d+[.]\d+[.]\d+", rev):
+        print("Revision %s for repo %s not allowed." % (rev, name))
+        return False
+
+    return True
+
+
+def config_check(cfg):
+    for repo in get_repos(cfg):
+        if not config_check_repo(repo):
+            sys.exit(1)
+
+
+def run_cmd(cmd, **kwargs):
+    res = subprocess.run(cmd, **kwargs)
+    if res.returncode != 0:
+        raise RuntimeError(
+            "command %s failed with exit status %s" % (cmd, res.returncode)
+        )
+    return res
+
+
+def is_clean(args):
+    if not isinstance(args, list):
+        args = [args]
+    cmd = ["git", "status", "--porcelain"] + args
+    res = run_cmd(cmd, capture_output=True, text=True)
+    return res.stdout == ""
+
+
+def autoupdate_repo(cfg, repo):
+    name = repo["repo"]
+    if name == "local":
+        # Skip local repo, there's no revision to update.
+        return
+
+    cmd = ["pre-commit", "autoupdate", "--repo", name]
+    run_cmd(cmd)
+
+    if is_clean(cfg):
+        # No autoupdate changes.
+        return
+
+    rev = repo["rev"]
+
+    # Config has changed, refresh repo.
+    found = False
+    for new_repo in get_repos(cfg):
+        if new_repo["repo"] == name:
+            found = True
+            break
+    if not found:
+        raise RuntimeError("Repo not found in updated %s" % cfg)
+    repo = new_repo
+
+    new_rev = repo["rev"]
+
+    if not config_check_repo(repo):
+        # Reject autoupdate for this repo.  Throwing away changes here is safe
+        # because we checked in function autoupdate that cfg is clean.
+        cmd = ["git", "checkout", "-f", cfg]
+        # Capture and ignore output.
+        run_cmd(cmd, capture_output=True, text=True)
+        return
+
+    # Commit autoupdate for this repo.
+    try:
+        name_for_msg = repo["hooks"][0]["id"]
+    except (KeyError, IndexError):
+        name_for_msg = name
+    msg = 'pre-commit: Update %s: %s -> %s\n\nRan "pre-commit.py --autoupdate".' % (
+        name_for_msg,
+        rev,
+        new_rev,
+    )
+    cmd = ["git", "commit", "-m", msg, cfg]
+    run_cmd(cmd)
+
+
+def autoupdate(cfg):
+    if not is_clean(cfg):
+        print("Not clean: %s" % cfg)
+        sys.exit(1)
+
+    for repo in get_repos(cfg):
+        autoupdate_repo(cfg, repo)
+
+
+def usage():
+    print("Usage: pre-commit.py --config-check [<file>]")
+    print("                     --autoupdate")
+    sys.exit(1)
+
+
+def main():
+    if len(sys.argv) < 2:
+        usage()
+
+    cfg = ".pre-commit-config.yaml"
+
+    if sys.argv[1] == "--config-check":
+        if len(sys.argv) not in [2, 3]:
+            usage()
+        if len(sys.argv) == 3:
+            cfg = sys.argv[2]
+        config_check(cfg)
+        return
+
+    if sys.argv[1] == "--autoupdate":
+        if len(sys.argv) != 2:
+            usage()
+        autoupdate(cfg)
+        return
+
+    usage()
+
+
+main()
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.