From: Richard Earnshaw <[email protected]>

Add some scaffolding for managing labels used by the forge.  This
patch handles some general infrastructure to read a 'database' of
labels, an initial set of lables and a script to manage the list of
labels on the forge instance.  It makes use of the forge's REST API to
automate the process, thus making it simple for a user with
appropriate privilages to update the list with minimal effort.

The classify.py script takes a list of file paths and applies the
match rules, printing out the labels that would apply to each filename.
You can see the effects by running
  (cd $GCC_SRC_BASE; find . -type f -print) | ./classify.py

I expect a future patch to use the matching data to automatically
label merge requests with the relevant labels as part of the initial
triaging process.

contrib/ChangeLog:

        * forge/forgelabels.py: New file.
        * forge/labels.yaml: New file.
        * forge/update-labels.py: New file.
        * forge/classify.py: New file.
---
 contrib/forge/classify.py      |  88 ++++
 contrib/forge/forgelabels.py   | 201 +++++++
 contrib/forge/labels.yaml      | 926 +++++++++++++++++++++++++++++++++
 contrib/forge/update-labels.py | 151 ++++++
 4 files changed, 1366 insertions(+)
 create mode 100755 contrib/forge/classify.py
 create mode 100755 contrib/forge/forgelabels.py
 create mode 100644 contrib/forge/labels.yaml
 create mode 100755 contrib/forge/update-labels.py

