From: Richard Earnshaw <[email protected]>
This script is intended to help with the most common case of creating
a new entry in the MAINTAINERS.yml data - adding the basic data for a
new account. It takes care of creating the entry in the canonical
location within the file and collecting the basic data. It only adds
the write-after role, but it should be trivial to add any additional
roles if necessary following the examples elsewhere.
contrib/ChangeLog:
* add-write-after.py: New file.
---
contrib/add-write-after.py | 150 +++++++++++++++++++++++++++++++++++++
1 file changed, 150 insertions(+)
create mode 100755 contrib/add-write-after.py
diff --git a/contrib/add-write-after.py b/contrib/add-write-after.py
new file mode 100755
index 000000000000..9c93324bc909
--- /dev/null
+++ b/contrib/add-write-after.py
@@ -0,0 +1,150 @@
+#! /usr/bin/env python3
+
+# Add a new Write-After account to MAINTAINERS.yml
+
+# 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 COPYING. If not, write to
+# the Free Software Foundation, 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+
+import os
+import pwd
+import re
+import sys
+import unidecode
+import yaml
+
+from optparse import OptionParser
+
+import maintainer_utils as maintutils
+
+def unilower(txt):
+ """return a lower-case version of txt, mapping accented characters
+ onto their ASCII near equivalents."""
+ return unidecode.unidecode(txt).lower()
+
+def get_surname(name):
+ parts = name.split()
+ surname = parts[-1]
+
+ if surname.startswith("d'"):
+ surname = surname[2:]
+
+ return surname
+
+def ask(prompt, default, required=True):
+ while True:
+ print(f"{prompt} [{default if default else '(no default)'}]: ", end="",
+ flush=True)
+ result = sys.stdin.readline().strip()
+ if result != "":
+ return result
+ if default or not required:
+ return default
+
+def email_valid(e):
+ template = r"^[A-Za-z0-9._%+-]+@(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}$"
+ if re.match(template, e):
+ return True
+ return False
+
+def getuserdata():
+ user = pwd.getpwuid(os.getuid())
+ # Make an initial guess that the name and GCC account will match
+ # the local account name.
+ # GECOS fields sometime contain a comma-separated list; in that case
+ # we only want the first component (the user's name)
+ print ("Please enter the details of your Sourceware account.")
+ print ("Some details have been guessed, based on your local account,")
+ print ("but you can adjust as required.")
+ newuser = {
+ 'sn': "",
+ 'cn': "",
+ 'email': [],
+ 'roles': [],
+ 'account': "",
+ }
+ newuser['cn'] = ask("Your name", user.pw_gecos.split(",", 1)[0])
+ newuser['sn'] = ask("Your surname (only used for sorting entries)",
+ get_surname(newuser['cn']))
+ account = ask("Your sourceware account", user.pw_name)
+ while True:
+ e = ask("Primary email address", None)
+ if email_valid(e):
+ break
+ print ("That address does not look valid.")
+ newuser['email'].append(e)
+ while (e := ask("Additional email (return to stop)", None,
+ required=False)):
+ if email_valid(e):
+ newuser['email'].append(e)
+ else:
+ print ("That address does not look valid. Ignored.")
+ gcc_email = account + "@gcc.gnu.org"
+ if not gcc_email in newuser['email']:
+ print(f"Appending {gcc_email} to the list of email addresses")
+ newuser['email'].append(gcc_email)
+ newuser['roles'] = list()
+ newuser['roles'].append('WriteAfter')
+ newuser['account'] = account
+ print("If you are using a Developer Certificate of Origin (DCO)")
+ print("you can add appropriate email addresses here")
+ while (e := ask("DCO email (return to stop)", None, required=False)):
+ if email_valid(e):
+ newuser['roles'].append({'DCO': e})
+ else:
+ print ("That address does not look valid. Ignored.")
+ return newuser
+
+def main():
+ optp = OptionParser(usage="Usage: %prog [<options>] <maintainers.yml>")
+ optp.add_option("-o", "--output", metavar="FILE",
+ default=None, dest="outfilename",
+ help="Write to FILE instead of updating original file")
+ opts, args = optp.parse_args()
+ if len(args) != 1:
+ optp.print_help()
+ sys.exit(1)
+
+ data = maintutils.load(args[0])
+ # Check the data before we try to update it.
+ maintutils.validate(data)
+ newuser = getuserdata()
+ if not newuser:
+ return 0
+ existing = [u for u in filter(lambda x: (x.get('account')
+ == newuser['account']),
+ data['users'])]
+ if len(existing):
+ print(f"Sorry that account name ({newuser['account']}) has already
been used.")
+ print("Note, this script can only be used to add new accounts.")
+ return 1
+ data['users'].append (newuser)
+ data['users'] = sorted(data['users'],
+ key = lambda k: (unilower(k['sn']),
+ unilower(k['cn'])))
+ if opts.outfilename and opts.outfilename != '-':
+ outfd = open (opts.outfilename, "w", encoding="utf-8")
+ elif opts.outfilename and opts.outfilename == '-':
+ outfd = sys.stdout
+ else:
+ outfd = open (args[0], "w", encoding="utf-8")
+ yaml.dump (data, outfd, allow_unicode = True, sort_keys = False)
+ return 0
+
+if __name__ == "__main__":
+ sys.exit (main())
--
2.54.0