mirror of
https://github.com/borgbackup/borg.git
synced 2026-10-12 02:11:36 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -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
|
||||

|
||||
|
||||
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 |
|
||||
|
||||
@@ -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
@@ -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!
|
||||
|
||||

|
||||
|
||||
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__":
|
||||
|
||||
Reference in New Issue
Block a user