diff --git a/contrib/forge/classify.py b/contrib/forge/classify.py
new file mode 100755
index 000000000000..da77c9c72390
--- /dev/null
+++ b/contrib/forge/classify.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+
+# Map files in the GCC source tree to forge labels.
+
+# Copyright (C) 2026 Free Software Foundation, Inc.
+#
+# This file is part of GCC.
+#
+# GCC 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, or (at your option) any later
+# version.
+#
+# GCC 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 GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.
+
+# Note, this file requires python 3.10 or later.
+
+import re
+import sys
+import forgelabels
+
+labels_file = 'labels'
+
+class classifier:
+    def __init__ (self, groups):
+        self.file_class = []
+        self.bz_class = []
+        for g in groups:
+            if not g.match_defaults or g.match_defaults.mclass != "file":
+                continue
+            for l in g.labels:
+                if l.match:
+                    self.file_class.append (self._file_entry (l.match,
+                                                              l.full_name))
+
+    def map_to_labels (self, name):
+        labels = []
+        priority = 100
+        for fc in self.file_class:
+            if fc['pattern'].match (name):
+                if fc['priority'] < priority:
+                    priority = fc['priority']
+                    labels = [fc['label']]
+                elif fc['priority'] == priority:
+                    labels.append (fc['label'])
+
+        return labels
+
+    def _file_entry (self, match, label):
+        priority = match.priority
+
+        d = dict()
+        try:
+            d['pattern'] = re.compile (match.file2re ())
+        except Exception as e:
+            # Re-raise the exception with some additional context
+            raise Exception (f"{label}: {str (e)}")
+
+        d['priority'] = priority
+        d['label'] = label
+        return d
+
+def load ():
+    """
+    Main entry point to initialize the classifier
+    """
+    return classifier (forgelabels.load ())
+
+def main ():
+    """
+    Entry point for testing the classifier.
+    Use it with something like:
+       (cd srcroot; find . -type f -print) | ./classify.py
+    """
+    my_classifier = load ()
+    for f in sys.stdin.readlines ():
+        labels = my_classifier.map_to_labels (f[1:].strip ('\n'))
+        print (f"{f.strip ('\n')}: {labels}")
+
+if __name__ == "__main__":
+    main ()
diff --git a/contrib/forge/forgelabels.py b/contrib/forge/forgelabels.py
new file mode 100755
index 000000000000..b4b42ba0feb4
--- /dev/null
+++ b/contrib/forge/forgelabels.py
@@ -0,0 +1,201 @@
+#! /usr/bin/env python3
+
+# Read the list of label data for a forgejo instance of GCC
+
+# Copyright (C) 2026 Free Software Foundation, Inc.
+#
+# This file is part of GCC.
+#
+# GCC 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, or (at your option) any later
+# version.
+#
+# GCC 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 GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.
+
+# Variables needed by the script.  Can be overridden by setting the
+# same name in the environment
+
+import yaml
+
+# Prefer the libYAML implementations if available.  Fall back to the
+# pure python versions as a backup.
+try:
+    from yaml import CLoader as Loader, CDumper as Dumper
+except:
+    from yaml import Loader, Dumper
+
+labels_file = "labels.yaml"
+
+class match_defaults:
+    def __init__(self, md_data):
+        self.mclass = 'file'
+        if 'class' in md_data:
+            if (md_data['class'] == "file"
+                or md_data['class'] == "BZ"):
+                self.mclass = md_data['class']
+            else:
+                raise Exception ("Unsupported match class: {md_data['class']}")
+
+        # todo: Add BZ class support
+
+class label_match:
+    def __init__(self, lm_data):
+        self.file = None
+        self.priority = 1
+        if 'file' in lm_data:
+            self.file = lm_data['file']
+        if 'priority' in lm_data:
+            self.priority = lm_data['priority']
+        # todo: Add BZ match support
+
+    def file2re(self):
+        """
+        Convert the extended glob file entry to a standard regular expression.
+
+        Supported features:
+        ^/ at the start of an entry anchors to the root of the file tree.
+        \<char> passes <char> through to the RE compiler, without any of
+           the interpretations below.
+        /**/ matches an arbitrary depth of directories.
+        * matches any character except '/'.
+        + matches the literal '+'.
+        . matches the literal '.'.
+        (...|...[|...]+) match any of the alternatives.
+        """
+        if not self.file:
+            raise Exception ("Match rule lacks a 'file' entry")
+        template = self.file
+        pos = 0
+        depth = 0
+        last_char = None
+        pattern = r''
+        length = len (template)
+        if template[pos] == '^':
+            pattern += r'^'
+            pos += 1
+            # Don't update last char
+        else:
+            pattern += r'.*/'
+
+        while pos < length:
+            if template[pos] == '\\' and pos + 1 < length:
+                pos += 1
+                pattern += template[pos]
+            # '*' is mapped to '[^/]*' unless the following characters are
+            # also '*/', in which case we match '(.*/)*'
+            elif template[pos] == '*':
+                if pos + 2 < length and template[:3] == '**/':
+                    pattern += r'(:?.*/)*'
+                    pos += 1
+                else:
+                    pattern += r'[^/]*'
+            elif template[pos] == '+':
+                pattern += r'\+'
+            elif template[pos] == '(':
+                # We don't need any groups from the regex.
+                pattern += r'(?:'
+                depth += 1
+            elif template[pos] == ')':
+                pattern += r')'
+                depth -= 1
+                if depth < 0:
+                    raise Exception ('Invalid template: ' + template)
+            elif template[pos] == '.':
+                pattern += r'\.'
+            elif template[pos] == '/':
+                pattern += r'/'
+                if (depth > 0
+                    and pos + 1 < length 
+                    and (template[pos + 1] == '|'
+                         or template[pos + 1] == ')')):
+                    # We expect '/|' (or '/)' to appear at the end of
+                    # an alternative and to mean anything in any
+                    # subdirectory matched.  But we may need to match
+                    # files in the alternative, so add an explicit
+                    # '.*'.
+                    pattern += r'.*'
+            else:
+                pattern += template[pos]
+            last_char = template[pos]
+            pos += 1
+
+        if depth != 0:
+            raise Exception ('Invalid template: ' + template)
+        if last_char != '/':
+            pattern += '$'
+
+        return pattern
+
+class label:
+    def __init__(self, l_data, group):
+        self.name = None
+        self.full_name = None
+        self.desc = None
+        self.match = None
+        self.color_val = None
+        self.group = group
+        if 'name' in l_data:
+            self.name = l_data['name']
+            self.full_name = group.group + '/' + self.name
+        else:
+            raise Exception (f"Missing label name (in group '{group.group}').")
+        if 'description' in l_data:
+            self.desc = l_data['description']
+        if 'match' in l_data:
+            self.match = label_match(l_data['match'])
+    def color(self):
+        if self.color_val:
+            return self.color_val
+        return self.group.color
+
+class group:
+    def __init__(self, gr_data):
+        # defaults
+        self.exclusive = False
+        self.color = "#ff0000"  # Red
+        self.match_defaults = None
+        self.labels = []
+
+        if 'group' in gr_data:
+            self.group = gr_data['group']
+        else:
+            raise Exception ('All groups must specify their name')
+        if 'color' in gr_data:
+            self.color = gr_data['color']
+        if 'exclusive' in gr_data:
+            self.exclusive = gr_data['exclusive']
+        if 'match_defaults' in gr_data:
+            self.match_defaults = match_defaults(gr_data['match_defaults'])
+        if 'labels' in gr_data:
+            for l in gr_data['labels']:
+                self.labels.append(label(l, self))
+        else:
+            raise Exception (f"Group {self.group} must contain at least one 
label")
+
+def load():
+    with open(labels_file) as f:
+        groups = []
+        data = yaml.load (f, Loader=Loader)
+
+        try:
+            for g in data['label_data']:
+                groups.append(group(g))
+
+        except Exception as e:
+            raise e
+            return
+    return groups
+
+def main():
+    print (load())
+
+if __name__ == "__main__":
+    main()
diff --git a/contrib/forge/labels.yaml b/contrib/forge/labels.yaml
new file mode 100644
index 000000000000..4abe4779b975
--- /dev/null
+++ b/contrib/forge/labels.yaml
@@ -0,0 +1,926 @@
+# Provisional list of labels for the gcc forge:
+#
+# Copyright (C) 2026 Free Software Foundation, Inc.
+#
+# This file is part of GCC.
+#
+# GCC 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, or (at your option) any later
+# version.
+#
+# GCC 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 GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.
+
+# Tags are not exclusive unless otherwise stated.
+
+# Note, *** indicates that this matches a strict category, but perhaps
+# we shouldn't permit these in labels.
+
+# Format of this file:
+#
+#   We currently recognize three classes matching rule for auto-applying
+#   labels: BZ, file and none.
+#   The matching rules are inherrited up the data scopes with the inner
+#   most rule taking precedence.  If no rule can be found, then the
+#   default rule of 'none' is applied.
+#   - BZ
+#     If the patch cover letter references a Bugzilla ticket, the ticket
+#     number will be looked up in the Bugzilla database and the
+#     appropriate data used to apply the relevant label.
+#   - file
+#     The extended glob patterns are matched against the files modified
+#     and the label is attached if the pattern matches and no higher
+#     priority matches exist.  The default priority for a match is 1
+#   - none
+#     Can be used to disable a label from being selected by matching
+#     when a default class would otherwise be inherrited.
+
+
+label_data: 
+  # Bug reports
+  # All of these labels are expected to be automatically scraped from
+  # the primary (first-referenced) bugzilla entry that is identified
+  # from the cover text of a patch.  Note that they will not be
+  # automatically updated if the bugzilla entry is changed after the
+  # pull request has been created.
+  - group: Bug
+    # Labels relating to bugs with a 'regression' marker in the
+    # bug summary.
+    exclusive: false
+    color: "bd0000"
+    labels:
+      - name: Regression
+        description: "The referenced Bugzilla ticket is a regression"
+        match:
+          class: BZ
+          field: summary
+          value: "imatch(\\[[^\\]]*regression\\])"
+  - group: Bug/Importance
+    exclusive: true
+    color: "f06d00"
+    match_defaults:
+      class: BZ
+      field: importance
+    labels:
+      - name: "P1"
+        description: "The Bugzilla ticket has priority P1"
+        match:
+          value: "== 'P1'"
+      - name: "P2"
+        description: "The Bugzilla ticket has priority P2"
+        match:
+          value: "== 'P2'"
+      - name: "P3"
+        description: "The Bugzilla ticket has priority P3"
+        match:
+          value: "== 'P3'"
+      - name: "P4"
+        description: "The Bugzilla ticket has priority P4"
+        match:
+          value: "== 'P4'"
+      - name: "P5"
+        description: "The Bugzilla ticket has priority P5"
+        match:
+          value: "== 'P5'"
+  # Labels relating to bugs with a specific component.  The final
+  # part of the label should be a string match for the component
+  # field of the BZ entry.
+  - group: "Bug/Component"
+    exclusive: true
+    color: "fbca04"
+    match_defaults:
+      class: BZ
+      field: component
+      label_name: true
+    labels:
+      - name: "ada"
+        description: "The Bugzilla component is 'ada'"
+      - name: "algol68"
+        description: "The Bugzilla component is 'algol68'"
+      - name: "analyzer"
+        description: "The Bugzilla component is 'analyzer'"
+      - name: "boehm-gc"
+        description: "The Bugzilla component is 'boehm-gc'"
+      - name: "bootstrap"
+        description: "The Bugzilla component is 'bootstrap'"
+      - name: "c"
+        description: "The Bugzilla component is 'c'"
+      - name: "c++"
+        description: "The Bugzilla component is 'c++'"
+      - name: "cobol"
+        description: "The Bugzilla component is 'cobol'"
+      - name: "d"
+        description: "The Bugzilla component is 'd'"
+      - name: "debug"
+        description: "The Bugzilla component is 'debug'"
+      - name: "demangler"
+        description: "The Bugzilla component is 'demangler'"
+      - name: "diagnostics"
+        description: "The Bugzilla component is 'diagnostics'"
+      - name: "driver"
+        description: "The Bugzilla component is 'driver'"
+      - name: "fortran"
+        description: "The Bugzilla component is 'fortran'"
+      - name: "gcov-profile"
+        description: "The Bugzilla component is 'gcov-profile'"
+      - name: "go"
+        description: "The Bugzilla component is 'go'"
+      - name: "ipa"
+        description: "The Bugzilla component is 'ipa'"
+      - name: "jit"
+        description: "The Bugzilla component is 'jit'"
+      - name: "libbacktrace"
+        description: "The Bugzilla component is 'libbacktrace'"
+      - name: "libcc1"
+        description: "The Bugzilla component is 'libcc1'"
+      - name: "libffi"
+        description: "The Bugzilla component is 'libffi'"
+      - name: "libfortran"
+        description: "The Bugzilla component is 'libfortran'"
+      - name: "libgcc"
+        description: "The Bugzilla component is 'libgcc'"
+      - name: "libgdiagnostics"
+        description: "The Bugzilla component is 'libdiagnostics'"
+      - name: "libgomp"
+        description: "The Bugzilla component is 'libgomp'"
+      - name: "libitm"
+        description: "The Bugzilla component is 'libitm'"
+      - name: "libobjc"
+        description: "The Bugzilla component is 'libobjc'"
+      - name: "libquadmath"
+        description: "The Bugzilla component is 'libquadmath'"
+      - name: "libstdc++"
+        description: "The Bugzilla component is 'libstdc++'"
+      - name: "lto"
+        description: "The Bugzilla component is 'lto'"
+      - name: "middle-end"
+        description: "The Bugzilla component is 'middle-end'"
+      - name: "modula2"
+        description: "The Bugzilla component is 'modula2'"
+      - name: "objc"
+        description: "The Bugzilla component is 'objc'"
+      - name: "other"
+        description: "The Bugzilla component is 'other'"
+      - name: "pch"
+        description: "The Bugzilla component is 'pch'"
+      - name: "pending"
+        description: "The Bugzilla component is 'pending'"
+      - name: "plugins"
+        description: "The Bugzilla component is 'plugins'"
+      - name: "preprocessor"
+        description: "The Bugzilla component is 'preprocessor'"
+      - name: "regression"
+        description: "The Bugzilla component is 'regression'"
+      - name: "rtl-optimization"
+        description: "The Bugzilla component is 'rtl-optimization'"
+      - name: "rust"
+        description: "The Bugzilla component is 'rust'"
+      - name: "sanitizer"
+        description: "The Bugzilla component is 'sanitizer'"
+      - name: "sarif-replay"
+        description: "The Bugzilla component is 'sarif-replay'"
+      - name: "target"
+        description: "The Bugzilla component is 'target'"
+      - name: "testsuite"
+        description: "The Bugzilla component is 'testsuite'"
+      - name: "translation"
+        description: "The Bugzilla component is 'translation'"
+      - name: "tree-optimization"
+        description: "The Bugzilla component is 'tree-optimization'"
+      - name: "web"
+        description: "The Bugzilla component is 'web'"
+  # Labels relating to releases (for backporting)
+  - group: "Release"
+    exclusive: false
+    color: "5319e7"
+    labels:
+      - name: "GCC-13"
+        description: "Consider backporting to GCC-13"
+      - name: "GCC-14"
+        description: "Consider backporting to GCC-14"
+      - name: "GCC-15"
+        description: "Consider backporting to GCC-15"
+      - name: "GCC-16"
+        description: "Consider backporting to GCC-16"
+  # Labels related to components
+  # Note that multiple labels in this section can be applied, based on
+  # the files modified.
+  - group: "General"
+    exclusive: false
+    color: "006b75"
+    match_defaults:
+      class: "file"
+    labels:
+      - name: "config"
+        description: "Affects configure or autoconf scripts"
+        match:
+          file: "^/(config/|**/*.(in|ac|m4)|**/configure*)"
+          priority: 2
+      - name: "contrib"
+        description: "Miscellaneous support scripts"
+        match:
+          file: "^/contrib/"
+      - name: "make"
+        description: "Affects Makefiles or automake"
+        match:
+          file: "(Makefile(.*)?|*.am)"
+          priority: 2
+      - name: "driver"
+        description: "GCC main driver program"
+        match:
+          file: "^/gcc/gcc(-main|-ar)?.(cc|h)"
+      - name: "forge"
+        description: "Forge and CI infrastructure"
+        match:
+          file: "^/.(forgejo|github)/"
+      - name: "unknown"
+        description: "Catch-all, does not match any other category"
+        match:
+          file: "*"
+          priority: 10
+      - name: "gdbhooks"
+        description: "Support for debugging GCC with GDB (gdbhooks.py)"
+        match:
+          file: "^/gcc/gdb(hooks.py|init.in)"
+      - name: "docs"
+        description: "Changes to the documentation"
+        match:
+          file: "(docs?/|*.texi)"
+      - name: "web"
+        description: "Changes to the web pages"
+        # No match entry!
+
+  # Library components
+  - group: "Library"
+    exclusive: false
+    color: "009800"
+    match_defaults:
+      class: "file"
+    labels:
+      - name: "libada"
+        description: "Affects libada"
+        match:
+          file: "^/libada/"
+      - name: "libatomic"
+        description: "Affects libatomic"
+        match:
+          file: "^/libatomic/"
+      - name: "libbacktrace"
+        description: "Affects libbacktrace"
+        match:
+          file: "^/libbacktrace/"
+      - name: "libcc1"
+        description: "Affects libcc1"
+        match:
+          file: "^/libcc1/"
+      - name: "libcody"
+        description: "Affects libcody"
+        match:
+          file: "^/libcody/"
+      - name: "libcpp"
+        description: "Affects libcpp"
+        match:
+          file: "^/libcpp/"
+      - name: "libdecnumber"
+        description: "Affects libdecnumber"
+        match:
+          file: "^/libdecnumber/"
+      - name: "libffi"
+        description: "Affects libffi"
+        match:
+          file: "^/libffi/"
+      - name: "libgcc"
+        description: "Affects libgcc"
+        match:
+          file: "^/libgcc/"
+      - name: "libgcobol"
+        description: "Affects libgcobol"
+        match:
+          file: "^/libgcobol/"
+      - name: "libgfortran"
+        description: "Affects libgfortran"
+        match:
+          file: "^/libgfortran/"
+      - name: "libgm2"
+        description: "Affects libgm2"
+        match:
+          file: "^/libgm2/"
+      - name: "libgo"
+        description: "Affects libgo"
+        match:
+          file: "^/libgo/"
+      - name: "libgomp"
+        description: "Affects libgomp"
+        match:
+          file: "^/libgomp/"
+      - name: "libgrust"
+        description: "Affects libgrust"
+        match:
+          file: "^/libgrust/"
+      - name: "libiberty"
+        description: "Affects libiberty"
+        match:
+          file: "^/(include|libiberty)/"
+      - name: "libitm"
+        description: "Affects libitm"
+        match:
+          file: "^/libitm/"
+      - name: "libobjc"
+        description: "Affects libobjc"
+        match:
+          file: "^/libobjc/"
+      - name: "libphobos"
+        description: "Affects libphobos"
+        match:
+          file: "^/libphobos/"
+      - name: "libquadmath"
+        description: "Affects libquadmath"
+        match:
+          file: "^/libquadmath/"
+      - name: "libsanitizer"
+        description: "Affects libsanitizer"
+        match:
+          file: "^/libsanitizer/"
+      - name: "libssp"
+        description: "Affects libssp"
+        match:
+          file: "^/libssp/"
+      - name: "libstdc++"
+        description: "Affects libstdc++"
+        match:
+          file: "^/libstdc++-v3/"
+      - name: "libvtv"
+        description: "Affects libvtv"
+        match:
+          file: "^/libvtv/"
+      - name: "zlib"
+        description: "Affects zlib"
+        match:
+          file: "^/zlib/"
+  # CPU targets (in compiler or in libraries)
+  - group: "Target"
+    exclusive: false
+    color: "70c24a"
+    match_defaults:
+      class: "file"
+    labels:
+      - name: "aarch64"
+        description: "Affects the aarch64 target"
+        match:
+          file: "config/aarch64/"
+      - name: "alpha"
+        description: "Affects the alpha target"
+        match:
+          file: "config/alpha/"
+      - name: "arc"
+        description: "Affects the arc target"
+        match:
+          file: "config/arc/"
+      - name: "arm"
+        description: "Affects the arm target"
+        match:
+          file: "config/arm/"
+      - name: "avr"
+        description: "Affects the avr target"
+        match:
+          file: "config/avr/"
+      - name: "bfin"
+        description: "Affects the bfin target"
+        match:
+          file: "config/bfin/"
+      - name: "bpf"
+        description: "Affects the bpf target"
+        match:
+          file: "config/bpf/"
+      - name: "c6x"
+        description: "Affects the c6x target"
+        match:
+          file: "config/c6x/"
+      - name: "cris"
+        description: "Affects the cris target"
+        match:
+          file: "config/cris/"
+      - name: "csky"
+        description: "Affects the csky target"
+        match:
+          file: "config/csky/"
+      - name: "epiphany"
+        description: "Affects the epiphany target"
+        match:
+          file: "config/epiphany/"
+      - name: "fr30"
+        description: "Affects the fr30 target"
+        match:
+          file: "config/fr30/"
+      - name: "frv"
+        description: "Affects the frv target"
+        match:
+          file: "config/frv/"
+      - name: "ft32"
+        description: "Affects the ft32 target"
+        match:
+          file: "config/ft32/"
+      - name: "gcn"
+        description: "Affects the gcn target"
+        match:
+          file: "config/gcn/"
+      - name: "h8300"
+        description: "Affects the h8300 target"
+        match:
+          file: "config/h8300/"
+      - name: "i386"
+        description: "Affects the i386 target"
+        match:
+          file: "config/i386/"
+      - name: "ia64"
+        description: "Affects the ia64 target"
+        match:
+          file: "config/ia64/"
+      - name: "iq2000"
+        description: "Affects the iq2000 target"
+        match:
+          file: "config/iq2000/"
+      - name: "lm32"
+        description: "Affects the lm32 target"
+        match:
+          file: "config/lm32/"
+      - name: "loongarch"
+        description: "Affects the loongarch target"
+        match:
+          file: "config/loongarch/"
+      - name: "m32c"
+        description: "Affects the m32c target"
+        match:
+          file: "config/m32c/"
+      - name: "m32r"
+        description: "Affects the m32r target"
+        match:
+          file: "config/m32r/"
+      - name: "m68k"
+        description: "Affects the m68k target"
+        match:
+          file: "config/m68k/"
+      - name: "mcore"
+        description: "Affects the mcore target"
+        match:
+          file: "config/mcore/"
+      - name: "microblaze"
+        description: "Affects the microblaze target"
+        match:
+          file: "config/microblaze/"
+      - name: "mips"
+        description: "Affects the mips target"
+        match:
+          file: "config/mips/"
+      - name: "mmix"
+        description: "Affects the mmix target"
+        match:
+          file: "config/mmix/"
+      - name: "mn10300"
+        description: "Affects the mn1030 target"
+        match:
+          file: "config/mn10300/"
+      - name: "moxie"
+        description: "Affects the moxie target"
+        match:
+          file: "config/moxie/"
+      - name: "msp430"
+        description: "Affects the msp430 target"
+        match:
+          file: "config/msp430/"
+      - name: "nds32"
+        description: "Affects the nds32 target"
+        match:
+          file: "config/nds32/"
+      - name: "nvptx"
+        description: "Affects the nvptx target"
+        match:
+          file: "config/nvptx/"
+      - name: "or1k"
+        description: "Affects the or1k target"
+        match:
+          file: "config/or1k/"
+      - name: "pa"
+        description: "Affects the pa target"
+        match:
+          file: "config/pa/"
+      - name: "pdp11"
+        description: "Affects the pdp1 target"
+        match:
+          file: "config/pdp11/"
+      - name: "pru"
+        description: "Affects the pru target"
+        match:
+          file: "config/pru/"
+      - name: "riscv"
+        description: "Affects the riscv target"
+        match:
+          file: "config/riscv/"
+      - name: "rl78"
+        description: "Affects the rl78 target"
+        match:
+          file: "config/rl78/"
+      - name: "rs6000"
+        description: "Affects the rs6000 target"
+        match:
+          file: "config/rs6000/"
+      - name: "rx"
+        description: "Affects the rx target"
+        match:
+          file: "config/rx/"
+      - name: "s390"
+        description: "Affects the s390 target"
+        match:
+          file: "config/s390/"
+      - name: "sh"
+        description: "Affects the sh target"
+        match:
+          file: "config/sh/"
+      - name: "sparc"
+        description: "Affects the sparc target"
+        match:
+          file: "config/sparc/"
+      - name: "stormy16"
+        description: "Affects the stormy16 target"
+        match:
+          file: "config/x?stormy16/"
+      - name: "v850"
+        description: "Affects the v850 target"
+        match:
+          file: "config/v850/"
+      - name: "vax"
+        description: "Affects the vax target"
+        match:
+          file: "config/vax/"
+      - name: "visium"
+        description: "Affects the visium target"
+        match:
+          file: "config/visium/"
+      - name: "xtensa"
+        description: "Affects the xtensa target"
+        match:
+          file: "config/xtensa/"
+  # Labels specific to an object file format
+  - group: "Obj"
+    exclusive: false
+    color: "b0ffb0"
+    match_defaults:
+      class: "file"
+    labels:
+      - name: "coff"
+        description: "Target independent code related to COFF object format"
+        match:
+          file: "*coff*"
+          priority: 2
+      - name: "elf"
+        description: "Target independent code related to COFF object format"
+        match:
+          file: "*elf*"
+          priority: 2
+      - name: "pe"
+        description: "Target independent code related to PE-COFF object format"
+        # No auto-match entry.
+  # Labels relating to a specific OS
+  - group: "OS"
+    exclusive: false
+    color: "b0fff9"
+    match_defaults:
+      class: "file"
+    labels:
+      - name: "aix"
+        description: "Affects AIX OS"
+        match:
+          file: "rs6000/*aix*"
+      - name: "android"
+        description: "Affects Android OS"
+        match:
+          file: "*android*"
+      - name: "cygwin"
+        description: "Affects cywin or mingw OSes"
+        match:
+          file: "(config/mingw/|*(cyqwin|mingw)*)"
+      - name: "darwin"
+        description: "Affects Darwin OS"
+        match:
+          file: "*darwin*"
+      - name: "dragonfly"
+        description: "Affects Dragonfly OS"
+        match:
+          file: "*dragonfly*"
+      - name: "freebsd"
+        description: "Affects FreeBSD OS"
+        match:
+          file: "*freebsd*"
+      - name: "hpux"
+        description: "Affects HP-UX OS"
+        match:
+          file: "*hpux*"
+      - name: "hurd"
+        description: "Affects GNU Hurd OS"
+        match:
+          file: "*hurd*"
+      - name: "linux"
+        description: "Affects generic Linux OS"
+        match:
+          file: "*linux*"
+          priority: 2
+      - name: "netbsd"
+        description: "Affects NetBSD OS"
+        match:
+          file: "*netbsd*"
+      - name: "openbsd"
+        description: "Affects OpenBSD OS"
+        match:
+          file: "*openbsd*"
+      - name: "rtems"
+        description: "Affects RTEMS OS"
+        match:
+          file: "*rtems*"
+      - name: "solaris"
+        description: "Affects Solaris OS"
+        match:
+          file: "(sol2*|*solaris*)"
+      - name: "vms"
+        description: "Affects VMS OS"
+        match:
+          file: "(*[^a-z])?vms*"
+      - name: "vxworks"
+        description: "Affects VxWorks"
+        match:
+          file: "(config/vxworks/|*vxworks*)"
+      - name: "windows"
+        description: "Affects Windows OS"
+        match:
+          file: "*windows*"
+  # Labels relating to compiler mid-end
+  - group: "Midend"
+    exclusive: false
+    color: "207de5"
+    match_defaults:
+      class: "file"
+    labels:
+      - name: "diagnostics"
+        description: "Diagnostics framework"
+        match:
+          file: "^/gcc/(diagnostics/|text-art/|*diagnostic*|*sarif*)"
+      - name: "rtl"
+        description: "General RTL code"
+        match:
+          file: "^/gcc/*.(cc|c|h)"
+          priority: 3
+      - name: "reg-alloc"
+        description: "Register allocation code"
+        match:
+          file: "^/gcc/*(ira|lra)*"
+      - name: "tree"
+        description: "General tree/gimple code"
+        match:
+          file: "^/gcc/*tree*"
+          priority: 2
+      - name: "vect"
+        description: "Vectorization"
+        match:
+          file: "^/gcc/*vect*"
+      - name: "cfg"
+        description: "Control Flow Graph framework"
+        match:
+          file: "^/gcc/*cfg*"
+      - name: "ggc"
+        description: "Garbage collection"
+        match:
+          file: "^/gcc/*ggc*"
+      - name: "gen"
+        description: "Generator programs, eg genattr"
+        match:
+          file: "^/gcc/gen*.(cc|h)"
+      - name: "ipa"
+        description: "Inter Procedural Analysis framework"
+        match:
+          file: "^/gcc/ipa*"
+      - name: "misc"
+        description: "Miscellaneous files that doesn't fit anything else"
+        match:
+          file: "^/gcc/*"
+          priority: 4
+      - name: "lto"
+        description: "Link Time Optimizer code"
+        match:
+          file: "^/gcc/(lto/|*lto*)"
+      - name: "jit"
+        description: "JIT (Just-In-Time) code"
+        match:
+          file: "^/gcc/jit/"
+      - name: "include"
+        description: "Include headers and fixincludes"
+        match:
+          file: "^/(fixincludes|gcc/ginclude)/"
+      - name: "dwarf"
+        description: "Dwarf debug information"
+        match:
+          file: "^/gcc/**/*dwarf*"
+      - name: "debug"
+        description: "Non-dwarf debug support"
+        match:
+          file: "^/gcc/*ctf*"
+      - name: "gimple"
+        description: "General GIMPLE support"
+        match:
+          file: "^/gcc/*gimple*"
+      - name: "gcse"
+        description: "RTL GCSE"
+        match:
+          file: "^/gcc/*gcse*"
+      - name: "jump"
+        description: "Jump and branch optimizations"
+        match:
+          file: "^/gcc/*jump*"
+      - name: "i18n"
+        description: "Internationalization and Localization"
+        match:
+          file: "^/gcc/(po/|intl.*)"
+      - name: "gcov"
+        description: "GCOV code coverage support"
+        match:
+          file: "^/gcc/gcov*"
+      - name: "opts"
+        description: "Option framework"
+        match:
+          file: "^/gcc/(opt*.awk|*.opt|opt[-s]*)"
+      - name: "ranger"
+        description: "Value range framework"
+        match:
+          file: "^/gcc/*range*"
+      - name: "rtl-ssa"
+        description: "RTL SSA framework"
+        match:
+          file: "^/gcc/rtl-ssa/"
+      - name: "tree-ssa"
+        description: "Tree SSA framework"
+        match:
+          file: "^/gcc/(tree|gimple)-ssa*"
+      - name: "autofdo"
+        description: "AutoFDO framework"
+        match:
+          file: "^/gcc/auto-profile*"
+      - name: "combine"
+        description: "Instruction Combiner"
+        match:
+          file: "^/gcc/combine*"
+      - name: "reload"
+        description: "Legacy reload pass (obsolete)"
+        match:
+          file: "^/gcc/reload*"
+      - name: "pair-fusion"
+        description: "Pair Fusion framework"
+        match:
+          file: "^/gcc/pair-fusion*"
+      - name: "ivopts"
+        description: "Loop IVopt framework"
+        match:
+          file: "^/gcc/*loop-iv*"
+      - name: "loop"
+        description: "Loop optimizations"
+        match:
+          file: "^/gcc/*loop*"
+      - name: "openacc"
+        description: "OpenACC framework"
+        # No auto-match entry
+      - name: "openmp"
+        description: "OpenMP framework"
+        match:
+          file: "^/gcc/omp*"
+      - name: "analyzer"
+        description: "Static analyzer"
+        match:
+          file: "^/gcc/analyzer/"
+  # Labels relating to language frontends
+  - group: "Frontend"
+    exclusive: false
+    color: "0052cc"
+    match_defaults:
+      class: "file"
+    labels:
+      - name: "ada"
+        description: "The Ada frontend"
+        match:
+          file: "^/gcc/ada/"
+      - name: "algol68"
+        description: "The Algol68 frontend"
+        match:
+          file: "^/gcc/algol68/"
+      - name: "c"
+        description: "The C frontend"
+        match:
+          file: "^/gcc/c(|-family)/"
+      - name: "c++"
+        description: "The C++ frontend"
+        match:
+          file: "^/gcc/c(p|-family)/"
+      - name: "cobol"
+        description: "The COBOL frontend"
+        match:
+          file: "^/gcc/cobol/"
+      - name: "d"
+        description: "The D frontend"
+        match:
+          file: "^/gcc/d/"
+      - name: "fortran"
+        description: "The Fortran frontend"
+        match:
+          file: "^/gcc/fortran/"
+      - name: "go"
+        description: "The Go frontend"
+        match:
+          file: "^/gcc/(go/|godump*)"
+      - name: "m2"
+        description: "The Modula-2 frontend"
+        match:
+          file: "^/gcc/m2/"
+      - name: "objc"
+        description: "The Objective-C/-C++ frontends"
+        match:
+          file: "^/gcc/objcp?/"
+      - name: "rust"
+        description: "The Rust frontend"
+        match:
+          file: "^/gcc/rust/"
+  # Labels relating to testsuites (probably incomplete)
+  - group: "Tests"
+    exclusive: false
+    color: "c544ff"
+    match_defaults:
+      class: "file"
+    labels:
+      - name: "framework"
+        description: "Changes to the test framework (testsuite/lib)"
+        match:
+          file: "testsuite/lib/"
+      - name: "torture"
+        description: "Changes to legacy torture tests"
+        match:
+          file: "^/gcc/testsuite/*-torture/"
+      - name: "target"
+        description: "Changes to target-specific tests (use target/* as well)"
+        match:
+          file: "^/gcc/testsuite/*.target/"
+      - name: "lang"
+        description: "Changes to language-specific tests"
+        match:
+          file: 
"^/gcc/testsuite/(gcc|gdc|gfortran|g++|obj|go|cobol|gnat|ada|c-c++|gm2|rust)*/"
+      - name: "lib"
+        description: "Changes to top-level library tests (use Library/* as 
well)"
+        match:
+          file: "^/lib*/**/testsuite/"
+      - name: "jit"
+        description: "Changes to GCC's jit tests"
+        match:
+          file: "^/gcc/testsuite/jit.dg/"
+      - name: "selftest"
+        description: "Changes to GCC's self-tests"
+        match:
+          file: "^/gcc/testsuite/selftests/"
+      - name: "diagnostics"
+        description: "Changes to GCC's diagnostic framework tests"
+        match:
+          file: "^/gcc/testsuite/libgdiagnostics.dg/"
+      - name: "sarif"
+        description: "Changes to GCC's sarif tests"
+        match:
+          file: "^/gcc/testsuite/sarif-replay.dg/"
+
+  # # Forge status labels
+  # These have been removed for now.  It's not clear how useful
+  # these would be, given that we may have more than one runner
+  #
+  # # Labels for runners (exclusive)
+  # - group: "Runners"
+  #   exclusive: true
+  #   color: "f6c6c7"
+  #   match_defaults:
+  #     class: "none"
+  #   labels:
+  #     - name: "pending"
+  #       description: "Runner has started, but results not yet available"
+  #     - name: "unresolved"
+  #       description: "Runner failed for an unknown reason"
+  #     - name: "red"
+  #       description: "Check failed with errors, code not fit for committing"
+  #     - name: "amber"
+  #       description: "Check failed with warnings, manually inspect results"
+  #     - name: "green"
+  #       description: "Checks passed"
+  - group: "Reviewed"
+    exclusive: true
+    color: "616161"
+    match_defaults:
+      class: "file"
+    labels:
+      - name: Confirmed
+        description: "Issue has been confirmed"
diff --git a/contrib/forge/update-labels.py b/contrib/forge/update-labels.py
new file mode 100755
index 000000000000..b8e81b617e43
--- /dev/null
+++ b/contrib/forge/update-labels.py
@@ -0,0 +1,151 @@
+#! /usr/bin/env python3
+# Update the list of labels on a Forgejo instance
+
+# Copyright (C) 2026 Free Software Foundation, Inc.
+#
+# This file is part of GCC.
+#
+# GCC 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, or (at your option) any later
+# version.
+#
+# GCC 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 GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.
+
+# Variables needed by the script.  Can be overridden by setting the
+# same name in the environment
+
+PROJECT = "gcc"
+REPO    = "gcc-test"
+FORGE  = "https://forge.sourceware.org/api/v1";
+
+APIKEY  = None
+
+import os, sys, forgelabels, urllib.request, json, getpass
+
+def setup():
+    global PROJECT, REPO, FORGE, APIKEY
+    PROJECT = os.getenv("PROJECT", PROJECT)
+    REPO = os.getenv("REPO", REPO)
+    FORGE = os.getenv("FORGE", FORGE)
+    APIKEY = getpass.getpass(prompt="API key: ")
+    print (f"Accessing {FORGE}/{PROJECT}/{REPO}.")
+
+def read_existing():
+    global FORGE, PROJECT, REPO
+    try:
+        url = f"{FORGE}/repos/{PROJECT}/{REPO}/labels"
+        reply = urllib.request.urlopen(url)
+        labels_list = json.loads(reply.read())
+    except Exception as e:
+        print (f"Error reading label data: {e}")
+        sys.exit (1)
+    return labels_list
+
+def add_label(l):
+    global FORGE, PROJECT, REPO, APIKEY
+    payload = {'name': l.full_name,
+               'color': l.color(),
+               'exclusive': l.group.exclusive,
+               'is_archived': False}
+    if l.desc != None:
+        payload['description'] = l.desc
+    headers = {'Authorization': f"token {APIKEY}",
+               'accept': "application/json",
+               'Content-Type': "application/json"} 
+    try:
+        url = f"{FORGE}/repos/{PROJECT}/{REPO}/labels"
+        payload = json.dumps (payload).encode("utf-8")
+        request = urllib.request.Request (url, data = payload,
+                                          headers = headers,
+                                          method = 'POST')
+        reply = urllib.request.urlopen(request)
+    except Exception as e:
+        print (f"Error writing label data: {e}")
+        sys.exit (1)
+    print (f"Success: add {l.full_name}")
+
+def modify_label(o, l):
+    global FORGE, PROJECT, REPO, APIKEY
+    payload = {'name': l.full_name,
+               'color': l.color(),
+               'exclusive': l.group.exclusive,
+               'is_archived': False}
+    if l.desc != None:
+        payload['description'] = l.desc
+    headers = {'Authorization': f"token {APIKEY}",
+               'accept': "application/json",
+               'Content-Type': "application/json"} 
+    try:
+        url = f"{FORGE}/repos/{PROJECT}/{REPO}/labels/{o['id']}"
+        payload = json.dumps (payload).encode("utf-8")
+        request = urllib.request.Request (url, data = payload,
+                                          headers = headers,
+                                          method = 'PATCH')
+        reply = urllib.request.urlopen(request)
+    except Exception as e:
+        print (f"Error writing label data: {e}")
+        sys.exit (1)
+    print (f"Success: modify {l.full_name}")
+
+def archive_label(o):
+    global FORGE, PROJECT, REPO, APIKEY
+    payload = {'name': o['name'],
+               'is_archived': True}
+    headers = {'Authorization': f"token {APIKEY}",
+               'accept': "application/json",
+               'Content-Type': "application/json"} 
+    try:
+        url = f"{FORGE}/repos/{PROJECT}/{REPO}/labels/{o['id']}"
+        payload = json.dumps (payload).encode("utf-8")
+        request = urllib.request.Request (url, data = payload,
+                                          headers = headers,
+                                          method = 'PATCH')
+        reply = urllib.request.urlopen(request)
+    except Exception as e:
+        print (f"Error writing label data: {e}")
+        sys.exit (1)
+    print (f"Success: archive {o['name']}")
+
+def main():
+    setup()
+    groups = forgelabels.load()
+    oldlabels = read_existing()
+    toadd = []
+    tochange = []
+
+    for g in groups:
+        for l in g.labels:
+            matched = False
+            for o in oldlabels:
+                if o['name'] == l.full_name:
+                    if (o['is_archived']
+                        or o['color'] != l.color()
+                        or o['exclusive'] != l.group.exclusive
+                        or o['description'] != l.desc):
+                        tochange.append((o, l))
+                    matched = True
+                    o['matched'] = True
+                    break
+            if not matched:
+                toadd.append(l)
+    for o in oldlabels:
+        if not 'matched' in o and not o['is_archived']:
+            archive_label(o)
+            print (f"To archive: {o['name']}")
+    for o, n in tochange:
+        modify_label (o, n)
+    for l in toadd:
+        add_label (l)
+
+if __name__ == "__main__":
+    main()
+
+    
-- 
2.54.0


Reply via email to