weblate/scripts/list-contributors.py
2026-02-13 16:10:40 +01:00

156 lines
4.1 KiB
Python
Executable file

#!/usr/bin/env python3
# Copyright © Michal Čihař <michal@weblate.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
import argparse
from collections import defaultdict
from pathlib import Path
from typing import TYPE_CHECKING, Literal
from git import Repo
import weblate.utils.version
if TYPE_CHECKING:
from git import Commit
from git.diff import Diff
VERSION = weblate.utils.version.VERSION_BASE
ROOT_DIR = Path(__file__).parent.parent
CategoryType = Literal["code", "translations", "docs"]
# Ignore known bots
IGNORE_AUTHORS: tuple[str, ...] = (
"GitHub",
"Anonymous",
"Hosted Weblate",
"Copilot",
"root",
)
# GitHub sometimes uses username instead of full name
MAP_AUTHORS: dict[str, str] = {
"nijel": "Michal Čihař",
"gersona": "Gersona",
"gers": "Gersona",
"KarenKonou": "Karen Konou",
}
CATEGORIES: dict[CategoryType, str] = {
"code": "Code contributions",
"translations": "Translations contributions",
"docs": "Documentation contributions",
}
TEMPLATE = """
{label}
.. only:: not gettext
{contributors}"""
def get_file_category(filename: str) -> CategoryType:
if filename.endswith((".po", ".pot")):
return "translations"
if filename.endswith(".rst") or filename.startswith("docs/"):
return "docs"
return "code"
def get_diff_categories(diff: list[Diff]) -> set[CategoryType]:
categories = set()
for item in diff:
if item.a_path:
categories.add(get_file_category(item.a_path))
if item.b_path:
categories.add(get_file_category(item.b_path))
return categories
def is_valid_author(author: str) -> bool:
return (
"(bot)" not in author
and "[bot]" not in author
and "add-on" not in author
and not author.startswith(IGNORE_AUTHORS)
)
def get_commit_authors(commit: Commit) -> set[str]:
authors: list[str] = [str(commit.author)]
if commit.committer:
authors.append(str(commit.committer))
authors.extend(
line[15:].split("<")[0].strip()
for line in str(commit.message).splitlines()
if line.lower().startswith("co-authored-by:")
)
return {
MAP_AUTHORS.get(author, author) for author in authors if is_valid_author(author)
}
def get_contributors() -> dict[CategoryType, list[str]]:
contributors: dict[CategoryType, list[str]] = defaultdict(list)
repo = Repo.init(ROOT_DIR)
tags = sorted(
(tag for tag in repo.tags if tag.name.startswith("weblate-")),
key=lambda tag: tag.commit.committed_date,
)
recent_tag = tags[-1]
commits = list(repo.iter_commits(f"{recent_tag}..HEAD"))
previous = recent_tag.object.object
for commit in reversed(commits):
authors = get_commit_authors(commit)
if authors:
diff = previous.diff(commit.tree)
for category in get_diff_categories(diff):
for author in authors:
if author not in contributors[category]:
contributors[category].append(author)
previous = commit.tree
return contributors
def get_contributors_text() -> str:
all_contributors = get_contributors()
output = []
for category, label in CATEGORIES.items():
contributors = all_contributors[category]
if contributors:
output.append(
TEMPLATE.format(label=label, contributors=", ".join(contributors))
)
return "\n".join(output)
def main() -> None:
parser = argparse.ArgumentParser(
prog="list-contributors.py",
description="Lists contributors in a repository since last tag",
)
parser.add_argument(
"-u", "--update", action="store_true", help="Updates docs/changes.rst"
)
args = parser.parse_args()
text = get_contributors_text()
if args.update:
changelog_file = (
ROOT_DIR / "docs" / "changes" / "contributors" / f"{VERSION}.rst"
)
changelog_file.write_text(f"{text}\n")
else:
print(text)
if __name__ == "__main__":
main()