Skip to content

Commit 979880e

Browse files
authored
Refactor AUTHORS (#2043)
1 parent 5f61ed9 commit 979880e

File tree

1 file changed

+131
-60
lines changed

1 file changed

+131
-60
lines changed

AUTHORS.py

Lines changed: 131 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1+
""" Generate the AUTHORS.rst file from git commit history.
2+
3+
This module reads git commit logs and produces a formatted list of contributors
4+
grouped by their contribution count, mapping email aliases and GitHub usernames.
5+
"""
6+
7+
from dataclasses import dataclass
18
import math
2-
import subprocess
9+
import subprocess # noqa: S404
310

4-
print(
5-
"""Contributors
6-
============
711

8-
All contributors (by number of commits):
9-
"""
10-
)
12+
def main() -> None:
13+
""" Generate and print the AUTHORS.rst content. """
14+
15+
contributors = get_git_contributors()
16+
print_contributors(contributors)
17+
# ------------------------------------------------------------------------------
1118

1219

13-
email_map = {
20+
EMAIL_ALIASES: dict[str, str | None] = {
1421
# Maintainers.
1522
1623
@@ -27,7 +34,7 @@
2734
2835
}
2936

30-
name_map = {
37+
CANONICAL_NAMES: dict[str, str] = {
3138
"[email protected]": "Casper van der Wel",
3239
"[email protected]": "Dan Allan",
3340
"[email protected]": "Manuel Goacolou",
@@ -37,7 +44,7 @@
3744
"[email protected]": "Xinran Xu",
3845
}
3946

40-
github_map = {
47+
GITHUB_USERNAMES: dict[str, str] = {
4148
"[email protected]": "billyshambrook",
4249
"[email protected]": "danielballan",
4350
"[email protected]": "adavoudi",
@@ -55,57 +62,121 @@
5562
"[email protected]": "xxr3376",
5663
"[email protected]": "laggykiller",
5764
"[email protected]": "WyattBlue",
65+
"[email protected]": "dotysan",
5866
}
5967

6068

61-
email_count = {}
62-
for line in (
63-
subprocess.check_output(["git", "log", "--format=%aN,%aE"]).decode().splitlines()
64-
):
65-
name, email = line.strip().rsplit(",", 1)
66-
67-
email = email_map.get(email, email)
68-
if not email:
69-
continue
70-
71-
names = name_map.setdefault(email, set())
72-
if isinstance(names, set):
73-
names.add(name)
74-
75-
email_count[email] = email_count.get(email, 0) + 1
76-
77-
78-
last = None
79-
block_i = 0
80-
for email, count in sorted(email_count.items(), key=lambda x: (-x[1], x[0])):
81-
# This is the natural log, because of course it should be. ;)
82-
order = int(math.log(count))
83-
if last and last != order:
84-
block_i += 1
85-
print()
86-
last = order
87-
88-
names = name_map[email]
89-
if isinstance(names, set):
90-
name = ", ".join(sorted(names))
91-
else:
92-
name = names
93-
94-
github = github_map.get(email)
95-
96-
# The '-' vs '*' is so that Sphinx treats them as different lists, and
97-
# introduces a gap between them.
98-
if github:
99-
print(
100-
"%s %s <%s>; `@%s <https://github.com/%s>`_"
101-
% ("-*"[block_i % 2], name, email, github, github)
102-
)
103-
else:
104-
print(
105-
"%s %s <%s>"
106-
% (
107-
"-*"[block_i % 2],
108-
name,
109-
email,
69+
@dataclass
70+
class Contributor:
71+
""" Represents a contributor with their email, names, and GitHub username. """
72+
73+
email: str
74+
names: set[str]
75+
github: str | None = None
76+
commit_count: int = 0
77+
78+
@property
79+
def display_name(self) -> str:
80+
""" Return the formatted display name for the contributor.
81+
82+
Returns:
83+
Comma-separated sorted list of contributor names.
84+
"""
85+
86+
return ", ".join(sorted(self.names))
87+
88+
def format_line(self, bullet: str) -> str:
89+
""" Format the contributor line for RST output.
90+
91+
Args:
92+
bullet: The bullet character to use (- or *).
93+
94+
Returns:
95+
Formatted RST line with contributor info.
96+
"""
97+
98+
if self.github:
99+
return (
100+
f"{bullet} {self.display_name} <{self.email}>; "
101+
f"`@{self.github} <https://github.com/{self.github}>`_"
102+
)
103+
return f"{bullet} {self.display_name} <{self.email}>"
104+
105+
106+
def get_git_contributors() -> dict[str, Contributor]:
107+
""" Parse git log and return contributors grouped by canonical email.
108+
109+
Returns:
110+
Dictionary mapping canonical emails to Contributor objects.
111+
"""
112+
113+
contributors: dict[str, Contributor] = {}
114+
git_log = subprocess.check_output(
115+
["git", "log", "--format=%aN,%aE"], # noqa: S607
116+
text=True,
117+
).splitlines()
118+
119+
for line in git_log:
120+
name, email = line.strip().rsplit(",", 1)
121+
canonical_email = EMAIL_ALIASES.get(email, email)
122+
123+
if not canonical_email:
124+
continue
125+
126+
if canonical_email not in contributors:
127+
contributors[canonical_email] = Contributor(
128+
email=canonical_email,
129+
names=set(),
130+
github=GITHUB_USERNAMES.get(canonical_email),
110131
)
111-
)
132+
133+
contributor = contributors[canonical_email]
134+
contributor.names.add(name)
135+
contributor.commit_count += 1
136+
137+
for email, canonical_name in CANONICAL_NAMES.items():
138+
if email in contributors:
139+
contributors[email].names = {canonical_name}
140+
141+
return contributors
142+
143+
144+
def print_contributors(contributors: dict[str, Contributor]) -> None:
145+
"""Print contributors grouped by logarithmic order of commits.
146+
147+
Args:
148+
contributors: Dictionary of contributors to print.
149+
"""
150+
151+
print("""\
152+
Contributors
153+
============
154+
155+
All contributors (by number of commits):
156+
""".replace(" ", ""))
157+
158+
sorted_contributors = sorted(
159+
contributors.values(),
160+
key=lambda c: (-c.commit_count, c.email),
161+
)
162+
163+
last_order: int | None = None
164+
block_index = 0
165+
166+
for contributor in sorted_contributors:
167+
# This is the natural log, because of course it should be. ;)
168+
order = int(math.log(contributor.commit_count))
169+
170+
if last_order and last_order != order:
171+
block_index += 1
172+
print()
173+
last_order = order
174+
175+
# The '-' vs '*' is so that Sphinx treats them as different lists, and
176+
# introduces a gap between them.
177+
bullet = "-*"[block_index % 2]
178+
print(contributor.format_line(bullet))
179+
180+
181+
if __name__ == "__main__":
182+
main()

0 commit comments

Comments
 (0)