fame.py: also generate FAME.svg, a chart of the top contributors

git-fame's own --format=svg renders the whole table as monospaced text,
which works for a handful of contributors but not for a few hundred, so
draw a bar chart of the top 20 (--svg-top) instead.  FAME.md embeds it.

The chart follows the reader's colour scheme, sizes its viewport in user
units plus a viewBox (em on the root <svg> would resolve against the wrong
font size) and escapes author names, which are commit-controlled input.
Bots keep their row in the table, but do not take a chart slot away from a
person.

The workflow now also runs when FAME.md changes on master, so that the two
files cannot drift apart.
This commit is contained in:
Thomas Waldmann
2026-08-02 12:08:48 +02:00
parent 79bd383aa2
commit 5fdc5aa280
4 changed files with 251 additions and 62 deletions
+16 -5
View File
@@ -1,4 +1,5 @@
# Refresh FAME.md, the contributor summary generated by scripts/fame.py.
# Refresh FAME.md and FAME.svg, the contributor summary generated by
# scripts/fame.py.
#
# The script needs the full history (it uses `git blame`), so this checks out
# with fetch-depth 0 and it always looks at master, no matter what triggered it.
@@ -6,14 +7,22 @@
# quiet week does not produce a pull request that only bumps the date.
#
# The pull request is created with GITHUB_TOKEN, so CI does not run on it - that
# is fine for a generated markdown file, but keep it in mind if this workflow
# ever starts touching code.
# is fine for generated files, but keep it in mind if this workflow ever starts
# touching code.
#
# Merging such a pull request pushes FAME.md and triggers this workflow again;
# that run regenerates the same files, finds no diff and opens nothing, so it
# just costs a minute of runner time.
name: FAME
on:
schedule:
- cron: '17 4 * * 1' # Mondays at 04:17 UTC
push:
branches: [ master ]
paths:
- 'FAME.md' # keep the chart in sync when the table changes
workflow_dispatch: # Allow manual trigger
concurrency:
@@ -51,13 +60,15 @@ jobs:
python -m pip install --upgrade pip
pip install 'git-fame ~= 3.1'
- name: Regenerate FAME.md
- name: Regenerate FAME.md and FAME.svg
run: python scripts/fame.py
- name: Create pull request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
add-paths: FAME.md
add-paths: |
FAME.md
FAME.svg
branch: auto/update-fame
delete-branch: true
commit-message: "FAME.md: update contributor statistics"
+47 -45
View File
@@ -1,9 +1,11 @@
# Contributors
370 people have contributed to Borg, with 10,581 commits in total.
370 people have contributed to Borg, with 10,585 commits in total.
Thanks to everyone who helped!
Generated on 2026-08-01 by `scripts/fame.py`, which computes the statistics with
![Contributors by commit count](FAME.svg)
Generated on 2026-08-02 by `scripts/fame.py`, which computes the statistics with
[git-fame](https://github.com/casperdcl/git-fame), from the `master` branch
only - commits that exist solely on other branches or in unmerged pull requests
are not counted.
@@ -19,64 +21,64 @@ show up there, so the number understates early contributions. Generated files
| Contributor | Commits | Lines | Files |
|:---|---:|---:|---:|
| Thomas Waldmann | 7,020 | 73,353 | 451 |
| Marian Beermann | 1,140 | 9,016 | 158 |
| Jonas Borgström | 560 | 1,080 | 38 |
| Antoine Beaupré | 285 | 456 | 35 |
| Thomas Waldmann | 7,024 | 75,119 | 454 |
| Marian Beermann | 1,140 | 8,400 | 157 |
| Jonas Borgström | 560 | 1,029 | 38 |
| Antoine Beaupré | 285 | 441 | 33 |
| Mrityunjay Raj | 198 | 7,988 | 102 |
| Andrey Bienkowski | 72 | 243 | 17 |
| Andrey Bienkowski | 72 | 210 | 16 |
| Ted Lawson | 71 | 4,606 | 39 |
| Thalian | 63 | 771 | 33 |
| Martin Hostettler | 46 | 2,589 | 10 |
| Milkey Mouse | 42 | 253 | 14 |
| Thalian | 63 | 594 | 32 |
| Martin Hostettler | 46 | 2,559 | 9 |
| Milkey Mouse | 42 | 248 | 14 |
| Dan Christensen | 40 | 5 | 2 |
| dependabot[bot] | 37 | 46 | 7 |
| Rayyan Ansari | 34 | 91 | 12 |
| Abdel-Rahman | 30 | 4 | 3 |
| Manuel Riel | 28 | 107 | 7 |
| Nehalenniæ Oudin | 26 | 55 | 11 |
| Manuel Riel | 28 | 94 | 6 |
| Nehalenniæ Oudin | 26 | 49 | 11 |
| anarcat | 24 | 34 | 4 |
| Hugo Wallenburg | 23 | 1,189 | 13 |
| Michael Hanselmann | 22 | 151 | 3 |
| Michael Hanselmann | 22 | 71 | 3 |
| Björn Ketelaars | 17 | 33 | 6 |
| SanskritFritz | 17 | 258 | 1 |
| Lee Bousfield | 14 | 169 | 10 |
| Alan Jenkins | 14 | 47 | 2 |
| Lee Bousfield | 14 | 160 | 9 |
| Alan Jenkins | 14 | 38 | 2 |
| Robin Schneider | 13 | 19 | 8 |
| Daniel Rudolf | 13 | 230 | 14 |
| Jürg Rast | 12 | 78 | 11 |
| Elmar Hoffmann | 12 | 98 | 9 |
| Ronny Pfannschmidt | 12 | 22 | 4 |
| Jürg Rast | 12 | 73 | 11 |
| Elmar Hoffmann | 12 | 90 | 6 |
| Ronny Pfannschmidt | 12 | 20 | 4 |
| mh4ckt3mh4ckt1c4s | 11 | 0 | 0 |
| Teemu Toivanen | 10 | 16 | 2 |
| Lauri Niskanen | 10 | 5 | 2 |
| finefoot | 10 | 18 | 3 |
| James Buren | 10 | 10 | 2 |
| James Buren | 10 | 2 | 2 |
| Per Guth | 9 | 1 | 1 |
| rugk | 9 | 13 | 5 |
| Ed Blackman | 9 | 14 | 3 |
| Ed Blackman | 9 | 13 | 3 |
| Carlo Teubner | 9 | 3 | 3 |
| Emmo Emminghaus | 9 | 87 | 11 |
| Emmo Emminghaus | 9 | 80 | 10 |
| Josh Soref | 8 | 3 | 3 |
| Jakob Schnitzer | 8 | 35 | 1 |
| Gianfranco Costamagna | 7 | 10 | 6 |
| Frank Sachsenheim | 7 | 3 | 1 |
| Frank Sachsenheim | 7 | 2 | 1 |
| Hartmut Goebel | 7 | 0 | 0 |
| Michael Deyaso | 6 | 170 | 14 |
| Alf Mikula | 6 | 19 | 1 |
| William D. Jones | 6 | 250 | 6 |
| Felix Schwarz | 6 | 5 | 2 |
| Peter Gerber | 6 | 105 | 10 |
| Felix Schwarz | 6 | 4 | 2 |
| Peter Gerber | 6 | 103 | 9 |
| Alexander-N | 6 | 0 | 0 |
| Paul D | 5 | 185 | 46 |
| Mark Edgington | 5 | 11 | 3 |
| Mark Edgington | 5 | 10 | 3 |
| Dominik Stadler | 5 | 77 | 1 |
| infectormp | 5 | 18 | 2 |
| remyabel | 5 | 25 | 3 |
| Alexander 'Leo' Bergolth | 5 | 26 | 3 |
| Simon Frei | 5 | 213 | 3 |
| Alexander 'Leo' Bergolth | 5 | 24 | 3 |
| Simon Frei | 5 | 174 | 3 |
| elandorr | 5 | 40 | 3 |
| step21 | 5 | 2 | 2 |
| step21 | 5 | 1 | 1 |
| Radu Ciorba | 5 | 0 | 0 |
| Suryansh Pal | 4 | 36 | 9 |
| 8bit | 4 | 30 | 5 |
@@ -92,7 +94,7 @@ show up there, so the number understates early contributions. Generated files
| Artem Sheremet | 4 | 10 | 1 |
| Narendra Vardi | 4 | 3 | 1 |
| Alexander Pyhalov | 4 | 0 | 0 |
| Will | 3 | 22 | 4 |
| Will | 3 | 21 | 4 |
| Lukas Fleischer | 3 | 23 | 2 |
| Daniel Reichelt | 3 | 25 | 5 |
| Stephen | 3 | 10 | 2 |
@@ -106,7 +108,7 @@ show up there, so the number understates early contributions. Generated files
| Michael Gajda | 3 | 23 | 2 |
| jungle-boogie | 3 | 13 | 1 |
| Thomas Harold | 3 | 1 | 1 |
| Gregor Kleen | 3 | 10 | 4 |
| Gregor Kleen | 3 | 8 | 3 |
| kmq | 3 | 15 | 1 |
| Luke Dashjr | 3 | 4 | 1 |
| Divyesh | 3 | 1 | 1 |
@@ -134,7 +136,7 @@ show up there, so the number understates early contributions. Generated files
| Ioannis Cherouvim | 2 | 32 | 2 |
| AJ Jordan | 2 | 2 | 1 |
| Markus Engelbrecht | 2 | 3 | 1 |
| Thomas Portmann | 2 | 257 | 5 |
| Thomas Portmann | 2 | 206 | 5 |
| Leo Famulari | 2 | 1 | 1 |
| Fabian Fröhlich | 2 | 4 | 1 |
| Andreas Gruhler | 2 | 3 | 1 |
@@ -143,7 +145,7 @@ show up there, so the number understates early contributions. Generated files
| Kurt Yoder | 2 | 8 | 1 |
| Jonas Schäfer | 2 | 13 | 1 |
| a1346054 | 2 | 4 | 3 |
| Sophie Herold | 2 | 3 | 2 |
| Sophie Herold | 2 | 2 | 2 |
| ReethuVinta | 2 | 20 | 2 |
| textshell | 2 | 1 | 1 |
| Saurav Sachidanand | 2 | 2 | 1 |
@@ -153,7 +155,7 @@ show up there, so the number understates early contributions. Generated files
| Matthew Glazar | 2 | 18 | 3 |
| Max-Julian Pogner | 2 | 15 | 1 |
| Atharva Varpe | 2 | 23 | 1 |
| Lapinot | 2 | 30 | 4 |
| Lapinot | 2 | 24 | 4 |
| Soumik Dutta | 2 | 85 | 3 |
| Eric Wolf | 2 | 51 | 4 |
| snsmac | 2 | 6 | 2 |
@@ -192,9 +194,9 @@ show up there, so the number understates early contributions. Generated files
| Daniel Dent | 1 | 2 | 1 |
| Lars K.W. Gohlke | 1 | 1 | 1 |
| Kevin Yin | 1 | 3 | 1 |
| Benedikt Seidl | 1 | 117 | 2 |
| Eddie Carswell | 1 | 4 | 1 |
| vancheese | 1 | 28 | 8 |
| Benedikt Seidl | 1 | 116 | 1 |
| Michael Bauer | 1 | 2 | 1 |
| Aleksey Korol | 1 | 95 | 1 |
| Coenraad Loubser | 1 | 1 | 1 |
@@ -236,7 +238,7 @@ show up there, so the number understates early contributions. Generated files
| Stefan Majewsky | 1 | 1 | 1 |
| Atemu | 1 | 11 | 2 |
| Dominik Tugend | 1 | 3 | 1 |
| Reiko Asakura | 1 | 12 | 2 |
| Reiko Asakura | 1 | 11 | 2 |
| Mher Kazandjian | 1 | 6 | 1 |
| Mike | 1 | 23 | 1 |
| Ceesjan Luiten | 1 | 1 | 1 |
@@ -247,9 +249,8 @@ show up there, so the number understates early contributions. Generated files
| Jim Paris | 1 | 2 | 1 |
| vhadzhiev | 1 | 2 | 1 |
| Tarrailt | 1 | 371 | 8 |
| Romain Vimont | 1 | 4 | 3 |
| Sam H | 1 | 27 | 2 |
| Leo Antunes | 1 | 8 | 1 |
| Sam H | 1 | 24 | 2 |
| Leo Antunes | 1 | 5 | 1 |
| Matt Van Horn | 1 | 1 | 1 |
| Mike Mason | 1 | 38 | 3 |
| edvatar | 1 | 16 | 2 |
@@ -258,18 +259,16 @@ show up there, so the number understates early contributions. Generated files
| nyuszika7h | 1 | 3 | 1 |
| Divyansh Singh | 1 | 1 | 1 |
| Phil Kulin | 1 | 4 | 1 |
| Félix Sipma | 1 | 14 | 1 |
| Graham Stockton | 1 | 13 | 1 |
| Félix Sipma | 1 | 13 | 1 |
| Graham Stockton | 1 | 7 | 1 |
| Peter Zangerl | 1 | 1 | 1 |
| lexa-a | 1 | 31 | 1 |
| Nikolaus Rath | 1 | 7 | 1 |
| axapaxa | 1 | 117 | 2 |
| Swanand01 | 1 | 1 | 1 |
| axapaxa | 1 | 105 | 2 |
| Abhijeet | 1 | 1 | 1 |
| Dark Dragon | 1 | 7 | 1 |
| Emil M George | 1 | 2 | 1 |
| Dark Dragon | 1 | 6 | 1 |
| Emil M George | 1 | 1 | 1 |
| Hauke Rehfeld | 1 | 1 | 1 |
| Henry-Joseph Audéoud | 1 | 1 | 1 |
| Ivan Shapovalov | 1 | 1 | 1 |
| Zhou Qiankang | 1 | 3 | 1 |
| Adam Kouse | 1 | 0 | 0 |
@@ -304,6 +303,7 @@ show up there, so the number understates early contributions. Generated files
| Flachz | 1 | 0 | 0 |
| Florian Klink | 1 | 0 | 0 |
| Hans-Peter Jansen | 1 | 0 | 0 |
| Henry-Joseph Audéoud | 1 | 0 | 0 |
| Jakub Wilk | 1 | 0 | 0 |
| Jan | 1 | 0 | 0 |
| Jens Rantil | 1 | 0 | 0 |
@@ -345,9 +345,11 @@ show up there, so the number understates early contributions. Generated files
| RR | 1 | 0 | 0 |
| Ricardo M. Correia | 1 | 0 | 0 |
| Roberto Polli | 1 | 0 | 0 |
| Romain Vimont | 1 | 0 | 0 |
| Ruben Rodriguez | 1 | 0 | 0 |
| Sebastiaan Lokhorst | 1 | 0 | 0 |
| Serghei Iakovlev | 1 | 0 | 0 |
| Swanand01 | 1 | 0 | 0 |
| Thiago Macieira | 1 | 0 | 0 |
| Thomas Deutschmann | 1 | 0 | 0 |
| Thomas Schwinge | 1 | 0 | 0 |
+81
View File
@@ -0,0 +1,81 @@
<svg xmlns="http://www.w3.org/2000/svg" width="760" height="524" viewBox="0 0 760 524" role="img" aria-label="Borg contributors by commit count">
<style>
text { font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif }
.bg { fill: #ffffff }
.title { font-size: 16px; font-weight: 600; fill: #1f2328 }
.name { font-size: 12px; fill: #1f2328 }
.subtitle, .footer, .count { font-size: 11px; fill: #59636e }
.count { font-size: 12px; text-anchor: end }
.bar { fill: #4c74b9 }
@media (prefers-color-scheme: dark) {
.bg { fill: #0d1117 }
.title, .name { fill: #e6edf3 }
.subtitle, .footer, .count { fill: #9198a1 }
.bar { fill: #4493f8 }
}
</style>
<rect class="bg" x="0" y="0" width="760" height="524" rx="6"/>
<text class="title" x="14" y="26">Borg contributors</text>
<text class="subtitle" x="14" y="44">top 20 of 369 by commits on the master branch</text>
<text class="name" x="14" y="73">Thomas Waldmann</text>
<rect class="bar" x="240" y="63" width="436" height="12" rx="2"/>
<text class="count" x="746" y="73">7,024</text>
<text class="name" x="14" y="95">Marian Beermann</text>
<rect class="bar" x="240" y="85" width="71" height="12" rx="2"/>
<text class="count" x="746" y="95">1,140</text>
<text class="name" x="14" y="117">Jonas Borgström</text>
<rect class="bar" x="240" y="107" width="35" height="12" rx="2"/>
<text class="count" x="746" y="117">560</text>
<text class="name" x="14" y="139">Antoine Beaupré</text>
<rect class="bar" x="240" y="129" width="18" height="12" rx="2"/>
<text class="count" x="746" y="139">285</text>
<text class="name" x="14" y="161">Mrityunjay Raj</text>
<rect class="bar" x="240" y="151" width="12" height="12" rx="2"/>
<text class="count" x="746" y="161">198</text>
<text class="name" x="14" y="183">Andrey Bienkowski</text>
<rect class="bar" x="240" y="173" width="4" height="12" rx="2"/>
<text class="count" x="746" y="183">72</text>
<text class="name" x="14" y="205">Ted Lawson</text>
<rect class="bar" x="240" y="195" width="4" height="12" rx="2"/>
<text class="count" x="746" y="205">71</text>
<text class="name" x="14" y="227">Thalian</text>
<rect class="bar" x="240" y="217" width="4" height="12" rx="2"/>
<text class="count" x="746" y="227">63</text>
<text class="name" x="14" y="249">Martin Hostettler</text>
<rect class="bar" x="240" y="239" width="3" height="12" rx="2"/>
<text class="count" x="746" y="249">46</text>
<text class="name" x="14" y="271">Milkey Mouse</text>
<rect class="bar" x="240" y="261" width="3" height="12" rx="2"/>
<text class="count" x="746" y="271">42</text>
<text class="name" x="14" y="293">Dan Christensen</text>
<rect class="bar" x="240" y="283" width="2" height="12" rx="2"/>
<text class="count" x="746" y="293">40</text>
<text class="name" x="14" y="315">Rayyan Ansari</text>
<rect class="bar" x="240" y="305" width="2" height="12" rx="2"/>
<text class="count" x="746" y="315">34</text>
<text class="name" x="14" y="337">Abdel-Rahman</text>
<rect class="bar" x="240" y="327" width="2" height="12" rx="2"/>
<text class="count" x="746" y="337">30</text>
<text class="name" x="14" y="359">Manuel Riel</text>
<rect class="bar" x="240" y="349" width="2" height="12" rx="2"/>
<text class="count" x="746" y="359">28</text>
<text class="name" x="14" y="381">Nehalenniæ Oudin</text>
<rect class="bar" x="240" y="371" width="2" height="12" rx="2"/>
<text class="count" x="746" y="381">26</text>
<text class="name" x="14" y="403">anarcat</text>
<rect class="bar" x="240" y="393" width="2" height="12" rx="2"/>
<text class="count" x="746" y="403">24</text>
<text class="name" x="14" y="425">Hugo Wallenburg</text>
<rect class="bar" x="240" y="415" width="2" height="12" rx="2"/>
<text class="count" x="746" y="425">23</text>
<text class="name" x="14" y="447">Michael Hanselmann</text>
<rect class="bar" x="240" y="437" width="2" height="12" rx="2"/>
<text class="count" x="746" y="447">22</text>
<text class="name" x="14" y="469">Björn Ketelaars</text>
<rect class="bar" x="240" y="459" width="2" height="12" rx="2"/>
<text class="count" x="746" y="469">17</text>
<text class="name" x="14" y="491">SanskritFritz</text>
<rect class="bar" x="240" y="481" width="2" height="12" rx="2"/>
<text class="count" x="746" y="491">17</text>
<text class="footer" x="14" y="514">generated by scripts/fame.py using git-fame</text>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

+107 -12
View File
@@ -1,7 +1,13 @@
#!/usr/bin/env python3
"""Regenerate FAME.md, a contributor summary produced by git-fame.
"""Regenerate FAME.md and FAME.svg, a contributor summary produced by git-fame.
Usage: python scripts/fame.py [--top N]
FAME.md is the full table, FAME.svg a bar chart of the busiest contributors that
FAME.md embeds. The chart is drawn here rather than taken from git-fame's own
`--format=svg`, which renders the whole table as monospaced text - fine for a
handful of contributors, unreadable for a few hundred. It leaves out bots,
which do appear in the table.
Usage: python scripts/fame.py [--top N] [--svg-top N]
Requires `git-fame` (pip install git-fame) and a full (non-shallow) clone,
because the line counts come from `git blame` over the whole history.
@@ -17,9 +23,9 @@ later refactored away, so it is not what we rank by.
Author identities are merged via .mailmap, so fix duplicates there rather than
here.
FAME.md is left alone if only the generation date would change, so that running
this regularly (see .github/workflows/fame.yml) does not produce a diff for a
week in which the statistics did not move.
Both files are left alone if nothing but the generation date would change, so
that running this regularly (see .github/workflows/fame.yml) does not produce a
diff for a week in which the statistics did not move.
"""
import argparse
@@ -28,11 +34,18 @@ import re
import subprocess
from datetime import date
from pathlib import Path
from xml.sax.saxutils import escape
BRANCH = "master"
DATE_LINE = re.compile(r"^Generated on \d{4}-\d{2}-\d{2} ", re.MULTILINE)
# Bots keep their row in the table - they did make those commits - but they
# would take a slot away from a person in the chart, which only has room for a
# few. GitHub appends "[bot]" to the account name of a bot, e.g.
# "dependabot[bot]"; matching that is safer than guessing from the name.
BOT = re.compile(r"\[bot\]$")
# Generated and binary-ish files would drown out hand-written code. Excalidraw
# scenes are the worst offenders: they are pretty-printed JSON, so a single
# diagram of a few boxes and arrows is over 10000 lines. Image formats are
@@ -48,6 +61,8 @@ PREAMBLE = """\
{count} people have contributed to Borg, with {commits} commits in total.
Thanks to everyone who helped!
![Contributors by commit count](FAME.svg)
Generated on {date} by `scripts/fame.py`, which computes the statistics with
[git-fame](https://github.com/casperdcl/git-fame), from the `{branch}` branch
only - commits that exist solely on other branches or in unmerged pull requests
@@ -65,6 +80,39 @@ show up there, so the number understates early contributions. Generated files
"""
# Chart geometry, in user units. The viewport is given in user units plus a
# viewBox rather than in em, because em on the root <svg> resolves against the
# font size that element inherits, not against the font size of the text inside.
SVG_TOP = 20 # a chart of all 300+ contributors would be unreadable
SVG_WIDTH = 760
SVG_ROW_H = 22
SVG_PAD = 14
SVG_LABEL_W = 240 # x where the bars start, i.e. width of the name column
SVG_COUNT_W = 70 # right hand column holding the commit count
SVG_HEAD_H = 58
SVG_FOOT_H = 26
SVG_NAME_MAX = 32 # characters that fit into the name column
# Fills are set per class so that the chart can follow the reader's colour
# scheme; GitHub renders the file in an <img>, where the media query applies.
SVG_STYLE = """\
<style>
text { font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif }
.bg { fill: #ffffff }
.title { font-size: 16px; font-weight: 600; fill: #1f2328 }
.name { font-size: 12px; fill: #1f2328 }
.subtitle, .footer, .count { font-size: 11px; fill: #59636e }
.count { font-size: 12px; text-anchor: end }
.bar { fill: #4c74b9 }
@media (prefers-color-scheme: dark) {
.bg { fill: #0d1117 }
.title, .name { fill: #e6edf3 }
.subtitle, .footer, .count { fill: #9198a1 }
.bar { fill: #4493f8 }
}
</style>"""
def collect():
"""Run git-fame and return (rows, total), all contributors, most commits first."""
out = subprocess.check_output(
@@ -91,28 +139,75 @@ def render(rows, total, contributors):
return preamble + "\n".join(table) + "\n"
def shorten(name):
"""Truncate `name` to what fits into the name column of the chart."""
return name if len(name) <= SVG_NAME_MAX else name[: SVG_NAME_MAX - 1] + "…"
def render_svg(rows, contributors):
"""Return an SVG bar chart of `rows`, the busiest out of `contributors` people."""
height = SVG_HEAD_H + len(rows) * SVG_ROW_H + SVG_FOOT_H
bars = SVG_WIDTH - SVG_LABEL_W - SVG_COUNT_W - SVG_PAD
most_commits = rows[0][1]
out = [
f'<svg xmlns="http://www.w3.org/2000/svg" width="{SVG_WIDTH}" height="{height}"'
f' viewBox="0 0 {SVG_WIDTH} {height}" role="img"'
f' aria-label="Borg contributors by commit count">',
SVG_STYLE,
f'<rect class="bg" x="0" y="0" width="{SVG_WIDTH}" height="{height}" rx="6"/>',
f'<text class="title" x="{SVG_PAD}" y="26">Borg contributors</text>',
f'<text class="subtitle" x="{SVG_PAD}" y="44">'
f"top {len(rows)} of {contributors:,} by commits on the {BRANCH} branch</text>",
]
for i, (name, commits, _loc, _files) in enumerate(rows):
y = SVG_HEAD_H + i * SVG_ROW_H
baseline = y + int(SVG_ROW_H * 0.7)
# the scale is linear, but a bar should stay visible however short it is
width = max(2, round(bars * commits / most_commits))
# names are commit-controlled input, so & and < would break the XML
out.append(f'<text class="name" x="{SVG_PAD}" y="{baseline}">{escape(shorten(name))}</text>')
out.append(
f'<rect class="bar" x="{SVG_LABEL_W}" y="{y + 5}" width="{width}" height="{SVG_ROW_H - 10}" rx="2"/>'
)
out.append(f'<text class="count" x="{SVG_WIDTH - SVG_PAD}" y="{baseline}">{commits:,}</text>')
out.append(
f'<text class="footer" x="{SVG_PAD}" y="{height - 10}">generated by scripts/fame.py using git-fame</text>'
)
out.append("</svg>")
return "\n".join(out) + "\n"
def same_statistics(old, new):
"""Return True if the two FAME.md contents differ only in the generation date."""
return DATE_LINE.sub("Generated on ", old) == DATE_LINE.sub("Generated on ", new)
def update(path, new, unchanged=str.__eq__):
"""Write `new` to `path` unless `unchanged` says it is already there."""
if path.exists() and unchanged(path.read_text(encoding="utf-8"), new):
print(f"{path} is up to date")
return
path.write_text(new, encoding="utf-8")
print(f"wrote {path}")
def main():
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--top", type=int, default=0, metavar="N", help="only list the top N contributors")
parser.add_argument(
"--svg-top", type=int, default=SVG_TOP, metavar="N", help="chart the top N contributors [default: %(default)s]"
)
args = parser.parse_args()
repo = Path(subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip())
rows, total = collect()
contributors = len(rows)
people = [row for row in rows if not BOT.search(row[0])]
if args.top:
rows = rows[: args.top]
output = repo / "FAME.md"
new = render(rows, total, contributors)
if output.exists() and same_statistics(output.read_text(encoding="utf-8"), new):
print(f"{output} is up to date ({len(rows)} of {contributors} contributors)")
return
output.write_text(new, encoding="utf-8")
print(f"wrote {output} ({len(rows)} of {contributors} contributors)")
print(f"{len(rows)} of {contributors} contributors, {contributors - len(people)} of them bots")
update(repo / "FAME.md", render(rows, total, contributors), unchanged=same_statistics)
update(repo / "FAME.svg", render_svg(people[: args.svg_top], len(people)))
if __name__ == "__main__":