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