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

Reply via email to