aboutsummaryrefslogtreecommitdiffstats
#!/usr/bin/env python3
#
# InspIRCd -- Internet Relay Chat Daemon
#
#   Copyright (C) 2026 Sadie Powell <sadie@witchery.services>
#
# This file is part of InspIRCd.  InspIRCd 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, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

import os
import re
import shutil
import subprocess
import sys
import textwrap

import columnize
import yaml

CC_RED = "\x1B[1;31m" if sys.stdout.isatty() else ""
CC_RESET = "\x1B[0m" if sys.stdout.isatty() else ""

ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))

GIT = os.getenv("GIT", "git")
if not shutil.which(GIT):
    print(
        f"{CC_RED}Error:{CC_RESET} Git ({GIT}) must be installed to build the author list!",
        file=sys.stderr,
    )

commit_log = subprocess.check_output(
    [GIT, "log", "--pretty=%an <%ae>%n%(trailers:key=Co-Authored-By,valueonly)", "HEAD"]
).split(b"\n")

committers = {}
for committer in [c for c in commit_log if c]:
    committers[committer] = committers.get(committer, 0) + 1

authors = {}
for committer, commits in committers.items():
    author = (
        subprocess.check_output([GIT, "check-mailmap", committer])
            .strip()
            .decode("utf-8")
    )

    # Remove email addresses that aren"t real.
    author = re.sub(
        r"^(.+) <(?:unknown@email.invalid|\S+@users.noreply.github.com)>$",
        r"\1",
        author,
    )

    # Skip GitHub bots like dependabot.
    if author.endswith("[bot]"):
        continue

    # Skip commits made by our own scripts.
    if author == "InspIRCd Robot <noreply@inspircd.org>":
        continue

    authors[author] = authors.get(author, 0) + commits

authors_txt = os.path.join(ROOT, "docs", "AUTHORS.txt")
with open(authors_txt, "w") as fh:
    print(
        textwrap.dedent(
            f"""
            Since the first commit in January 2003 {len(authors)} people have submitted patches,
            commits, and other useful contributions to InspIRCd. These people, ordered by
            the number of contributions they have made, are:
            """
        ).lstrip(),
        file=fh,
    )

    # We show the names in the authors file ordered by number of commits and
    # then name case insensitively.
    sorted_authors = list(authors.items())
    sorted_authors.sort(key=lambda a: a[0].lower())
    sorted_authors.sort(key=lambda a: a[1], reverse=True)
    for author, commits in sorted_authors:
        print(f"  * {author}", file=fh)

info_h = os.path.join(ROOT, "modules", "core", "core_info", "info.h")
with open(info_h, "w") as ofh:
    print(
        textwrap.dedent(
            """
            // This file was generated by mkauthors. Any changes will be overwritten.
            #pragma once
            static constexpr const char* const lines[] = {
            \t" _____                        _____   _____    _____      _ ",
            \t"|_   _|                      |_   _| |  __ \\\\  / ____|    | |",
            \t"  | |    _ __    ___   _ __    | |   | |__) || |       __| |",
            \t"  | |   | '_ \\\\  / __| | '_ \\\\   | |   |  _  / | |      / _` |",
            \t" _| |_  | | | | \\\\__ \\\\ | |_) | _| |_  | | \\\\ \\\\ | |____ | (_| |",
            \t"|_____| |_| |_| |___/ | .__/ |_____| |_|  \\\\_\\\\ \\\\_____| \\\\__,_|",
            \t" _____________________| |__________________________________ ",
            \t"|_____________________|_|__________________________________|",
            """
        ).strip(),
        file=ofh,
    )
    info_yml = os.path.join(ROOT, "modules", "core", "core_info", "info.yml")
    with open(info_yml, "r") as ifh:
        seen = []
        for team, members in yaml.safe_load(ifh).items():
            print(f'\t" ",\n\t"\\002{team}\\002:",', file=ofh)

            names = []
            if isinstance(members, list):
                for member in members:
                    if "name" in member:
                        names.append(f"{member["name"]} ({member["nick"]})")
                        seen.append(member["name"])
                    else:
                        names.append(member["nick"])
            else:
                for author in authors:
                    name = re.sub(r"^(.+) <.+>$", r"\1", author)
                    if name not in seen:
                        names.append(name)

            names.sort(key=lambda n: n.lower())
            lines = (
                columnize.columnize(
                    names, arrange_vertical=False, colsep=" " * 4, displaywidth=78
                )
                .strip()
                .split("\n")
            )
            for line in lines:
                print(f'\t"  {line}",', file=ofh)

    print(
        textwrap.dedent(
            """
            \t"",
            \t"For more information visit https://www.inspircd.org/ or our IRC channel at",
            \t"ircs://irc.teranova.net/inspircd.",
            \tnullptr,
            };
            """
        ).strip(),
        file=ofh,
    )

if int(os.getenv("MKAUTHORS_COMMIT", "1")) > 0:
    subprocess.check_call(
        [
            GIT,
            "commit",
            "--author", "InspIRCd Robot <noreply@inspircd.org>",
            "--message", "Update the author list.",
            "--",
            authors_txt,
            info_h,
        ]
    )