From 7efd43a1c01e5b5921a7e025c00190063d85d2e8 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 29 Oct 2025 02:50:35 +0000 Subject: [PATCH 001/303] Update generated files --- generated/languages.svg | 32 ++++++++++++++++---------------- generated/overview.svg | 8 ++++---- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 3909e7977d3..28a4cbaf6a2 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
- +
@@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.80% +30.73% @@ -153,25 +153,25 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.63% +18.59%
  • - -JavaScript -8.64% +Zig +8.73%
  • - -Zig -8.53% +JavaScript +8.62%
  • @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -7.79% +7.77% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -6.60% +6.58% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.58% +5.57% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.05% +2.04% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.98% +1.97% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Java -1.38% +1.37% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.34% +1.35% diff --git a/generated/overview.svg b/generated/overview.svg index b5b7dddba85..29d96e40a89 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,388 -Forks1,131 +Forks1,133 -All-time contributions4,278 +All-time contributions4,279 -Lines of code changed2,748,474 +Lines of code changed2,741,647 -Repository views (past two weeks)1,689 +Repository views (past two weeks)1,673 Repositories with contributions127 From d86ebe99a6fc88b04df48cb417020254ab60088a Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 30 Oct 2025 02:15:17 +0000 Subject: [PATCH 002/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 29d96e40a89..3e587aa4b1a 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -97,9 +97,9 @@ tr { All-time contributions4,279 -Lines of code changed2,741,647 +Lines of code changed2,748,530 -Repository views (past two weeks)1,673 +Repository views (past two weeks)1,643 Repositories with contributions127 From ec7e0b9179af3107add15ae76dde2e780fa566d9 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 31 Oct 2025 02:37:35 +0000 Subject: [PATCH 003/303] Update generated files --- generated/languages.svg | 10 +++++----- generated/overview.svg | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 28a4cbaf6a2..9a14a8e4377 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.73% +30.71% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.59% +18.58% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -8.73% +8.79% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.62% +8.61% diff --git a/generated/overview.svg b/generated/overview.svg index 3e587aa4b1a..e48c6598504 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,388 +Stars7,390 -Forks1,133 +Forks1,134 All-time contributions4,279 -Lines of code changed2,748,530 +Lines of code changed2,746,461 -Repository views (past two weeks)1,643 +Repository views (past two weeks)1,700 Repositories with contributions127 From 98596219e0f3cc3af8aa2fb29e4ddea545969584 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 1 Nov 2025 02:19:56 +0000 Subject: [PATCH 004/303] Update generated files --- generated/languages.svg | 22 +++++++++++----------- generated/overview.svg | 6 +++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 9a14a8e4377..f82bfdd577e 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.71% +30.67% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.58% +18.55% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -8.79% +8.86% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.61% +8.60% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -7.77% +7.76% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -6.58% +6.57% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.57% +5.56% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.53% +2.52% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.35% +1.34% @@ -333,7 +333,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Tree-sitter Query -0.06% +0.13% diff --git a/generated/overview.svg b/generated/overview.svg index e48c6598504..d076e13b0c0 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -95,11 +95,11 @@ tr { Forks1,134 -All-time contributions4,279 +All-time contributions4,280 -Lines of code changed2,746,461 +Lines of code changed2,745,487 -Repository views (past two weeks)1,700 +Repository views (past two weeks)1,629 Repositories with contributions127 From 61340b08e78cfa7533eca7b4f6cb33302b2543b3 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 2 Nov 2025 02:51:23 +0000 Subject: [PATCH 005/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index d076e13b0c0..82ff644da2f 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,390 +Stars7,392 Forks1,134 All-time contributions4,280 -Lines of code changed2,745,487 +Lines of code changed2,734,284 -Repository views (past two weeks)1,629 +Repository views (past two weeks)1,695 Repositories with contributions127 From 5271799abf47befe37d74c2f054aa0dda8ae6956 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 3 Nov 2025 02:15:43 +0000 Subject: [PATCH 006/303] Update generated files --- generated/languages.svg | 20 ++++++++++---------- generated/overview.svg | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index f82bfdd577e..15158f5e4b0 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.67% +30.60% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.55% +18.51% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -8.86% +9.04% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.60% +8.58% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -7.76% +7.74% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -6.57% +6.56% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.56% +5.54% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.04% +2.03% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.34% +1.36% diff --git a/generated/overview.svg b/generated/overview.svg index 82ff644da2f..d9c0f0c55e7 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,392 +Stars7,394 Forks1,134 -All-time contributions4,280 +All-time contributions4,281 -Lines of code changed2,734,284 +Lines of code changed2,748,584 -Repository views (past two weeks)1,695 +Repository views (past two weeks)1,725 Repositories with contributions127 From b7623663090b0b86317034f047c3f3796cfd4777 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 4 Nov 2025 02:57:06 +0000 Subject: [PATCH 007/303] Update generated files --- generated/languages.svg | 8 ++++---- generated/overview.svg | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 15158f5e4b0..d86fe29f0c4 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.60% +30.59% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -6.56% +6.55% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.36% +1.37% diff --git a/generated/overview.svg b/generated/overview.svg index d9c0f0c55e7..3b8e29c24a0 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,394 +Stars7,393 -Forks1,134 +Forks1,133 -All-time contributions4,281 +All-time contributions4,284 -Lines of code changed2,748,584 +Lines of code changed2,748,624 -Repository views (past two weeks)1,725 +Repository views (past two weeks)1,726 Repositories with contributions127 From 8dd54bc9b44158daefe4555fa84faf2ebc419a79 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 5 Nov 2025 03:17:30 +0000 Subject: [PATCH 008/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 3b8e29c24a0..7d1027e8ff4 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,393 +Stars7,394 Forks1,133 All-time contributions4,284 -Lines of code changed2,748,624 +Lines of code changed2,663,617 -Repository views (past two weeks)1,726 +Repository views (past two weeks)1,675 Repositories with contributions127 From 9e8fe0ad19c6931eff74072115bb0a0b9a614924 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 6 Nov 2025 02:21:01 +0000 Subject: [PATCH 009/303] Update generated files --- generated/languages.svg | 20 ++++++++++---------- generated/overview.svg | 10 +++++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index d86fe29f0c4..b8b7014c6c5 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.59% +30.58% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.51% +18.50% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.52% +2.51% @@ -234,24 +234,24 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.63% +1.64%
  • - -Java -1.37% +Makefile +1.39%
  • - -Makefile +Java 1.37%
  • diff --git a/generated/overview.svg b/generated/overview.svg index 7d1027e8ff4..d4cdf9d345a 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,394 +Stars7,398 -Forks1,133 +Forks1,134 -All-time contributions4,284 +All-time contributions4,332 -Lines of code changed2,663,617 +Lines of code changed2,750,046 -Repository views (past two weeks)1,675 +Repository views (past two weeks)1,693 Repositories with contributions127 From ebbf7651970477b6d7431998dc366f6d666d40e3 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 7 Nov 2025 02:11:56 +0000 Subject: [PATCH 010/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index d4cdf9d345a..543e5aea7f3 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,398 +Stars7,401 Forks1,134 All-time contributions4,332 -Lines of code changed2,750,046 +Lines of code changed2,753,121 -Repository views (past two weeks)1,693 +Repository views (past two weeks)1,778 Repositories with contributions127 From a2d6a3d897109e46e1f2e3b1067152edc55de547 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 8 Nov 2025 02:05:31 +0000 Subject: [PATCH 011/303] Update generated files --- generated/languages.svg | 4 ++-- generated/overview.svg | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index b8b7014c6c5..8f8381b5cdd 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -9.04% +9.05% diff --git a/generated/overview.svg b/generated/overview.svg index 543e5aea7f3..c5833e8d00c 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,401 -Forks1,134 +Forks1,135 All-time contributions4,332 Lines of code changed2,753,121 -Repository views (past two weeks)1,778 +Repository views (past two weeks)1,852 Repositories with contributions127 From f7cb028a21d8c3ee249b94214306b9cb86bbffc2 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 9 Nov 2025 02:16:57 +0000 Subject: [PATCH 012/303] Update generated files --- generated/languages.svg | 18 +++++++++--------- generated/overview.svg | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 8f8381b5cdd..fda1e0cc290 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.58% +30.56% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.50% +18.48% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -9.05% +9.06% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.58% +8.57% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -7.74% +7.73% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -6.55% +6.60% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.97% +1.96% @@ -270,7 +270,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> OpenSCAD -0.37% +0.36% diff --git a/generated/overview.svg b/generated/overview.svg index c5833e8d00c..e7ed6c01aa0 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,401 +Stars7,402 Forks1,135 -All-time contributions4,332 +All-time contributions4,339 -Lines of code changed2,753,121 +Lines of code changed2,753,448 -Repository views (past two weeks)1,852 +Repository views (past two weeks)1,825 Repositories with contributions127 From dab7728fdb342d312853244bfe891b138d6af1fb Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 10 Nov 2025 02:17:54 +0000 Subject: [PATCH 013/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index e7ed6c01aa0..d4b72e78c81 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,7 +91,7 @@ tr { -Stars7,402 +Stars7,403 Forks1,135 @@ -99,7 +99,7 @@ tr { Lines of code changed2,753,448 -Repository views (past two weeks)1,825 +Repository views (past two weeks)1,773 Repositories with contributions127 From dbc4833949f95023c46acf3f447be20c87e5f2f1 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 11 Nov 2025 02:14:00 +0000 Subject: [PATCH 014/303] Update generated files --- generated/languages.svg | 34 +++++++++++++++++----------------- generated/overview.svg | 10 +++++----- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index fda1e0cc290..89ea78544bf 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.56% +30.40% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.48% +18.38% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -9.06% +9.02% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.57% +8.53% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -7.73% +7.69% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -6.60% +6.57% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.54% +5.99% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.51% +2.50% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.03% +2.04% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.96% +1.95% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.64% +1.63% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.39% +1.38% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Java -1.37% +1.36% @@ -261,7 +261,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C++ -0.69% +0.68% @@ -342,7 +342,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Dockerfile -0.06% +0.08% @@ -351,7 +351,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Just -0.06% +0.07% diff --git a/generated/overview.svg b/generated/overview.svg index d4b72e78c81..4a0f8f1841a 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,15 +93,15 @@ tr { Stars7,403 -Forks1,135 +Forks1,136 -All-time contributions4,339 +All-time contributions4,342 -Lines of code changed2,753,448 +Lines of code changed2,753,475 -Repository views (past two weeks)1,773 +Repository views (past two weeks)1,752 -Repositories with contributions127 +Repositories with contributions128 From 9761f963502ceab57f109125f60e72d2153ddee0 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 12 Nov 2025 02:14:34 +0000 Subject: [PATCH 015/303] Update generated files --- generated/languages.svg | 6 +++--- generated/overview.svg | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 89ea78544bf..98086bec9e9 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.40% +30.39% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -6.57% +6.58% diff --git a/generated/overview.svg b/generated/overview.svg index 4a0f8f1841a..7af0097c749 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,403 +Stars7,405 Forks1,136 -All-time contributions4,342 +All-time contributions4,343 -Lines of code changed2,753,475 +Lines of code changed2,753,495 -Repository views (past two weeks)1,752 +Repository views (past two weeks)1,708 Repositories with contributions128 From a16ff7cfa96acd65deb75932451894ea59abe701 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 13 Nov 2025 02:58:49 +0000 Subject: [PATCH 016/303] Update generated files --- generated/languages.svg | 20 ++++++++++---------- generated/overview.svg | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 98086bec9e9..4f49e2e5504 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.39% +30.35% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.38% +18.35% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -9.02% +9.06% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.53% +8.51% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -7.69% +7.68% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -6.58% +6.57% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.99% +5.98% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.50% +2.49% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.04% +2.12% diff --git a/generated/overview.svg b/generated/overview.svg index 7af0097c749..70712e73bbe 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -95,11 +95,11 @@ tr { Forks1,136 -All-time contributions4,343 +All-time contributions4,344 -Lines of code changed2,753,495 +Lines of code changed2,751,511 -Repository views (past two weeks)1,708 +Repository views (past two weeks)1,707 Repositories with contributions128 From e49aebd56e555e04d231520222936afbb50a9376 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 14 Nov 2025 02:19:52 +0000 Subject: [PATCH 017/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 70712e73bbe..97892466281 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,405 +Stars7,404 Forks1,136 All-time contributions4,344 -Lines of code changed2,751,511 +Lines of code changed2,753,579 -Repository views (past two weeks)1,707 +Repository views (past two weeks)1,658 Repositories with contributions128 From 73f11c34167d8809f65a7d757fb065c37cb480d8 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 15 Nov 2025 02:45:54 +0000 Subject: [PATCH 018/303] Update generated files --- generated/languages.svg | 2 +- generated/overview.svg | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 4f49e2e5504..8ba2b27304b 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    diff --git a/generated/overview.svg b/generated/overview.svg index 97892466281..14c5198c909 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,404 -Forks1,136 +Forks1,137 -All-time contributions4,344 +All-time contributions4,348 -Lines of code changed2,753,579 +Lines of code changed2,735,822 -Repository views (past two weeks)1,658 +Repository views (past two weeks)1,703 Repositories with contributions128 From 6c7d9a5434a7afd78682ac1cae4df63b183d7a85 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 16 Nov 2025 02:23:58 +0000 Subject: [PATCH 019/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 14c5198c909..e92200a44a3 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -97,9 +97,9 @@ tr { All-time contributions4,348 -Lines of code changed2,735,822 +Lines of code changed2,753,587 -Repository views (past two weeks)1,703 +Repository views (past two weeks)1,683 Repositories with contributions128 From 7972659efe634ed08a82c85e0e33680f51866250 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 17 Nov 2025 02:15:09 +0000 Subject: [PATCH 020/303] Update generated files --- generated/overview.svg | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index e92200a44a3..53778125625 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,404 +Stars7,405 -Forks1,137 +Forks1,139 -All-time contributions4,348 +All-time contributions4,350 -Lines of code changed2,753,587 +Lines of code changed2,753,593 -Repository views (past two weeks)1,683 +Repository views (past two weeks)1,686 Repositories with contributions128 From 3b6240c3187362e62589e9faff3ef67abd6cd302 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 18 Nov 2025 02:25:54 +0000 Subject: [PATCH 021/303] Update generated files --- generated/languages.svg | 4 ++-- generated/overview.svg | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 8ba2b27304b..c6c1bf5130a 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -6.57% +6.58% diff --git a/generated/overview.svg b/generated/overview.svg index 53778125625..6998c855d63 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,405 -Forks1,139 +Forks1,138 -All-time contributions4,350 +All-time contributions4,353 -Lines of code changed2,753,593 +Lines of code changed2,753,614 -Repository views (past two weeks)1,686 +Repository views (past two weeks)1,707 Repositories with contributions128 From 7ba8b6911596cf610a419a7d7b46007b75f16cc0 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 19 Nov 2025 02:14:34 +0000 Subject: [PATCH 022/303] Update generated files --- generated/languages.svg | 14 +++++++------- generated/overview.svg | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index c6c1bf5130a..cb4c4557eda 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.35% +30.31% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.35% +18.33% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -9.06% +9.05% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.51% +8.59% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -7.68% +7.67% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -6.58% +6.60% diff --git a/generated/overview.svg b/generated/overview.svg index 6998c855d63..921d3df9162 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,405 +Stars7,407 -Forks1,138 +Forks1,139 -All-time contributions4,353 +All-time contributions4,355 -Lines of code changed2,753,614 +Lines of code changed2,753,742 -Repository views (past two weeks)1,707 +Repository views (past two weeks)1,699 Repositories with contributions128 From 192edc32e37d978a0ac17288c790ce8dd99855fa Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 20 Nov 2025 02:12:16 +0000 Subject: [PATCH 023/303] Update generated files --- generated/languages.svg | 4 ++-- generated/overview.svg | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index cb4c4557eda..bf750312164 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -6.60% +6.61% diff --git a/generated/overview.svg b/generated/overview.svg index 921d3df9162..9358337376c 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,407 +Stars7,408 Forks1,139 -All-time contributions4,355 +All-time contributions4,356 -Lines of code changed2,753,742 +Lines of code changed2,753,753 -Repository views (past two weeks)1,699 +Repository views (past two weeks)1,630 Repositories with contributions128 From dc1d793659941260b4c346bc8fa04e3101d7abd8 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 21 Nov 2025 02:25:21 +0000 Subject: [PATCH 024/303] Update generated files --- generated/languages.svg | 6 +++--- generated/overview.svg | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index bf750312164..ec7cc9e3def 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -6.61% +6.62% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.98% +5.97% diff --git a/generated/overview.svg b/generated/overview.svg index 9358337376c..e469b7c37f6 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,408 +Stars7,409 Forks1,139 -All-time contributions4,356 +All-time contributions4,358 -Lines of code changed2,753,753 +Lines of code changed2,756,499 -Repository views (past two weeks)1,630 +Repository views (past two weeks)1,588 Repositories with contributions128 From bc432df7117b6f69afa0c37f823a96d9e64d7352 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 22 Nov 2025 02:18:28 +0000 Subject: [PATCH 025/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index e469b7c37f6..aec20a13e74 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,409 -Forks1,139 +Forks1,138 All-time contributions4,358 -Lines of code changed2,756,499 +Lines of code changed2,756,934 -Repository views (past two weeks)1,588 +Repository views (past two weeks)1,499 Repositories with contributions128 From 4a6acc948979bf5375b2e8d06306e0b23627afde Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 23 Nov 2025 02:31:14 +0000 Subject: [PATCH 026/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index aec20a13e74..bb23063a38f 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,7 +91,7 @@ tr { -Stars7,409 +Stars7,407 Forks1,138 @@ -99,7 +99,7 @@ tr { Lines of code changed2,756,934 -Repository views (past two weeks)1,499 +Repository views (past two weeks)1,542 Repositories with contributions128 From b51c41163be159a96046bcaa3ee5fd7e3a6c8597 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 24 Nov 2025 02:22:06 +0000 Subject: [PATCH 027/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index bb23063a38f..e7923af64cd 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,407 +Stars7,408 -Forks1,138 +Forks1,139 All-time contributions4,358 Lines of code changed2,756,934 -Repository views (past two weeks)1,542 +Repository views (past two weeks)1,557 Repositories with contributions128 From 82229fd44b643b512384c0e5ac33cc1dfed8444f Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 25 Nov 2025 02:23:29 +0000 Subject: [PATCH 028/303] Update generated files --- generated/languages.svg | 30 +++++++++++++++--------------- generated/overview.svg | 4 ++-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index ec7cc9e3def..274d1c628f7 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.31% +30.18% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.33% +18.25% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -9.05% +9.39% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.59% +8.56% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -7.67% +7.63% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -6.62% +6.59% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.49% +2.48% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.12% +2.11% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.95% +1.94% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.63% +1.62% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.38% +1.37% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Java -1.36% +1.35% @@ -279,7 +279,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TypeScript -0.36% +0.35% @@ -288,7 +288,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Vim Script -0.20% +0.19% diff --git a/generated/overview.svg b/generated/overview.svg index e7923af64cd..f62ffa017bb 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,7 +91,7 @@ tr { -Stars7,408 +Stars7,407 Forks1,139 @@ -99,7 +99,7 @@ tr { Lines of code changed2,756,934 -Repository views (past two weeks)1,557 +Repository views (past two weeks)1,587 Repositories with contributions128 From 75c4029c140c06665c2035816cbc3c694c157939 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 26 Nov 2025 02:38:29 +0000 Subject: [PATCH 029/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index f62ffa017bb..42ffff44249 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,407 +Stars7,408 Forks1,139 All-time contributions4,358 -Lines of code changed2,756,934 +Lines of code changed2,751,469 -Repository views (past two weeks)1,587 +Repository views (past two weeks)1,595 Repositories with contributions128 From 438d46009d16c9a51eb905efc34b2aa144f28482 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 27 Nov 2025 02:31:32 +0000 Subject: [PATCH 030/303] Update generated files --- generated/languages.svg | 10 +++++----- generated/overview.svg | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 274d1c628f7..2e8f2408ef9 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.18% +30.17% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.25% +18.24% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -9.39% +9.44% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.56% +8.55% diff --git a/generated/overview.svg b/generated/overview.svg index 42ffff44249..04c95cdfd40 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,408 +Stars7,412 -Forks1,139 +Forks1,140 All-time contributions4,358 -Lines of code changed2,751,469 +Lines of code changed2,756,934 -Repository views (past two weeks)1,595 +Repository views (past two weeks)1,614 Repositories with contributions128 From e8008b6b7746360e0ab88e153e896d398d84f273 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 28 Nov 2025 02:10:11 +0000 Subject: [PATCH 031/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 04c95cdfd40..d2e99c74728 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,7 +91,7 @@ tr { -Stars7,412 +Stars7,414 Forks1,140 @@ -99,7 +99,7 @@ tr { Lines of code changed2,756,934 -Repository views (past two weeks)1,614 +Repository views (past two weeks)1,597 Repositories with contributions128 From 2f04c8a189c2580864a7181c237b510e331e8071 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 29 Nov 2025 02:26:00 +0000 Subject: [PATCH 032/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index d2e99c74728..4229110b2a6 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,7 +91,7 @@ tr { -Stars7,414 +Stars7,418 Forks1,140 @@ -99,7 +99,7 @@ tr { Lines of code changed2,756,934 -Repository views (past two weeks)1,597 +Repository views (past two weeks)1,570 Repositories with contributions128 From 6b2f59c328f4ac5eafcaa8e5a0cfa07eaf30f0a1 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 30 Nov 2025 02:50:48 +0000 Subject: [PATCH 033/303] Update generated files --- generated/languages.svg | 4 ++-- generated/overview.svg | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 2e8f2408ef9..4b1eee7fe86 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -6.59% +6.58% diff --git a/generated/overview.svg b/generated/overview.svg index 4229110b2a6..7dc6e5016a5 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,418 +Stars7,420 -Forks1,140 +Forks1,141 -All-time contributions4,358 +All-time contributions4,360 -Lines of code changed2,756,934 +Lines of code changed2,757,219 -Repository views (past two weeks)1,570 +Repository views (past two weeks)1,629 Repositories with contributions128 From 326c23b1a63d9d10e4b7212eadf4c31a9ed7db41 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 1 Dec 2025 02:38:50 +0000 Subject: [PATCH 034/303] Update generated files --- generated/languages.svg | 26 +++++++++++++------------- generated/overview.svg | 10 +++++----- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 4b1eee7fe86..7a36e399ebf 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.17% +30.08% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.24% +18.19% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -9.44% +9.69% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.55% +8.53% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -7.63% +7.61% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -6.58% +6.57% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.97% +5.95% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.48% +2.47% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.11% +2.10% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.94% +1.93% @@ -261,7 +261,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C++ -0.68% +0.67% @@ -333,7 +333,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Tree-sitter Query -0.13% +0.12% diff --git a/generated/overview.svg b/generated/overview.svg index 7dc6e5016a5..94613b95670 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,420 +Stars7,422 -Forks1,141 +Forks1,142 -All-time contributions4,360 +All-time contributions4,363 -Lines of code changed2,757,219 +Lines of code changed2,757,238 -Repository views (past two weeks)1,629 +Repository views (past two weeks)1,626 Repositories with contributions128 From 58ca5c1ed538ddcc725008bb4992ac44299998a7 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 1 Dec 2025 16:55:13 +0000 Subject: [PATCH 035/303] Update generated files --- generated/languages.svg | 92 ++++++++++++++++++++--------------------- generated/overview.svg | 10 ++--- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 7a36e399ebf..a5fd7ad94b4 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.08% +31.01% @@ -153,25 +153,25 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.19% +18.75%
  • - -Zig -9.69% +JavaScript +8.79%
  • - -JavaScript -8.53% +Zig +8.09%
  • @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -7.61% +7.06% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -6.57% +6.77% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.95% +6.14% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.47% +2.55% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.10% +2.17% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.93% +1.99% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.62% +1.67% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.37% +1.41% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Java -1.35% +1.39% @@ -261,7 +261,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C++ -0.67% +0.70% @@ -270,29 +270,20 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> OpenSCAD -0.36% +0.37%
  • - -TypeScript -0.35% -
  • - - -
  • Vim Script -0.19% +0.20%
  • -
  • +
  • @@ -301,7 +292,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z">
  • -
  • +
  • @@ -310,34 +301,34 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z">
  • -
  • - + -Nix -0.16% +PHP +0.14%
  • -
  • - + -PHP -0.14% +TypeScript +0.11%
  • -
  • - + -Tree-sitter Query -0.12% +Nix +0.08%
  • -
  • +
  • @@ -346,7 +337,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z">
  • -
  • +
  • @@ -355,6 +346,15 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z">
  • +
  • + +Tree-sitter Query +0.06% +
  • + +
  • -
    Stars7,422
    +
    Stars7,423
    Forks1,142
    -
    All-time contributions4,363
    +
    All-time contributions4,365
    -
    Lines of code changed2,757,238
    +
    Lines of code changed2,761,389
    -
    Repository views (past two weeks)1,626
    +
    Repository views (past two weeks)1,663
    -
    Repositories with contributions128
    +
    Repositories with contributions125
    From 3f2ed0adcf481a1e2ce4ddd5856e4a70c5e57e0d Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 2 Dec 2025 02:15:16 +0000 Subject: [PATCH 036/303] Update generated files --- generated/overview.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generated/overview.svg b/generated/overview.svg index 33311859f6e..0abbcb05267 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -99,7 +99,7 @@ tr {
    Lines of code changed2,761,389
    -
    Repository views (past two weeks)1,663
    +
    Repository views (past two weeks)1,673
    Repositories with contributions125
    From 1f9c479161f19b2ec1c54ca5e01b2f3cb8dc7716 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 3 Dec 2025 02:16:15 +0000 Subject: [PATCH 037/303] Update generated files --- generated/languages.svg | 12 ++++++------ generated/overview.svg | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index a5fd7ad94b4..aff8c4245b4 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -31.01% +30.99%
  • @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.75% +18.74% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -8.09% +8.15% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -6.14% +6.13% @@ -261,7 +261,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C++ -0.70% +0.69% diff --git a/generated/overview.svg b/generated/overview.svg index 0abbcb05267..e3d31805e5f 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,423 +Stars7,427 Forks1,142 -All-time contributions4,365 +All-time contributions4,367 -Lines of code changed2,761,389 +Lines of code changed2,760,814 -Repository views (past two weeks)1,673 +Repository views (past two weeks)1,762 Repositories with contributions125 From 3fecd11130259defe2861faba1e43609513f3c24 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 4 Dec 2025 02:19:48 +0000 Subject: [PATCH 038/303] Update generated files --- generated/languages.svg | 34 +++++++++++++++++----------------- generated/overview.svg | 12 ++++++------ 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index aff8c4245b4..257e5a3ccb6 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.99% +30.70% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.74% +18.57% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.79% +8.71% @@ -176,20 +176,20 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z">
  • - -Standard ML -7.06% +Svelte +7.44%
  • - -Svelte -6.77% +Standard ML +6.99%
  • @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -6.13% +6.08% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.55% +2.52% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.17% +2.15% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.99% +1.97% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.67% +1.71% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.41% +1.45% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Java -1.39% +1.37% diff --git a/generated/overview.svg b/generated/overview.svg index e3d31805e5f..5ff23356eeb 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,17 +91,17 @@ tr { -Stars7,427 +Stars7,428 -Forks1,142 +Forks1,144 -All-time contributions4,367 +All-time contributions4,392 -Lines of code changed2,760,814 +Lines of code changed2,765,764 -Repository views (past two weeks)1,762 +Repository views (past two weeks)1,878 -Repositories with contributions125 +Repositories with contributions126 From cbba0ca2f60a0a6105bd0bb8b1cd0b1b3d77f91f Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 5 Dec 2025 02:16:16 +0000 Subject: [PATCH 039/303] Update generated files --- generated/languages.svg | 58 ++++++++++++++++++++--------------------- generated/overview.svg | 10 +++---- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 257e5a3ccb6..5b1e9acd989 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.70% +30.46% @@ -153,25 +153,25 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.57% +18.42%
  • - -JavaScript -8.71% +Zig +8.78%
  • - -Zig -8.15% +JavaScript +8.64%
  • @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.44% +7.38% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.99% +6.94% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -6.08% +6.04% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.52% +2.50% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.15% +2.13% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.97% +1.96% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.71% +1.69% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.45% +1.44% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Java -1.37% +1.36% @@ -261,7 +261,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C++ -0.69% +0.68% @@ -270,7 +270,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> OpenSCAD -0.37% +0.36% @@ -302,29 +302,29 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z">
  • - -PHP -0.14% +Nix +0.16%
  • - -TypeScript -0.11% +PHP +0.14%
  • - -Nix -0.08% +TypeScript +0.11%
  • diff --git a/generated/overview.svg b/generated/overview.svg index 5ff23356eeb..644645f792c 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,17 +91,17 @@ tr { -Stars7,428 +Stars7,435 Forks1,144 -All-time contributions4,392 +All-time contributions4,394 -Lines of code changed2,765,764 +Lines of code changed2,766,010 -Repository views (past two weeks)1,878 +Repository views (past two weeks)1,908 -Repositories with contributions126 +Repositories with contributions127 From b170f7165744573b247bf8881f4635635f6f68e5 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 6 Dec 2025 02:15:32 +0000 Subject: [PATCH 040/303] Update generated files --- generated/languages.svg | 20 ++++++++++---------- generated/overview.svg | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 5b1e9acd989..f01fe492886 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.46% +30.40% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.42% +18.38% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -8.78% +8.97% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.64% +8.63% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.38% +7.37% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.94% +6.92% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -6.04% +6.02% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.13% +2.12% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.96% +1.95% diff --git a/generated/overview.svg b/generated/overview.svg index 644645f792c..6c29edd54f1 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,435 +Stars7,434 Forks1,144 -All-time contributions4,394 +All-time contributions4,396 -Lines of code changed2,766,010 +Lines of code changed2,766,879 -Repository views (past two weeks)1,908 +Repository views (past two weeks)1,969 Repositories with contributions127 From 7926f7f79d51931fae30f9343a26de2bd90e2dea Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 7 Dec 2025 03:09:50 +0000 Subject: [PATCH 041/303] Update generated files --- generated/languages.svg | 20 ++++++++++---------- generated/overview.svg | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index f01fe492886..a754cf88fbb 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.40% +30.34% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.38% +18.35% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -8.97% +9.08% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.63% +8.67% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.37% +7.31% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.92% +6.91% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -6.02% +6.04% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.50% +2.49% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.44% +1.43% diff --git a/generated/overview.svg b/generated/overview.svg index 6c29edd54f1..5b661827c94 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,434 +Stars7,435 Forks1,144 -All-time contributions4,396 +All-time contributions4,400 -Lines of code changed2,766,879 +Lines of code changed2,763,351 -Repository views (past two weeks)1,969 +Repository views (past two weeks)2,020 Repositories with contributions127 From ad38de629529f4db212986cc4c6c938380182f5a Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 8 Dec 2025 03:36:01 +0000 Subject: [PATCH 042/303] Update generated files --- generated/languages.svg | 24 ++++++++++++------------ generated/overview.svg | 8 ++++---- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index a754cf88fbb..084daaae023 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.34% +30.27% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.35% +18.31% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -9.08% +9.30% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.67% +8.65% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.31% +7.30% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.91% +6.89% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -6.04% +6.03% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.95% +1.94% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.69% +1.68% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Java -1.36% +1.35% @@ -279,7 +279,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Vim Script -0.20% +0.19% diff --git a/generated/overview.svg b/generated/overview.svg index 5b661827c94..adaefd42123 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,435 +Stars7,437 Forks1,144 -All-time contributions4,400 +All-time contributions4,404 -Lines of code changed2,763,351 +Lines of code changed2,662,791 -Repository views (past two weeks)2,020 +Repository views (past two weeks)2,097 Repositories with contributions127 From 1883b9a545a1279ebe3193ababd3c958c1cc697a Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 9 Dec 2025 02:33:06 +0000 Subject: [PATCH 043/303] Update generated files --- generated/languages.svg | 22 +++++++++++----------- generated/overview.svg | 8 ++++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 084daaae023..426f336e22f 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.27% +30.12% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.31% +18.21% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -9.30% +9.76% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.65% +8.60% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.30% +7.26% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.89% +6.86% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -6.03% +6.00% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.49% +2.48% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.12% +2.10% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.43% +1.42% diff --git a/generated/overview.svg b/generated/overview.svg index adaefd42123..59ef7fca974 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,437 +Stars7,438 Forks1,144 -All-time contributions4,404 +All-time contributions4,405 -Lines of code changed2,662,791 +Lines of code changed2,756,750 -Repository views (past two weeks)2,097 +Repository views (past two weeks)2,158 Repositories with contributions127 From 99b7370f2b31387cdf48402d31e7a00d348b6743 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 10 Dec 2025 02:54:38 +0000 Subject: [PATCH 044/303] Update generated files --- generated/languages.svg | 28 ++++++++++++++-------------- generated/overview.svg | 8 ++++---- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 426f336e22f..071fc80191f 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.12% +30.02% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.21% +18.16% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -9.76% +10.05% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.60% +8.57% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.26% +7.24% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.86% +6.84% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -6.00% +5.98% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.48% +2.47% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.94% +1.93% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.68% +1.67% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Java -1.35% +1.34% @@ -261,7 +261,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C++ -0.68% +0.67% @@ -324,7 +324,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TypeScript -0.11% +0.10% diff --git a/generated/overview.svg b/generated/overview.svg index 59ef7fca974..72b4f06e3bc 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,438 +Stars7,439 Forks1,144 -All-time contributions4,405 +All-time contributions4,406 -Lines of code changed2,756,750 +Lines of code changed2,748,637 -Repository views (past two weeks)2,158 +Repository views (past two weeks)2,196 Repositories with contributions127 From fa79d755d8d018d168d424927cc1cbbb673fc066 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 11 Dec 2025 03:10:44 +0000 Subject: [PATCH 045/303] Update generated files --- generated/languages.svg | 20 ++++++++++---------- generated/overview.svg | 10 +++++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 071fc80191f..c0063f64d7b 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -30.02% +29.97% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.16% +18.13% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.05% +10.20% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.57% +8.56% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.24% +7.22% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.84% +6.83% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.98% +5.97% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.47% +2.46% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.10% +2.09% diff --git a/generated/overview.svg b/generated/overview.svg index 72b4f06e3bc..6033242505e 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,439 +Stars7,440 -Forks1,144 +Forks1,145 -All-time contributions4,406 +All-time contributions4,410 -Lines of code changed2,748,637 +Lines of code changed2,736,573 -Repository views (past two weeks)2,196 +Repository views (past two weeks)2,200 Repositories with contributions127 From df0e988d21e7172c9f16eb386ee4ac6ced0b8430 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 12 Dec 2025 02:36:50 +0000 Subject: [PATCH 046/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 6033242505e..fa56363abfb 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,440 +Stars7,443 -Forks1,145 +Forks1,147 All-time contributions4,410 -Lines of code changed2,736,573 +Lines of code changed2,770,176 -Repository views (past two weeks)2,200 +Repository views (past two weeks)2,234 Repositories with contributions127 From 3f1e0c3660ac9bf98ad0c8fc8016d1096b3c7e30 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 13 Dec 2025 02:23:05 +0000 Subject: [PATCH 047/303] Update generated files --- generated/languages.svg | 24 ++++++++++++------------ generated/overview.svg | 8 ++++---- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index c0063f64d7b..08cf4eef04c 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.97% +29.93% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.13% +18.10% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.20% +10.32% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.56% +8.55% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.22% +7.21% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.83% +6.82% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.97% +5.96% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.93% +1.92% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.67% +1.66% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.42% +1.41% @@ -288,7 +288,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Assembly -0.18% +0.17% diff --git a/generated/overview.svg b/generated/overview.svg index fa56363abfb..60e4f5a5c12 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,443 +Stars7,446 Forks1,147 -All-time contributions4,410 +All-time contributions4,412 -Lines of code changed2,770,176 +Lines of code changed2,770,905 -Repository views (past two weeks)2,234 +Repository views (past two weeks)2,220 Repositories with contributions127 From 7a6ac1746d7356ce15e9305dd6f0947dc3bd2dab Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 14 Dec 2025 02:48:10 +0000 Subject: [PATCH 048/303] Update generated files --- generated/languages.svg | 12 ++++++------ generated/overview.svg | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 08cf4eef04c..f79760a8699 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.93% +29.91% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.10% +18.09% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.32% +10.38% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.55% +8.54% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.82% +6.81% diff --git a/generated/overview.svg b/generated/overview.svg index 60e4f5a5c12..b26ce4b9473 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,446 +Stars7,450 Forks1,147 -All-time contributions4,412 +All-time contributions4,413 -Lines of code changed2,770,905 +Lines of code changed2,772,022 -Repository views (past two weeks)2,220 +Repository views (past two weeks)2,100 Repositories with contributions127 From 496d5458e9297993485c863e3836059540759bc1 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 15 Dec 2025 02:42:11 +0000 Subject: [PATCH 049/303] Update generated files --- generated/languages.svg | 30 +++++++++++++++--------------- generated/overview.svg | 10 +++++----- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index f79760a8699..55011a619cd 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.91% +29.74% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -18.09% +17.99% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.38% +10.47% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.54% +8.81% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.21% +7.22% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.81% +6.77% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.96% +5.92% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.46% +2.44% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.09% +2.08% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.92% +1.91% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.66% +1.68% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.41% +1.43% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Java -1.34% +1.33% @@ -270,7 +270,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> OpenSCAD -0.36% +0.35% diff --git a/generated/overview.svg b/generated/overview.svg index b26ce4b9473..836802f9d32 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,17 +91,17 @@ tr { -Stars7,450 +Stars7,452 Forks1,147 -All-time contributions4,413 +All-time contributions4,422 -Lines of code changed2,772,022 +Lines of code changed2,774,118 -Repository views (past two weeks)2,100 +Repository views (past two weeks)2,074 -Repositories with contributions127 +Repositories with contributions128 From d97084bd6a5c3ebdba588916e2bb851b7c1483e0 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 16 Dec 2025 02:19:07 +0000 Subject: [PATCH 050/303] Update generated files --- generated/languages.svg | 10 +++++----- generated/overview.svg | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 55011a619cd..9bcafc24dce 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.74% +29.71% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.99% +17.97% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.47% +10.46% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.81% +8.88% diff --git a/generated/overview.svg b/generated/overview.svg index 836802f9d32..640e34e3c1c 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,452 +Stars7,455 Forks1,147 -All-time contributions4,422 +All-time contributions4,427 -Lines of code changed2,774,118 +Lines of code changed2,774,327 -Repository views (past two weeks)2,074 +Repository views (past two weeks)2,064 Repositories with contributions128 From 33d42f17dd7af8b46506222a2d86b029d66d7f54 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 17 Dec 2025 02:14:01 +0000 Subject: [PATCH 051/303] Update generated files --- generated/languages.svg | 4 ++-- generated/overview.svg | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 9bcafc24dce..7ace4a30655 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.88% +8.89% diff --git a/generated/overview.svg b/generated/overview.svg index 640e34e3c1c..027b27c7b5d 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -95,11 +95,11 @@ tr { Forks1,147 -All-time contributions4,427 +All-time contributions4,428 -Lines of code changed2,774,327 +Lines of code changed2,774,341 -Repository views (past two weeks)2,064 +Repository views (past two weeks)2,011 Repositories with contributions128 From 17a49a22d87f9d3a3a1c7aa537231b1a7cdb4e74 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 18 Dec 2025 02:28:12 +0000 Subject: [PATCH 052/303] Update generated files --- generated/languages.svg | 4 ++-- generated/overview.svg | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 7ace4a30655..a5ea4ea2810 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.22% +7.23% diff --git a/generated/overview.svg b/generated/overview.svg index 027b27c7b5d..1a5594ccfbc 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,455 +Stars7,454 Forks1,147 -All-time contributions4,428 +All-time contributions4,429 -Lines of code changed2,774,341 +Lines of code changed2,774,386 -Repository views (past two weeks)2,011 +Repository views (past two weeks)1,918 Repositories with contributions128 From de59d384dedc00d192971e8904d64c30d44fed2b Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 19 Dec 2025 02:21:59 +0000 Subject: [PATCH 053/303] Update generated files --- generated/languages.svg | 18 +++++++++--------- generated/overview.svg | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index a5ea4ea2810..8f584927be5 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.71% +29.69% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.97% +17.95% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.46% +10.45% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.89% +8.94% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.23% +7.24% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.77% +6.76% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.92% +5.91% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.08% +2.07% diff --git a/generated/overview.svg b/generated/overview.svg index 1a5594ccfbc..c794e2c2006 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,454 +Stars7,456 Forks1,147 -All-time contributions4,429 +All-time contributions4,431 -Lines of code changed2,774,386 +Lines of code changed2,772,045 -Repository views (past two weeks)1,918 +Repository views (past two weeks)1,881 Repositories with contributions128 From d9254ea71ea2f2ce637e12bd9f4971421b7c2303 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 20 Dec 2025 02:41:12 +0000 Subject: [PATCH 054/303] Update generated files --- generated/languages.svg | 20 ++++++++++---------- generated/overview.svg | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 8f584927be5..01ff930d9a0 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.69% +29.63% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.95% +17.92% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.45% +10.43% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -8.94% +9.07% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.24% +7.25% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.76% +6.75% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.91% +5.90% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.91% +1.90% @@ -261,7 +261,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C++ -0.67% +0.66% diff --git a/generated/overview.svg b/generated/overview.svg index c794e2c2006..e2117b23a36 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -95,11 +95,11 @@ tr { Forks1,147 -All-time contributions4,431 +All-time contributions4,436 -Lines of code changed2,772,045 +Lines of code changed2,773,484 -Repository views (past two weeks)1,881 +Repository views (past two weeks)1,847 Repositories with contributions128 From 6dde23e8ef83a8c102049abfb4122fae2bf1dfc4 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 21 Dec 2025 02:34:56 +0000 Subject: [PATCH 055/303] Update generated files --- generated/languages.svg | 20 ++++++++++---------- generated/overview.svg | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 01ff930d9a0..5f9594bb9e6 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.63% +29.58% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.92% +17.89% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.43% +10.42% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.07% +9.16% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.25% +7.30% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.75% +6.74% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.90% +5.89% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.44% +2.43% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Java -1.33% +1.32% diff --git a/generated/overview.svg b/generated/overview.svg index e2117b23a36..04b0fa2682e 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,456 -Forks1,147 +Forks1,149 -All-time contributions4,436 +All-time contributions4,445 -Lines of code changed2,773,484 +Lines of code changed2,775,323 -Repository views (past two weeks)1,847 +Repository views (past two weeks)1,796 Repositories with contributions128 From efcdb3716480de5b3fee5a3f2002a6246693209e Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 22 Dec 2025 04:04:15 +0000 Subject: [PATCH 056/303] Update generated files --- generated/languages.svg | 26 +++++++++++++------------- generated/overview.svg | 6 +++--- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 5f9594bb9e6..40398c39f08 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.58% +29.49% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.89% +17.83% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.42% +10.38% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.16% +9.33% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.30% +7.39% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.74% +6.72% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.89% +5.87% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.43% +2.42% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.07% +2.06% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.90% +1.89% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.68% +1.69% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.43% +1.42% diff --git a/generated/overview.svg b/generated/overview.svg index 04b0fa2682e..6dcd84ee200 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -95,11 +95,11 @@ tr { Forks1,149 -All-time contributions4,445 +All-time contributions4,477 -Lines of code changed2,775,323 +Lines of code changed2,672,970 -Repository views (past two weeks)1,796 +Repository views (past two weeks)1,719 Repositories with contributions128 From 12053f64dd10afad76cf407bbff2bd9bb27e7d81 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 23 Dec 2025 02:23:45 +0000 Subject: [PATCH 057/303] Update generated files --- generated/languages.svg | 16 ++++++++-------- generated/overview.svg | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 40398c39f08..f622042acbc 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.49% +29.45% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.83% +17.81% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.38% +10.37% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.33% +9.41% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.39% +7.40% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.72% +6.71% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.69% +1.68% diff --git a/generated/overview.svg b/generated/overview.svg index 6dcd84ee200..ab846de97be 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,456 +Stars7,457 Forks1,149 -All-time contributions4,477 +All-time contributions4,494 -Lines of code changed2,672,970 +Lines of code changed2,776,580 -Repository views (past two weeks)1,719 +Repository views (past two weeks)1,639 Repositories with contributions128 From eac80b6c2606408502e0f56a45f1774bcbca8c17 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 24 Dec 2025 02:47:37 +0000 Subject: [PATCH 058/303] Update generated files --- generated/languages.svg | 4 ++-- generated/overview.svg | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index f622042acbc..2c73fec3cda 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.40% +7.41% diff --git a/generated/overview.svg b/generated/overview.svg index ab846de97be..977764838c9 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -95,11 +95,11 @@ tr { Forks1,149 -All-time contributions4,494 +All-time contributions4,499 -Lines of code changed2,776,580 +Lines of code changed2,776,635 -Repository views (past two weeks)1,639 +Repository views (past two weeks)1,622 Repositories with contributions128 From d94f7b1a88bda2545ae41c0c5769bfca00ee724a Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 25 Dec 2025 02:23:29 +0000 Subject: [PATCH 059/303] Update generated files --- generated/languages.svg | 16 ++++++++-------- generated/overview.svg | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 2c73fec3cda..b272d192a5b 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.45% +29.44% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.81% +17.80% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.37% +10.36% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.41% +9.45% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.71% +6.70% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.87% +5.86% @@ -297,7 +297,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> GDB -0.17% +0.16% diff --git a/generated/overview.svg b/generated/overview.svg index 977764838c9..02b4ce626a3 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,457 +Stars7,458 -Forks1,149 +Forks1,148 -All-time contributions4,499 +All-time contributions4,510 -Lines of code changed2,776,635 +Lines of code changed2,776,752 -Repository views (past two weeks)1,622 +Repository views (past two weeks)1,568 Repositories with contributions128 From 41b8c719072aa038cb210ba8eca51339f759baae Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 26 Dec 2025 02:21:01 +0000 Subject: [PATCH 060/303] Update generated files --- generated/languages.svg | 6 +++--- generated/overview.svg | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index b272d192a5b..c95c8722ade 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.44% +29.43% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.45% +9.46% diff --git a/generated/overview.svg b/generated/overview.svg index 02b4ce626a3..4bd30b80aa5 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,458 +Stars7,460 -Forks1,148 +Forks1,150 -All-time contributions4,510 +All-time contributions4,513 -Lines of code changed2,776,752 +Lines of code changed2,776,794 -Repository views (past two weeks)1,568 +Repository views (past two weeks)1,521 Repositories with contributions128 From 800abf096b878eabd9ed49b1a3e941fa16f96061 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 27 Dec 2025 02:55:09 +0000 Subject: [PATCH 061/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 4bd30b80aa5..c0825bbc18f 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,460 +Stars7,461 -Forks1,150 +Forks1,151 All-time contributions4,513 -Lines of code changed2,776,794 +Lines of code changed2,775,099 -Repository views (past two weeks)1,521 +Repository views (past two weeks)1,530 Repositories with contributions128 From 2c03e138a57f6af8d6dd7d93bc1daad84712ce08 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 28 Dec 2025 03:39:23 +0000 Subject: [PATCH 062/303] Update generated files --- generated/languages.svg | 12 ++++++------ generated/overview.svg | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index c95c8722ade..1ea87c544e5 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.43% +29.41% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.80% +17.79% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.36% +10.35% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.46% +9.45% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.41% +7.48% diff --git a/generated/overview.svg b/generated/overview.svg index c0825bbc18f..184a05a5c27 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -95,11 +95,11 @@ tr { Forks1,151 -All-time contributions4,513 +All-time contributions4,520 -Lines of code changed2,775,099 +Lines of code changed2,770,117 -Repository views (past two weeks)1,530 +Repository views (past two weeks)1,619 Repositories with contributions128 From 661c328cba555671594b325c61e054f433573e7d Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 29 Dec 2025 02:45:35 +0000 Subject: [PATCH 063/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 184a05a5c27..efeb4fb9aa8 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,461 +Stars7,460 Forks1,151 All-time contributions4,520 -Lines of code changed2,770,117 +Lines of code changed2,776,947 -Repository views (past two weeks)1,619 +Repository views (past two weeks)1,632 Repositories with contributions128 From 225a055ca7bb8d1f8e59ba16765206edd02d39e7 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 30 Dec 2025 02:25:48 +0000 Subject: [PATCH 064/303] Update generated files --- generated/languages.svg | 4 ++-- generated/overview.svg | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 1ea87c544e5..de8aa3a4bee 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.35% +10.36% diff --git a/generated/overview.svg b/generated/overview.svg index efeb4fb9aa8..f1ab2aec752 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,460 -Forks1,151 +Forks1,152 All-time contributions4,520 Lines of code changed2,776,947 -Repository views (past two weeks)1,632 +Repository views (past two weeks)1,639 Repositories with contributions128 From 96c403a71fa18f4021140425ec5df4ba1c4557f6 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 31 Dec 2025 02:29:02 +0000 Subject: [PATCH 065/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index f1ab2aec752..f7bd6050e8e 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,460 -Forks1,152 +Forks1,151 All-time contributions4,520 -Lines of code changed2,776,947 +Lines of code changed2,775,283 -Repository views (past two weeks)1,639 +Repository views (past two weeks)1,635 Repositories with contributions128 From 956b3c0ff509e23550e2d12874bf1863c429c4ba Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 1 Jan 2026 02:40:40 +0000 Subject: [PATCH 066/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index f7bd6050e8e..e4e4b7985c9 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -97,9 +97,9 @@ tr { All-time contributions4,520 -Lines of code changed2,775,283 +Lines of code changed2,776,947 -Repository views (past two weeks)1,635 +Repository views (past two weeks)1,607 Repositories with contributions128 From a399400445d5eec7330a3841e0271f653919904c Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 2 Jan 2026 02:23:02 +0000 Subject: [PATCH 067/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index e4e4b7985c9..4bd4205670a 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,460 +Stars7,461 -Forks1,151 +Forks1,153 All-time contributions4,520 Lines of code changed2,776,947 -Repository views (past two weeks)1,607 +Repository views (past two weeks)1,608 Repositories with contributions128 From 4b1fab0150851750ffa2107aacfc124828fcd6bc Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 3 Jan 2026 02:23:55 +0000 Subject: [PATCH 068/303] Update generated files --- generated/languages.svg | 4 ++-- generated/overview.svg | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index de8aa3a4bee..0d6cdd4812a 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.36% +10.35% diff --git a/generated/overview.svg b/generated/overview.svg index 4bd4205670a..d315bb4326b 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -95,11 +95,11 @@ tr { Forks1,153 -All-time contributions4,520 +All-time contributions4,522 -Lines of code changed2,776,947 +Lines of code changed2,776,949 -Repository views (past two weeks)1,608 +Repository views (past two weeks)1,692 Repositories with contributions128 From 74f1ac16f0554d969f1da838a098446f39f5486c Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 4 Jan 2026 02:57:21 +0000 Subject: [PATCH 069/303] Update generated files --- generated/overview.svg | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index d315bb4326b..7b6cca32ac9 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,461 +Stars7,463 -Forks1,153 +Forks1,152 -All-time contributions4,522 +All-time contributions4,532 -Lines of code changed2,776,949 +Lines of code changed2,770,556 -Repository views (past two weeks)1,692 +Repository views (past two weeks)1,815 Repositories with contributions128 From 75d5f2a16549a424cae4e8c17c6b639b1831285a Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 5 Jan 2026 03:27:36 +0000 Subject: [PATCH 070/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 7b6cca32ac9..ea0e7535afc 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,463 -Forks1,152 +Forks1,153 All-time contributions4,532 -Lines of code changed2,770,556 +Lines of code changed2,756,976 -Repository views (past two weeks)1,815 +Repository views (past two weeks)1,866 Repositories with contributions128 From 29766fd6f4c49b848895835bafcf0e4633de82dc Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 6 Jan 2026 02:25:54 +0000 Subject: [PATCH 071/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index ea0e7535afc..bd862c780dd 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,463 +Stars7,465 Forks1,153 All-time contributions4,532 -Lines of code changed2,756,976 +Lines of code changed2,777,882 -Repository views (past two weeks)1,866 +Repository views (past two weeks)1,889 Repositories with contributions128 From 482e213a252d3a61c95e061f9d6a76118f91e856 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 7 Jan 2026 02:24:24 +0000 Subject: [PATCH 072/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index bd862c780dd..15381875784 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,465 +Stars7,466 -Forks1,153 +Forks1,151 All-time contributions4,532 Lines of code changed2,777,882 -Repository views (past two weeks)1,889 +Repository views (past two weeks)1,906 Repositories with contributions128 From c9024e23b2581da39b5ba0b855361b26a67e3000 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 8 Jan 2026 02:26:29 +0000 Subject: [PATCH 073/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 15381875784..dd6908d5b2d 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,466 +Stars7,468 -Forks1,151 +Forks1,149 All-time contributions4,532 Lines of code changed2,777,882 -Repository views (past two weeks)1,906 +Repository views (past two weeks)1,897 Repositories with contributions128 From 3e69e5a2ad79265b913120d32acf4bd4e5adcb82 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 9 Jan 2026 02:30:31 +0000 Subject: [PATCH 074/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index dd6908d5b2d..61502961f3f 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,468 +Stars7,469 Forks1,149 All-time contributions4,532 -Lines of code changed2,777,882 +Lines of code changed2,527,899 -Repository views (past two weeks)1,897 +Repository views (past two weeks)1,983 Repositories with contributions128 From 2d0fa8502839f0a7553f400755d19d2446dc5ea3 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 10 Jan 2026 02:26:00 +0000 Subject: [PATCH 075/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 61502961f3f..1ef1e7baca1 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,469 +Stars7,471 Forks1,149 All-time contributions4,532 -Lines of code changed2,527,899 +Lines of code changed2,777,882 -Repository views (past two weeks)1,983 +Repository views (past two weeks)2,010 Repositories with contributions128 From f41a715ee760f8a3e6250aba3b2e720ccb28be06 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 11 Jan 2026 05:03:32 +0000 Subject: [PATCH 076/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 1ef1e7baca1..0d0c284587b 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,471 +Stars7,472 -Forks1,149 +Forks1,150 All-time contributions4,532 -Lines of code changed2,777,882 +Lines of code changed618,625 -Repository views (past two weeks)2,010 +Repository views (past two weeks)2,080 Repositories with contributions128 From b66568d96d57c6066378a5abc6922ee382513c45 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 12 Jan 2026 04:46:56 +0000 Subject: [PATCH 077/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 0d0c284587b..64d0f5b3400 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,472 +Stars7,476 Forks1,150 All-time contributions4,532 -Lines of code changed618,625 +Lines of code changed2,422,834 -Repository views (past two weeks)2,080 +Repository views (past two weeks)2,025 Repositories with contributions128 From 4a4344715d50dd46c4aefdf9d44c0c6215f5afbb Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 13 Jan 2026 02:26:10 +0000 Subject: [PATCH 078/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 64d0f5b3400..36e06bf9b2c 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,476 +Stars7,477 Forks1,150 All-time contributions4,532 -Lines of code changed2,422,834 +Lines of code changed2,777,882 -Repository views (past two weeks)2,025 +Repository views (past two weeks)2,001 Repositories with contributions128 From 608b3db127fa9d5ab0ab1c7a78bb8dbca13e21d3 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 14 Jan 2026 03:28:38 +0000 Subject: [PATCH 079/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 36e06bf9b2c..6529699bafb 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,477 +Stars7,478 Forks1,150 All-time contributions4,532 -Lines of code changed2,777,882 +Lines of code changed2,726,432 -Repository views (past two weeks)2,001 +Repository views (past two weeks)1,989 Repositories with contributions128 From c3355353e7f1aff62a3959a87bdfb49a981d0275 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 15 Jan 2026 02:36:08 +0000 Subject: [PATCH 080/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 6529699bafb..6d17377143b 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,478 +Stars7,482 -Forks1,150 +Forks1,152 All-time contributions4,532 -Lines of code changed2,726,432 +Lines of code changed2,777,882 -Repository views (past two weeks)1,989 +Repository views (past two weeks)1,987 Repositories with contributions128 From 6c84c0ef10bfb244b3ec9dc151f71e2d604679c9 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 16 Jan 2026 02:38:42 +0000 Subject: [PATCH 081/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 6d17377143b..5c68e79cd8c 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,482 +Stars7,484 -Forks1,152 +Forks1,155 All-time contributions4,532 Lines of code changed2,777,882 -Repository views (past two weeks)1,987 +Repository views (past two weeks)2,031 Repositories with contributions128 From c9dc62da08351fa52a68543add42b8766329c53c Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 17 Jan 2026 02:24:21 +0000 Subject: [PATCH 082/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 5c68e79cd8c..a839551c6dd 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,484 +Stars7,486 -Forks1,155 +Forks1,156 All-time contributions4,532 Lines of code changed2,777,882 -Repository views (past two weeks)2,031 +Repository views (past two weeks)2,040 Repositories with contributions128 From 309bb7695eaa3214739e0f5f80393a31ceac791d Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 18 Jan 2026 03:31:49 +0000 Subject: [PATCH 083/303] Update generated files --- generated/languages.svg | 44 ++++++++++++++++++++--------------------- generated/overview.svg | 6 +++--- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 0d6cdd4812a..11ab71fbc9e 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.41% +29.37% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.79% +17.76% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.35% +10.43% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.45% +9.44% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.48% +7.47% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.70% +6.69% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.86% +5.85% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.42% +2.41% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.06% +2.05% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Java -1.32% +1.31% @@ -320,20 +320,20 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z">
  • - -TypeScript -0.10% +Tree-sitter Query +0.12%
  • - -Dockerfile -0.08% +TypeScript +0.10%
  • @@ -341,17 +341,17 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> -Just -0.07% +Dockerfile +0.08%
  • - -Tree-sitter Query -0.06% +Just +0.07%
  • diff --git a/generated/overview.svg b/generated/overview.svg index a839551c6dd..6781ecbed26 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,486 +Stars7,487 Forks1,156 All-time contributions4,532 -Lines of code changed2,777,882 +Lines of code changed2,518,327 -Repository views (past two weeks)2,040 +Repository views (past two weeks)1,913 Repositories with contributions128 From b655d85f2557cc2ec42dfc329a0f4fb8f65ce0d2 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 19 Jan 2026 03:49:43 +0000 Subject: [PATCH 084/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 6781ecbed26..5232077c234 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,487 +Stars7,488 -Forks1,156 +Forks1,157 All-time contributions4,532 -Lines of code changed2,518,327 +Lines of code changed2,714,065 -Repository views (past two weeks)1,913 +Repository views (past two weeks)1,867 Repositories with contributions128 From d9093a7dee472f5611856221bdf6f71c47487813 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 20 Jan 2026 03:30:32 +0000 Subject: [PATCH 085/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 5232077c234..90881ea6385 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,488 +Stars7,491 Forks1,157 All-time contributions4,532 -Lines of code changed2,714,065 +Lines of code changed2,036,520 -Repository views (past two weeks)1,867 +Repository views (past two weeks)1,861 Repositories with contributions128 From 31dc1b4656c4cadbb7fdcd288d86add85106acfe Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 21 Jan 2026 02:54:00 +0000 Subject: [PATCH 086/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 90881ea6385..80870936e13 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,491 +Stars7,492 -Forks1,157 +Forks1,156 All-time contributions4,532 -Lines of code changed2,036,520 +Lines of code changed1,458,525 -Repository views (past two weeks)1,861 +Repository views (past two weeks)1,852 Repositories with contributions128 From b548ff8be89d5e8a870286c6ec793a7382ce03d0 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 22 Jan 2026 02:46:13 +0000 Subject: [PATCH 087/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 80870936e13..9a86b6f450c 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -97,9 +97,9 @@ tr { All-time contributions4,532 -Lines of code changed1,458,525 +Lines of code changed2,777,882 -Repository views (past two weeks)1,852 +Repository views (past two weeks)1,823 Repositories with contributions128 From 654feccdea7de61912d517f6bccfc3b5f8ae4727 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 23 Jan 2026 02:38:30 +0000 Subject: [PATCH 088/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 9a86b6f450c..9ed342faead 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,492 +Stars7,494 -Forks1,156 +Forks1,157 All-time contributions4,532 Lines of code changed2,777,882 -Repository views (past two weeks)1,823 +Repository views (past two weeks)1,723 Repositories with contributions128 From df0cd62ea8f902f29fc83027aef0e244f2b3256f Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 24 Jan 2026 02:25:49 +0000 Subject: [PATCH 089/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 9ed342faead..309c042e9fc 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,494 +Stars7,496 Forks1,157 -All-time contributions4,532 +All-time contributions4,533 Lines of code changed2,777,882 -Repository views (past two weeks)1,723 +Repository views (past two weeks)1,688 Repositories with contributions128 From 6ac51fc8a00436d9477ae84564005f6def17ab11 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 25 Jan 2026 02:41:29 +0000 Subject: [PATCH 090/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 309c042e9fc..7b18a8a0bf6 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,496 +Stars7,497 -Forks1,157 +Forks1,156 All-time contributions4,533 Lines of code changed2,777,882 -Repository views (past two weeks)1,688 +Repository views (past two weeks)1,623 Repositories with contributions128 From 81d578affe03b7777be3ffd630425e55b0653767 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 26 Jan 2026 02:47:53 +0000 Subject: [PATCH 091/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 7b18a8a0bf6..3fd981b6ae7 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,7 +91,7 @@ tr { -Stars7,497 +Stars7,498 Forks1,156 @@ -99,7 +99,7 @@ tr { Lines of code changed2,777,882 -Repository views (past two weeks)1,623 +Repository views (past two weeks)1,599 Repositories with contributions128 From 8e07fc9c9a47ef50b1dd7b0b2c7c7cdb141ab292 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 27 Jan 2026 03:03:57 +0000 Subject: [PATCH 092/303] Update generated files --- generated/overview.svg | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 3fd981b6ae7..30de41cd45d 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,498 +Stars7,500 -Forks1,156 +Forks1,157 -All-time contributions4,533 +All-time contributions4,535 -Lines of code changed2,777,882 +Lines of code changed2,768,440 -Repository views (past two weeks)1,599 +Repository views (past two weeks)1,586 Repositories with contributions128 From 42da1c64e981a390a75dd69eec1688575c918df0 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 28 Jan 2026 02:36:59 +0000 Subject: [PATCH 093/303] Update generated files --- generated/languages.svg | 60 ++++++++++++++++++++--------------------- generated/overview.svg | 8 +++--- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 11ab71fbc9e..6f10cb4a331 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.37% +29.18% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.76% +17.65% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.43% +10.76% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.44% +9.42% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.47% +7.42% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.69% +6.64% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.85% +5.89% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.41% +2.40% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.05% +2.04% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.89% +1.87% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.68% +1.72% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.42% +1.41% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Java -1.31% +1.30% @@ -261,7 +261,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C++ -0.66% +0.65% @@ -284,6 +284,15 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z">
  • + +TypeScript +0.18% +
  • + + +
  • @@ -292,7 +301,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z">
  • -
  • +
  • @@ -301,7 +310,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z">
  • -
  • +
  • @@ -310,16 +319,16 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z">
  • -
  • +
  • PHP -0.14% +0.13%
  • -
  • +
  • @@ -328,15 +337,6 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z">
  • -
  • - -TypeScript -0.10% -
  • - -
  • viewBox="0 0 16 16" version="1.1" width="16" height="16"> Just -0.07% +0.06%
  • diff --git a/generated/overview.svg b/generated/overview.svg index 30de41cd45d..1b98fbbe9de 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,500 +Stars7,499 Forks1,157 -All-time contributions4,535 +All-time contributions4,537 -Lines of code changed2,768,440 +Lines of code changed2,778,092 -Repository views (past two weeks)1,586 +Repository views (past two weeks)1,554 Repositories with contributions128 From b3380b78a97485423467f6b6066937ce26339b0e Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 29 Jan 2026 02:50:46 +0000 Subject: [PATCH 094/303] Update generated files --- generated/languages.svg | 34 +++++++++++++++++----------------- generated/overview.svg | 10 +++++----- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 6f10cb4a331..b1a13af26cc 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.18% +29.35% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.65% +17.75% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.76% +10.85% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.42% +9.43% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.42% +7.47% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.64% +6.68% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.89% +5.36% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.40% +2.41% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.04% +2.05% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.87% +1.89% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.72% +1.69% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.41% +1.42% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Java -1.30% +1.31% @@ -261,7 +261,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C++ -0.65% +0.66% @@ -324,7 +324,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> PHP -0.13% +0.14% @@ -351,7 +351,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Just -0.06% +0.08% diff --git a/generated/overview.svg b/generated/overview.svg index 1b98fbbe9de..7d04523e954 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,499 +Stars7,500 -Forks1,157 +Forks1,158 -All-time contributions4,537 +All-time contributions4,539 -Lines of code changed2,778,092 +Lines of code changed2,778,158 -Repository views (past two weeks)1,554 +Repository views (past two weeks)1,551 Repositories with contributions128 From 08ffd1a6aea2546e23449577b0028b8b1db6bf81 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 30 Jan 2026 03:03:45 +0000 Subject: [PATCH 095/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 7d04523e954..38727ee4262 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -95,11 +95,11 @@ tr { Forks1,158 -All-time contributions4,539 +All-time contributions4,540 -Lines of code changed2,778,158 +Lines of code changed2,778,160 -Repository views (past two weeks)1,551 +Repository views (past two weeks)1,499 Repositories with contributions128 From d3bbbc44fcf866e4f76b21562d55872791de7621 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 31 Jan 2026 03:28:02 +0000 Subject: [PATCH 096/303] Update generated files --- generated/overview.svg | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 38727ee4262..4c50027f996 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,500 +Stars7,503 -Forks1,158 +Forks1,159 -All-time contributions4,540 +All-time contributions4,543 -Lines of code changed2,778,160 +Lines of code changed2,723,796 -Repository views (past two weeks)1,499 +Repository views (past two weeks)1,391 Repositories with contributions128 From 108accf5fcc116a28d715bca03b66c14455f78bf Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 1 Feb 2026 03:51:59 +0000 Subject: [PATCH 097/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 4c50027f996..39005efba4f 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,503 +Stars7,504 Forks1,159 All-time contributions4,543 -Lines of code changed2,723,796 +Lines of code changed2,778,269 -Repository views (past two weeks)1,391 +Repository views (past two weeks)1,421 Repositories with contributions128 From c5ced2a57fb7ad0d71993fdf3b72b5a3e037a88c Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 2 Feb 2026 03:37:23 +0000 Subject: [PATCH 098/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 39005efba4f..c72fd205197 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,504 +Stars7,505 -Forks1,159 +Forks1,160 All-time contributions4,543 -Lines of code changed2,778,269 +Lines of code changed2,770,321 -Repository views (past two weeks)1,421 +Repository views (past two weeks)1,432 Repositories with contributions128 From 7f8c72e829b69d7d4cf54c0992c68a9ed17a244a Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 3 Feb 2026 03:11:54 +0000 Subject: [PATCH 099/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index c72fd205197..751133ed122 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,505 -Forks1,160 +Forks1,159 All-time contributions4,543 -Lines of code changed2,770,321 +Lines of code changed2,778,269 -Repository views (past two weeks)1,432 +Repository views (past two weeks)1,430 Repositories with contributions128 From 4593394d362c75ab5280f2ab81985a7f7208ce5f Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 4 Feb 2026 03:10:12 +0000 Subject: [PATCH 100/303] Update generated files --- generated/languages.svg | 6 +++--- generated/overview.svg | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index b1a13af26cc..45c4d1dc367 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.85% +10.86% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.36% +5.35% diff --git a/generated/overview.svg b/generated/overview.svg index 751133ed122..9f321bd3cda 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,7 +91,7 @@ tr { -Stars7,505 +Stars7,506 Forks1,159 @@ -99,7 +99,7 @@ tr { Lines of code changed2,778,269 -Repository views (past two weeks)1,430 +Repository views (past two weeks)1,418 Repositories with contributions128 From 363efcd98cf175de5507731cd8ce79ece0e17822 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 5 Feb 2026 03:11:14 +0000 Subject: [PATCH 101/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 9f321bd3cda..44eb6390827 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,506 +Stars7,507 -Forks1,159 +Forks1,160 All-time contributions4,543 Lines of code changed2,778,269 -Repository views (past two weeks)1,418 +Repository views (past two weeks)1,406 Repositories with contributions128 From 8cbe7092c9fc19fbd3112bb8813754aa860046bf Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 6 Feb 2026 03:01:04 +0000 Subject: [PATCH 102/303] Update generated files --- generated/languages.svg | 24 ++++++++++++------------ generated/overview.svg | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 45c4d1dc367..f7441e1f94a 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.35% +29.30% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.75% +17.72% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -10.86% +11.01% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.43% +9.41% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.47% +7.45% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.68% +6.67% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.89% +1.88% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.69% +1.68% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.42% +1.41% @@ -288,7 +288,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TypeScript -0.18% +0.19% @@ -324,7 +324,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> PHP -0.14% +0.13% diff --git a/generated/overview.svg b/generated/overview.svg index 44eb6390827..936f7d8b926 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,7 +91,7 @@ tr { -Stars7,507 +Stars7,508 Forks1,160 @@ -99,7 +99,7 @@ tr { Lines of code changed2,778,269 -Repository views (past two weeks)1,406 +Repository views (past two weeks)1,427 Repositories with contributions128 From a3a824c2415f53778a480709bd3a66822d657bd0 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 7 Feb 2026 03:02:09 +0000 Subject: [PATCH 103/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 936f7d8b926..5d1b07e7b55 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,508 +Stars7,509 Forks1,160 All-time contributions4,543 -Lines of code changed2,778,269 +Lines of code changed2,776,973 -Repository views (past two weeks)1,427 +Repository views (past two weeks)1,486 Repositories with contributions128 From 5cd034ce7d8de6faa3b4f7a42a7788b5607d095d Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 8 Feb 2026 03:56:34 +0000 Subject: [PATCH 104/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 5d1b07e7b55..4fd396f5ed6 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,509 +Stars7,511 -Forks1,160 +Forks1,159 All-time contributions4,543 -Lines of code changed2,776,973 +Lines of code changed2,774,995 -Repository views (past two weeks)1,486 +Repository views (past two weeks)1,507 Repositories with contributions128 From 13af43583c85255291efc177e187548f67d905c2 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 9 Feb 2026 03:02:11 +0000 Subject: [PATCH 105/303] Update generated files --- generated/languages.svg | 14 +++++++------- generated/overview.svg | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index f7441e1f94a..02d63624f6b 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.72% +17.71% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -11.01% +11.03% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.35% +5.34% @@ -341,8 +341,8 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> -Dockerfile -0.08% +Just +0.09% @@ -350,7 +350,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> -Just +Dockerfile 0.08% diff --git a/generated/overview.svg b/generated/overview.svg index 4fd396f5ed6..a4192f358fa 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,511 +Stars7,513 Forks1,159 All-time contributions4,543 -Lines of code changed2,774,995 +Lines of code changed2,778,269 -Repository views (past two weeks)1,507 +Repository views (past two weeks)1,524 Repositories with contributions128 From a6bdffe8314cec5f57e55f4c2d48b6a82ab6d10a Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 10 Feb 2026 03:17:39 +0000 Subject: [PATCH 106/303] Update generated files --- generated/overview.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generated/overview.svg b/generated/overview.svg index a4192f358fa..ba6c79f8616 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -99,7 +99,7 @@ tr { Lines of code changed2,778,269 -Repository views (past two weeks)1,524 +Repository views (past two weeks)1,537 Repositories with contributions128 From 8561db79ee470242822eb359b3dc1f23e2e73dea Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 11 Feb 2026 03:10:03 +0000 Subject: [PATCH 107/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index ba6c79f8616..e2b0b6baf34 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,513 +Stars7,515 -Forks1,159 +Forks1,160 All-time contributions4,543 -Lines of code changed2,778,269 +Lines of code changed2,777,663 -Repository views (past two weeks)1,537 +Repository views (past two weeks)1,568 Repositories with contributions128 From da5679d78c2faa5359f7de723108889277532919 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 11 Feb 2026 20:36:37 -0500 Subject: [PATCH 108/303] Add empty Zig project --- .github/workflows/main.yml | 24 +- .gitignore | 4 + build.zig | 26 ++ build.zig.zon | 15 + generate_images.py | 136 --------- github_stats.py | 545 ------------------------------------- requirements.txt | 2 - src/main.zig | 6 + 8 files changed, 59 insertions(+), 699 deletions(-) create mode 100644 build.zig create mode 100644 build.zig.zon delete mode 100644 generate_images.py delete mode 100644 github_stats.py delete mode 100644 requirements.txt create mode 100644 src/main.zig diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0e2488133e9..bfa93b79f7e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,7 +2,8 @@ name: Generate Stats Images on: push: - branches: [ master ] + branches: + - master schedule: - cron: "5 0 * * *" workflow_dispatch: @@ -15,28 +16,19 @@ jobs: runs-on: ubuntu-latest steps: - # Check out repository under $GITHUB_WORKSPACE, so the job can access it - uses: actions/checkout@v3 - - # Run using Python 3.8 for consistency and aiohttp - - name: Set up Python 3.8 - uses: actions/setup-python@v4 + - uses: mlugg/setup-zig@v2 with: - python-version: '3.8' - architecture: 'x64' - cache: 'pip' + version: 0.15.2 - # Install dependencies with `pip` - - name: Install requirements + # TODO: Cache build + - name: Build run: | - python3 -m pip install --upgrade pip setuptools wheel - python3 -m pip install -r requirements.txt + echo TODO - # Generate all statistics images - name: Generate images run: | - python3 --version - python3 generate_images.py + echo TODO env: ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index a9e4db76055..38c3d4792fe 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,7 @@ dmypy.json # PyCharm project files .idea + +# Zig files +.zig-cache +zig-out diff --git a/build.zig b/build.zig new file mode 100644 index 00000000000..cc6b9c6dd23 --- /dev/null +++ b/build.zig @@ -0,0 +1,26 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{ + .preferred_optimize_mode = .ReleaseSafe, + }); + + const exe = b.addExecutable(.{ + .name = "github_stats", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), + }); + b.installArtifact(exe); + + const run_step = b.step("run", "Run the app"); + const run_cmd = b.addRunArtifact(exe); + run_step.dependOn(&run_cmd.step); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 00000000000..c7bf1934a9c --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,15 @@ +.{ + .name = .github_stats, + .version = "0.0.0", + .fingerprint = 0x80bb05a632422e37, // Changing this has security and trust implications. + .minimum_zig_version = "0.15.2", + .dependencies = .{}, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "LICENSE", + "README.md", + "templates", + }, +} diff --git a/generate_images.py b/generate_images.py deleted file mode 100644 index e800b9357ea..00000000000 --- a/generate_images.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/python3 - -import asyncio -import os -import re - -import aiohttp - -from github_stats import Stats - - -################################################################################ -# Helper Functions -################################################################################ - - -def generate_output_folder() -> None: - """ - Create the output folder if it does not already exist - """ - if not os.path.isdir("generated"): - os.mkdir("generated") - - -################################################################################ -# Individual Image Generation Functions -################################################################################ - - -async def generate_overview(s: Stats) -> None: - """ - Generate an SVG badge with summary statistics - :param s: Represents user's GitHub statistics - """ - with open("templates/overview.svg", "r") as f: - output = f.read() - - output = re.sub("{{ name }}", await s.name, output) - output = re.sub("{{ stars }}", f"{await s.stargazers:,}", output) - output = re.sub("{{ forks }}", f"{await s.forks:,}", output) - output = re.sub("{{ contributions }}", f"{await s.total_contributions:,}", output) - changed = (await s.lines_changed)[0] + (await s.lines_changed)[1] - output = re.sub("{{ lines_changed }}", f"{changed:,}", output) - output = re.sub("{{ views }}", f"{await s.views:,}", output) - output = re.sub("{{ repos }}", f"{len(await s.repos):,}", output) - - generate_output_folder() - with open("generated/overview.svg", "w") as f: - f.write(output) - - -async def generate_languages(s: Stats) -> None: - """ - Generate an SVG badge with summary languages used - :param s: Represents user's GitHub statistics - """ - with open("templates/languages.svg", "r") as f: - output = f.read() - - progress = "" - lang_list = "" - sorted_languages = sorted( - (await s.languages).items(), reverse=True, key=lambda t: t[1].get("size") - ) - delay_between = 150 - for i, (lang, data) in enumerate(sorted_languages): - color = data.get("color") - color = color if color is not None else "#000000" - progress += ( - f'' - ) - lang_list += f""" -
  • - -{lang} -{data.get("prop", 0):0.2f}% -
  • - -""" - - output = re.sub(r"{{ progress }}", progress, output) - output = re.sub(r"{{ lang_list }}", lang_list, output) - - generate_output_folder() - with open("generated/languages.svg", "w") as f: - f.write(output) - - -################################################################################ -# Main Function -################################################################################ - - -async def main() -> None: - """ - Generate all badges - """ - access_token = os.getenv("ACCESS_TOKEN") - if not access_token: - # access_token = os.getenv("GITHUB_TOKEN") - raise Exception("A personal access token is required to proceed!") - user = os.getenv("GITHUB_ACTOR") - if user is None: - raise RuntimeError("Environment variable GITHUB_ACTOR must be set.") - exclude_repos = os.getenv("EXCLUDED") - excluded_repos = ( - {x.strip() for x in exclude_repos.split(",")} if exclude_repos else None - ) - exclude_langs = os.getenv("EXCLUDED_LANGS") - excluded_langs = ( - {x.strip() for x in exclude_langs.split(",")} if exclude_langs else None - ) - # Convert a truthy value to a Boolean - raw_ignore_forked_repos = os.getenv("EXCLUDE_FORKED_REPOS") - ignore_forked_repos = ( - not not raw_ignore_forked_repos - and raw_ignore_forked_repos.strip().lower() != "false" - ) - async with aiohttp.ClientSession() as session: - s = Stats( - user, - access_token, - session, - exclude_repos=excluded_repos, - exclude_langs=excluded_langs, - ignore_forked_repos=ignore_forked_repos, - ) - await asyncio.gather(generate_languages(s), generate_overview(s)) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/github_stats.py b/github_stats.py deleted file mode 100644 index b663896e942..00000000000 --- a/github_stats.py +++ /dev/null @@ -1,545 +0,0 @@ -#!/usr/bin/python3 - -import asyncio -import os -from typing import Dict, List, Optional, Set, Tuple, Any, cast - -import aiohttp -import requests - - -############################################################################### -# Main Classes -############################################################################### - - -class Queries(object): - """ - Class with functions to query the GitHub GraphQL (v4) API and the REST (v3) - API. Also includes functions to dynamically generate GraphQL queries. - """ - - def __init__( - self, - username: str, - access_token: str, - session: aiohttp.ClientSession, - max_connections: int = 10, - ): - self.username = username - self.access_token = access_token - self.session = session - self.semaphore = asyncio.Semaphore(max_connections) - - async def query(self, generated_query: str) -> Dict: - """ - Make a request to the GraphQL API using the authentication token from - the environment - :param generated_query: string query to be sent to the API - :return: decoded GraphQL JSON output - """ - headers = { - "Authorization": f"Bearer {self.access_token}", - } - try: - async with self.semaphore: - r_async = await self.session.post( - "https://api.github.com/graphql", - headers=headers, - json={"query": generated_query}, - ) - result = await r_async.json() - if result is not None: - return result - except: - print("aiohttp failed for GraphQL query") - # Fall back on non-async requests - async with self.semaphore: - r_requests = requests.post( - "https://api.github.com/graphql", - headers=headers, - json={"query": generated_query}, - ) - result = r_requests.json() - if result is not None: - return result - return dict() - - async def query_rest(self, path: str, params: Optional[Dict] = None) -> Dict: - """ - Make a request to the REST API - :param path: API path to query - :param params: Query parameters to be passed to the API - :return: deserialized REST JSON output - """ - - for _ in range(60): - headers = { - "Authorization": f"token {self.access_token}", - } - if params is None: - params = dict() - if path.startswith("/"): - path = path[1:] - try: - async with self.semaphore: - r_async = await self.session.get( - f"https://api.github.com/{path}", - headers=headers, - params=tuple(params.items()), - ) - if r_async.status == 202: - # print(f"{path} returned 202. Retrying...") - print(f"A path returned 202. Retrying...") - await asyncio.sleep(2) - continue - - result = await r_async.json() - if result is not None: - return result - except: - print("aiohttp failed for rest query") - # Fall back on non-async requests - async with self.semaphore: - r_requests = requests.get( - f"https://api.github.com/{path}", - headers=headers, - params=tuple(params.items()), - ) - if r_requests.status_code == 202: - print(f"A path returned 202. Retrying...") - await asyncio.sleep(2) - continue - elif r_requests.status_code == 200: - return r_requests.json() - # print(f"There were too many 202s. Data for {path} will be incomplete.") - print("There were too many 202s. Data for this repository will be incomplete.") - return dict() - - @staticmethod - def repos_overview( - contrib_cursor: Optional[str] = None, owned_cursor: Optional[str] = None - ) -> str: - """ - :return: GraphQL query with overview of user repositories - """ - return f"""{{ - viewer {{ - login, - name, - repositories( - first: 100, - orderBy: {{ - field: UPDATED_AT, - direction: DESC - }}, - isFork: false, - after: {"null" if owned_cursor is None else '"'+ owned_cursor +'"'} - ) {{ - pageInfo {{ - hasNextPage - endCursor - }} - nodes {{ - nameWithOwner - stargazers {{ - totalCount - }} - forkCount - languages(first: 10, orderBy: {{field: SIZE, direction: DESC}}) {{ - edges {{ - size - node {{ - name - color - }} - }} - }} - }} - }} - repositoriesContributedTo( - first: 100, - includeUserRepositories: false, - orderBy: {{ - field: UPDATED_AT, - direction: DESC - }}, - contributionTypes: [ - COMMIT, - PULL_REQUEST, - REPOSITORY, - PULL_REQUEST_REVIEW - ] - after: {"null" if contrib_cursor is None else '"'+ contrib_cursor +'"'} - ) {{ - pageInfo {{ - hasNextPage - endCursor - }} - nodes {{ - nameWithOwner - stargazers {{ - totalCount - }} - forkCount - languages(first: 10, orderBy: {{field: SIZE, direction: DESC}}) {{ - edges {{ - size - node {{ - name - color - }} - }} - }} - }} - }} - }} -}} -""" - - @staticmethod - def contrib_years() -> str: - """ - :return: GraphQL query to get all years the user has been a contributor - """ - return """ -query { - viewer { - contributionsCollection { - contributionYears - } - } -} -""" - - @staticmethod - def contribs_by_year(year: str) -> str: - """ - :param year: year to query for - :return: portion of a GraphQL query with desired info for a given year - """ - return f""" - year{year}: contributionsCollection( - from: "{year}-01-01T00:00:00Z", - to: "{int(year) + 1}-01-01T00:00:00Z" - ) {{ - contributionCalendar {{ - totalContributions - }} - }} -""" - - @classmethod - def all_contribs(cls, years: List[str]) -> str: - """ - :param years: list of years to get contributions for - :return: query to retrieve contribution information for all user years - """ - by_years = "\n".join(map(cls.contribs_by_year, years)) - return f""" -query {{ - viewer {{ - {by_years} - }} -}} -""" - - -class Stats(object): - """ - Retrieve and store statistics about GitHub usage. - """ - - def __init__( - self, - username: str, - access_token: str, - session: aiohttp.ClientSession, - exclude_repos: Optional[Set] = None, - exclude_langs: Optional[Set] = None, - ignore_forked_repos: bool = False, - ): - self.username = username - self._ignore_forked_repos = ignore_forked_repos - self._exclude_repos = set() if exclude_repos is None else exclude_repos - self._exclude_langs = set() if exclude_langs is None else exclude_langs - self.queries = Queries(username, access_token, session) - - self._name: Optional[str] = None - self._stargazers: Optional[int] = None - self._forks: Optional[int] = None - self._total_contributions: Optional[int] = None - self._languages: Optional[Dict[str, Any]] = None - self._repos: Optional[Set[str]] = None - self._lines_changed: Optional[Tuple[int, int]] = None - self._views: Optional[int] = None - - async def to_str(self) -> str: - """ - :return: summary of all available statistics - """ - languages = await self.languages_proportional - formatted_languages = "\n - ".join( - [f"{k}: {v:0.4f}%" for k, v in languages.items()] - ) - lines_changed = await self.lines_changed - return f"""Name: {await self.name} -Stargazers: {await self.stargazers:,} -Forks: {await self.forks:,} -All-time contributions: {await self.total_contributions:,} -Repositories with contributions: {len(await self.repos)} -Lines of code added: {lines_changed[0]:,} -Lines of code deleted: {lines_changed[1]:,} -Lines of code changed: {lines_changed[0] + lines_changed[1]:,} -Project page views: {await self.views:,} -Languages: - - {formatted_languages}""" - - async def get_stats(self) -> None: - """ - Get lots of summary statistics using one big query. Sets many attributes - """ - self._stargazers = 0 - self._forks = 0 - self._languages = dict() - self._repos = set() - - exclude_langs_lower = {x.lower() for x in self._exclude_langs} - - next_owned = None - next_contrib = None - while True: - raw_results = await self.queries.query( - Queries.repos_overview( - owned_cursor=next_owned, contrib_cursor=next_contrib - ) - ) - raw_results = raw_results if raw_results is not None else {} - - self._name = raw_results.get("data", {}).get("viewer", {}).get("name", None) - if self._name is None: - self._name = ( - raw_results.get("data", {}) - .get("viewer", {}) - .get("login", "No Name") - ) - - contrib_repos = ( - raw_results.get("data", {}) - .get("viewer", {}) - .get("repositoriesContributedTo", {}) - ) - owned_repos = ( - raw_results.get("data", {}).get("viewer", {}).get("repositories", {}) - ) - - repos = owned_repos.get("nodes", []) - if not self._ignore_forked_repos: - repos += contrib_repos.get("nodes", []) - - for repo in repos: - if repo is None: - continue - name = repo.get("nameWithOwner") - if name in self._repos or name in self._exclude_repos: - continue - self._repos.add(name) - self._stargazers += repo.get("stargazers").get("totalCount", 0) - self._forks += repo.get("forkCount", 0) - - for lang in repo.get("languages", {}).get("edges", []): - name = lang.get("node", {}).get("name", "Other") - languages = await self.languages - if name.lower() in exclude_langs_lower: - continue - if name in languages: - languages[name]["size"] += lang.get("size", 0) - languages[name]["occurrences"] += 1 - else: - languages[name] = { - "size": lang.get("size", 0), - "occurrences": 1, - "color": lang.get("node", {}).get("color"), - } - - if owned_repos.get("pageInfo", {}).get( - "hasNextPage", False - ) or contrib_repos.get("pageInfo", {}).get("hasNextPage", False): - next_owned = owned_repos.get("pageInfo", {}).get( - "endCursor", next_owned - ) - next_contrib = contrib_repos.get("pageInfo", {}).get( - "endCursor", next_contrib - ) - else: - break - - # TODO: Improve languages to scale by number of contributions to - # specific filetypes - langs_total = sum([v.get("size", 0) for v in self._languages.values()]) - for k, v in self._languages.items(): - v["prop"] = 100 * (v.get("size", 0) / langs_total) - - @property - async def name(self) -> str: - """ - :return: GitHub user's name (e.g., Jacob Strieb) - """ - if self._name is not None: - return self._name - await self.get_stats() - assert self._name is not None - return self._name - - @property - async def stargazers(self) -> int: - """ - :return: total number of stargazers on user's repos - """ - if self._stargazers is not None: - return self._stargazers - await self.get_stats() - assert self._stargazers is not None - return self._stargazers - - @property - async def forks(self) -> int: - """ - :return: total number of forks on user's repos - """ - if self._forks is not None: - return self._forks - await self.get_stats() - assert self._forks is not None - return self._forks - - @property - async def languages(self) -> Dict: - """ - :return: summary of languages used by the user - """ - if self._languages is not None: - return self._languages - await self.get_stats() - assert self._languages is not None - return self._languages - - @property - async def languages_proportional(self) -> Dict: - """ - :return: summary of languages used by the user, with proportional usage - """ - if self._languages is None: - await self.get_stats() - assert self._languages is not None - - return {k: v.get("prop", 0) for (k, v) in self._languages.items()} - - @property - async def repos(self) -> Set[str]: - """ - :return: list of names of user's repos - """ - if self._repos is not None: - return self._repos - await self.get_stats() - assert self._repos is not None - return self._repos - - @property - async def total_contributions(self) -> int: - """ - :return: count of user's total contributions as defined by GitHub - """ - if self._total_contributions is not None: - return self._total_contributions - - self._total_contributions = 0 - years = ( - (await self.queries.query(Queries.contrib_years())) - .get("data", {}) - .get("viewer", {}) - .get("contributionsCollection", {}) - .get("contributionYears", []) - ) - by_year = ( - (await self.queries.query(Queries.all_contribs(years))) - .get("data", {}) - .get("viewer", {}) - .values() - ) - for year in by_year: - self._total_contributions += year.get("contributionCalendar", {}).get( - "totalContributions", 0 - ) - return cast(int, self._total_contributions) - - @property - async def lines_changed(self) -> Tuple[int, int]: - """ - :return: count of total lines added, removed, or modified by the user - """ - if self._lines_changed is not None: - return self._lines_changed - additions = 0 - deletions = 0 - for repo in await self.repos: - r = await self.queries.query_rest(f"/repos/{repo}/stats/contributors") - for author_obj in r: - # Handle malformed response from the API by skipping this repo - if not isinstance(author_obj, dict) or not isinstance( - author_obj.get("author", {}), dict - ): - continue - author = author_obj.get("author", {}).get("login", "") - if author != self.username: - continue - - for week in author_obj.get("weeks", []): - additions += week.get("a", 0) - deletions += week.get("d", 0) - - self._lines_changed = (additions, deletions) - return self._lines_changed - - @property - async def views(self) -> int: - """ - Note: only returns views for the last 14 days (as-per GitHub API) - :return: total number of page views the user's projects have received - """ - if self._views is not None: - return self._views - - total = 0 - for repo in await self.repos: - r = await self.queries.query_rest(f"/repos/{repo}/traffic/views") - for view in r.get("views", []): - total += view.get("count", 0) - - self._views = total - return total - - -############################################################################### -# Main Function -############################################################################### - - -async def main() -> None: - """ - Used mostly for testing; this module is not usually run standalone - """ - access_token = os.getenv("ACCESS_TOKEN") - user = os.getenv("GITHUB_ACTOR") - if access_token is None or user is None: - raise RuntimeError( - "ACCESS_TOKEN and GITHUB_ACTOR environment variables cannot be None!" - ) - async with aiohttp.ClientSession() as session: - s = Stats(user, access_token, session) - print(await s.to_str()) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 84b68da70b9..00000000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -requests -aiohttp \ No newline at end of file diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 00000000000..63397663096 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,6 @@ +const std = @import("std"); + +pub fn main() !void { + // Prints to stderr, ignoring potential errors. + std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); +} From 6a2e061b1534de86f0e159a5075cafaf4a89916e Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 11 Feb 2026 21:14:14 -0500 Subject: [PATCH 109/303] Set up logging --- src/main.zig | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main.zig b/src/main.zig index 63397663096..88c5dc0aa50 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,26 @@ +const builtin = @import("builtin"); const std = @import("std"); +pub const std_options: std.Options = .{ + .logFn = logFn, +}; + +var log_level = std.log.default_level; + +fn logFn( + comptime message_level: std.log.Level, + comptime scope: @TypeOf(.enum_literal), + comptime format: []const u8, + args: anytype, +) void { + if (@intFromEnum(message_level) <= @intFromEnum(log_level)) { + std.log.defaultLog(message_level, scope, format, args); + } +} + pub fn main() !void { - // Prints to stderr, ignoring potential errors. - std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); + // TODO: Parse environment variables + // TODO: Parse CLI flags + // TODO: Download statistics to populate data structures + // TODO: Output images from templates } From 4fd0032093897abb56d4bba92039d5dccebf0dbb Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 12 Feb 2026 03:28:34 +0000 Subject: [PATCH 110/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index e2b0b6baf34..ccb386e32d5 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,515 +Stars7,520 -Forks1,160 +Forks1,162 All-time contributions4,543 -Lines of code changed2,777,663 +Lines of code changed2,528,286 -Repository views (past two weeks)1,568 +Repository views (past two weeks)1,572 Repositories with contributions128 From 64503cc2edd608dfc594be2cd289679666d4e2e4 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 11 Feb 2026 23:36:40 -0500 Subject: [PATCH 111/303] Make an HTTP request --- src/main.zig | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main.zig b/src/main.zig index 88c5dc0aa50..4cf2186609a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,6 +6,7 @@ pub const std_options: std.Options = .{ }; var log_level = std.log.default_level; +var allocator: std.mem.Allocator = undefined; fn logFn( comptime message_level: std.log.Level, @@ -19,8 +20,25 @@ fn logFn( } pub fn main() !void { + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; + defer _ = gpa.deinit(); + allocator = gpa.allocator(); + // TODO: Parse environment variables // TODO: Parse CLI flags + + var client: std.http.Client = .{ .allocator = allocator }; + defer client.deinit(); + var writer = std.Io.Writer.Allocating.init(allocator); + defer writer.deinit(); + _ = try client.fetch(.{ + .location = .{ .url = "https://jstrieb.github.io" }, + .response_writer = &writer.writer, + }); + const body = try writer.toOwnedSlice(); + defer allocator.free(body); + // TODO: Download statistics to populate data structures + // TODO: Output images from templates } From 6ee8c1b377b2b701d5742c79a9dbbe740393749f Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Thu, 12 Feb 2026 00:09:23 -0500 Subject: [PATCH 112/303] Wrap HTTP client with nicer methods --- src/main.zig | 69 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/src/main.zig b/src/main.zig index 4cf2186609a..5d3a8d907d6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -19,6 +19,56 @@ fn logFn( } } +/// Naive, unoptimized HTTP client with .get and .post methods. Simple, and not +/// particularly efficient. +const Client = struct { + client: std.http.Client, + + const Self = @This(); + + pub fn init() Self { + return .{ + .client = .{ .allocator = allocator }, + }; + } + + pub fn deinit(self: *Self) void { + self.client.deinit(); + } + + pub fn get( + self: *Self, + url: []const u8, + headers: ?std.http.Client.Request.Headers, + ) ![]u8 { + var writer = try std.Io.Writer.Allocating.initCapacity(allocator, 1024); + defer writer.deinit(); + _ = try self.client.fetch(.{ + .location = .{ .url = url }, + .response_writer = &writer.writer, + .headers = headers orelse .{}, + }); + return try writer.toOwnedSlice(); + } + + pub fn post( + self: *Self, + url: []const u8, + body: []const u8, + headers: ?std.http.Client.Request.Headers, + ) ![]u8 { + var writer = try std.Io.Writer.Allocating.initCapacity(allocator, 1024); + defer writer.deinit(); + _ = try self.client.fetch(.{ + .location = .{ .url = url }, + .response_writer = &writer.writer, + .payload = body, + .headers = headers orelse .{}, + }); + return try writer.toOwnedSlice(); + } +}; + pub fn main() !void { var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; defer _ = gpa.deinit(); @@ -27,16 +77,19 @@ pub fn main() !void { // TODO: Parse environment variables // TODO: Parse CLI flags - var client: std.http.Client = .{ .allocator = allocator }; + var client: Client = .init(); defer client.deinit(); - var writer = std.Io.Writer.Allocating.init(allocator); - defer writer.deinit(); - _ = try client.fetch(.{ - .location = .{ .url = "https://jstrieb.github.io" }, - .response_writer = &writer.writer, - }); - const body = try writer.toOwnedSlice(); + var body = try client.get("https://jstrieb.github.io", null); + std.log.debug("{s}\n", .{body[0..100]}); + allocator.free(body); + + body = try client.post( + "https://httpbin.org/post", + "{\"a\": 10, \"b\": [ 1, 2, 3 ]}", + .{ .content_type = .{ .override = "application/json" } }, + ); defer allocator.free(body); + std.log.debug("{s}\n", .{body}); // TODO: Download statistics to populate data structures From f893cb4903fbd965cceacafcc080bb7b140a39ba Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Thu, 12 Feb 2026 00:34:25 -0500 Subject: [PATCH 113/303] Arena allocate request bodies --- src/main.zig | 50 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/src/main.zig b/src/main.zig index 5d3a8d907d6..f0ab31c6ee8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -20,20 +20,30 @@ fn logFn( } /// Naive, unoptimized HTTP client with .get and .post methods. Simple, and not -/// particularly efficient. +/// particularly efficient. Response bodies stay allocated for the lifetime of +/// the client. const Client = struct { + arena: *std.heap.ArenaAllocator, + allocator: std.mem.Allocator, client: std.http.Client, const Self = @This(); - pub fn init() Self { + pub fn init() !Self { + const arena = try allocator.create(std.heap.ArenaAllocator); + arena.* = std.heap.ArenaAllocator.init(allocator); + const a = arena.allocator(); return .{ - .client = .{ .allocator = allocator }, + .arena = arena, + .allocator = a, + .client = .{ .allocator = a }, }; } pub fn deinit(self: *Self) void { self.client.deinit(); + self.arena.deinit(); + allocator.destroy(self.arena); } pub fn get( @@ -41,7 +51,10 @@ const Client = struct { url: []const u8, headers: ?std.http.Client.Request.Headers, ) ![]u8 { - var writer = try std.Io.Writer.Allocating.initCapacity(allocator, 1024); + var writer = try std.Io.Writer.Allocating.initCapacity( + self.allocator, + 1024, + ); defer writer.deinit(); _ = try self.client.fetch(.{ .location = .{ .url = url }, @@ -57,7 +70,10 @@ const Client = struct { body: []const u8, headers: ?std.http.Client.Request.Headers, ) ![]u8 { - var writer = try std.Io.Writer.Allocating.initCapacity(allocator, 1024); + var writer = try std.Io.Writer.Allocating.initCapacity( + self.allocator, + 1024, + ); defer writer.deinit(); _ = try self.client.fetch(.{ .location = .{ .url = url }, @@ -77,21 +93,19 @@ pub fn main() !void { // TODO: Parse environment variables // TODO: Parse CLI flags - var client: Client = .init(); + var client: Client = try .init(); defer client.deinit(); - var body = try client.get("https://jstrieb.github.io", null); - std.log.debug("{s}\n", .{body[0..100]}); - allocator.free(body); - - body = try client.post( - "https://httpbin.org/post", - "{\"a\": 10, \"b\": [ 1, 2, 3 ]}", - .{ .content_type = .{ .override = "application/json" } }, - ); - defer allocator.free(body); - std.log.debug("{s}\n", .{body}); + std.log.debug("{s}\n", .{ + try client.get("https://jstrieb.github.io", null), + }); + std.log.debug("{s}\n", .{ + try client.post( + "https://httpbin.org/post", + "{\"a\": 10, \"b\": [ 1, 2, 3 ]}", + .{ .content_type = .{ .override = "application/json" } }, + ), + }); // TODO: Download statistics to populate data structures - // TODO: Output images from templates } From 50bb92e01aacdc51c603dd8cc4bcdb403bfa2ba5 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Thu, 12 Feb 2026 00:50:43 -0500 Subject: [PATCH 114/303] Simplify get and post args --- src/main.zig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main.zig b/src/main.zig index f0ab31c6ee8..70167dc3b2e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -49,7 +49,7 @@ const Client = struct { pub fn get( self: *Self, url: []const u8, - headers: ?std.http.Client.Request.Headers, + headers: std.http.Client.Request.Headers, ) ![]u8 { var writer = try std.Io.Writer.Allocating.initCapacity( self.allocator, @@ -59,7 +59,7 @@ const Client = struct { _ = try self.client.fetch(.{ .location = .{ .url = url }, .response_writer = &writer.writer, - .headers = headers orelse .{}, + .headers = headers, }); return try writer.toOwnedSlice(); } @@ -68,7 +68,7 @@ const Client = struct { self: *Self, url: []const u8, body: []const u8, - headers: ?std.http.Client.Request.Headers, + headers: std.http.Client.Request.Headers, ) ![]u8 { var writer = try std.Io.Writer.Allocating.initCapacity( self.allocator, @@ -79,7 +79,7 @@ const Client = struct { .location = .{ .url = url }, .response_writer = &writer.writer, .payload = body, - .headers = headers orelse .{}, + .headers = headers, }); return try writer.toOwnedSlice(); } @@ -96,7 +96,7 @@ pub fn main() !void { var client: Client = try .init(); defer client.deinit(); std.log.debug("{s}\n", .{ - try client.get("https://jstrieb.github.io", null), + try client.get("https://jstrieb.github.io", .{}), }); std.log.debug("{s}\n", .{ try client.post( From 73344b9c85a8f9661685ca9b167129ca78563451 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Thu, 12 Feb 2026 00:53:50 -0500 Subject: [PATCH 115/303] Move HTTP client into separate file --- src/http_client.zig | 66 ++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 70 ++------------------------------------------- 2 files changed, 69 insertions(+), 67 deletions(-) create mode 100644 src/http_client.zig diff --git a/src/http_client.zig b/src/http_client.zig new file mode 100644 index 00000000000..e30180239bd --- /dev/null +++ b/src/http_client.zig @@ -0,0 +1,66 @@ +//! Naive, unoptimized HTTP client with .get and .post methods. Simple, and not +//! particularly efficient. Response bodies stay allocated for the lifetime of +//! the client. + +const std = @import("std"); + +gpa: std.mem.Allocator, +arena: *std.heap.ArenaAllocator, +client: std.http.Client, + +const Self = @This(); + +pub fn init(allocator: std.mem.Allocator) !Self { + const arena = try allocator.create(std.heap.ArenaAllocator); + arena.* = std.heap.ArenaAllocator.init(allocator); + const a = arena.allocator(); + return .{ + .gpa = allocator, + .arena = arena, + .client = .{ .allocator = a }, + }; +} + +pub fn deinit(self: *Self) void { + self.client.deinit(); + self.arena.deinit(); + self.gpa.destroy(self.arena); +} + +pub fn get( + self: *Self, + url: []const u8, + headers: std.http.Client.Request.Headers, +) ![]u8 { + var writer = try std.Io.Writer.Allocating.initCapacity( + self.arena.allocator(), + 1024, + ); + defer writer.deinit(); + _ = try self.client.fetch(.{ + .location = .{ .url = url }, + .response_writer = &writer.writer, + .headers = headers, + }); + return try writer.toOwnedSlice(); +} + +pub fn post( + self: *Self, + url: []const u8, + body: []const u8, + headers: std.http.Client.Request.Headers, +) ![]u8 { + var writer = try std.Io.Writer.Allocating.initCapacity( + self.arena.allocator(), + 1024, + ); + defer writer.deinit(); + _ = try self.client.fetch(.{ + .location = .{ .url = url }, + .response_writer = &writer.writer, + .payload = body, + .headers = headers, + }); + return try writer.toOwnedSlice(); +} diff --git a/src/main.zig b/src/main.zig index 70167dc3b2e..83cf3c6a7f7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,8 @@ const builtin = @import("builtin"); const std = @import("std"); +const HttpClient = @import("http_client.zig"); + pub const std_options: std.Options = .{ .logFn = logFn, }; @@ -19,72 +21,6 @@ fn logFn( } } -/// Naive, unoptimized HTTP client with .get and .post methods. Simple, and not -/// particularly efficient. Response bodies stay allocated for the lifetime of -/// the client. -const Client = struct { - arena: *std.heap.ArenaAllocator, - allocator: std.mem.Allocator, - client: std.http.Client, - - const Self = @This(); - - pub fn init() !Self { - const arena = try allocator.create(std.heap.ArenaAllocator); - arena.* = std.heap.ArenaAllocator.init(allocator); - const a = arena.allocator(); - return .{ - .arena = arena, - .allocator = a, - .client = .{ .allocator = a }, - }; - } - - pub fn deinit(self: *Self) void { - self.client.deinit(); - self.arena.deinit(); - allocator.destroy(self.arena); - } - - pub fn get( - self: *Self, - url: []const u8, - headers: std.http.Client.Request.Headers, - ) ![]u8 { - var writer = try std.Io.Writer.Allocating.initCapacity( - self.allocator, - 1024, - ); - defer writer.deinit(); - _ = try self.client.fetch(.{ - .location = .{ .url = url }, - .response_writer = &writer.writer, - .headers = headers, - }); - return try writer.toOwnedSlice(); - } - - pub fn post( - self: *Self, - url: []const u8, - body: []const u8, - headers: std.http.Client.Request.Headers, - ) ![]u8 { - var writer = try std.Io.Writer.Allocating.initCapacity( - self.allocator, - 1024, - ); - defer writer.deinit(); - _ = try self.client.fetch(.{ - .location = .{ .url = url }, - .response_writer = &writer.writer, - .payload = body, - .headers = headers, - }); - return try writer.toOwnedSlice(); - } -}; - pub fn main() !void { var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; defer _ = gpa.deinit(); @@ -93,7 +29,7 @@ pub fn main() !void { // TODO: Parse environment variables // TODO: Parse CLI flags - var client: Client = try .init(); + var client: HttpClient = try .init(allocator); defer client.deinit(); std.log.debug("{s}\n", .{ try client.get("https://jstrieb.github.io", .{}), From b441bbb98047c7fa0ce60ef2ca05d818f0656076 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 13 Feb 2026 03:04:39 +0000 Subject: [PATCH 116/303] Update generated files --- generated/languages.svg | 20 ++++++++++---------- generated/overview.svg | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 02d63624f6b..f6875e3bef8 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.30% +29.26% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.71% +17.69% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -11.03% +11.16% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.41% +9.40% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.45% +7.44% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.67% +6.66% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.34% +5.33% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.41% +2.40% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.05% +2.04% diff --git a/generated/overview.svg b/generated/overview.svg index ccb386e32d5..99921e27edf 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,520 +Stars7,521 Forks1,162 All-time contributions4,543 -Lines of code changed2,528,286 +Lines of code changed2,778,269 -Repository views (past two weeks)1,572 +Repository views (past two weeks)1,596 Repositories with contributions128 From 0273def38d01f3c9e3dea2d641bc1fa5d53dc65b Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 14 Feb 2026 02:58:29 +0000 Subject: [PATCH 117/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 99921e27edf..108519e5f4e 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,521 +Stars7,518 -Forks1,162 +Forks1,159 All-time contributions4,543 Lines of code changed2,778,269 -Repository views (past two weeks)1,596 +Repository views (past two weeks)1,585 Repositories with contributions128 From 00d07b1e1e4656044bee0485df6a6d734007a4f6 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 15 Feb 2026 03:18:45 +0000 Subject: [PATCH 118/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 108519e5f4e..226ad699ef8 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,518 +Stars7,519 Forks1,159 All-time contributions4,543 -Lines of code changed2,778,269 +Lines of code changed2,777,357 -Repository views (past two weeks)1,585 +Repository views (past two weeks)1,499 Repositories with contributions128 From 754d55e4ff6c6be576f1fcb0eacac6ec3f941362 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 16 Feb 2026 03:04:07 +0000 Subject: [PATCH 119/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 226ad699ef8..a98824f6c4d 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,519 +Stars7,520 -Forks1,159 +Forks1,162 All-time contributions4,543 -Lines of code changed2,777,357 +Lines of code changed2,778,269 -Repository views (past two weeks)1,499 +Repository views (past two weeks)1,490 Repositories with contributions128 From 862328501afa9aafebd3a7ce78c8763852433e57 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 16 Feb 2026 18:25:31 -0500 Subject: [PATCH 120/303] Pull GitHub contribution years from GraphQL API --- src/http_client.zig | 25 +++++++++++++++++++++++- src/main.zig | 46 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/http_client.zig b/src/http_client.zig index e30180239bd..5485bac092c 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -7,10 +7,11 @@ const std = @import("std"); gpa: std.mem.Allocator, arena: *std.heap.ArenaAllocator, client: std.http.Client, +bearer: []const u8, const Self = @This(); -pub fn init(allocator: std.mem.Allocator) !Self { +pub fn init(allocator: std.mem.Allocator, token: []const u8) !Self { const arena = try allocator.create(std.heap.ArenaAllocator); arena.* = std.heap.ArenaAllocator.init(allocator); const a = arena.allocator(); @@ -18,6 +19,7 @@ pub fn init(allocator: std.mem.Allocator) !Self { .gpa = allocator, .arena = arena, .client = .{ .allocator = a }, + .bearer = try std.fmt.allocPrint(a, "Bearer {s}", .{token}), }; } @@ -64,3 +66,24 @@ pub fn post( }); return try writer.toOwnedSlice(); } + +const Query = struct { + query: []const u8, +}; + +pub fn graphql( + self: *Self, + body: []const u8, +) ![]u8 { + var arena = std.heap.ArenaAllocator.init(self.arena.allocator()); + defer arena.deinit(); + const allocator = arena.allocator(); + return try self.post( + "https://api.github.com/graphql", + try std.json.Stringify.valueAlloc(allocator, Query{ .query = body }, .{}), + .{ + .authorization = .{ .override = self.bearer }, + .content_type = .{ .override = "application/json" }, + }, + ); +} diff --git a/src/main.zig b/src/main.zig index 83cf3c6a7f7..bda0406a0ed 100644 --- a/src/main.zig +++ b/src/main.zig @@ -21,6 +21,34 @@ fn logFn( } } +fn years(client: *HttpClient) ![]u32 { + const response = try client.graphql( + \\query { + \\ viewer { + \\ contributionsCollection { + \\ contributionYears + \\ } + \\ } + \\} + ); + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const r = try std.json.parseFromSliceLeaky( + struct { data: struct { viewer: struct { + contributionsCollection: struct { + contributionYears: []u32, + }, + } } }, + arena.allocator(), + response, + .{ .ignore_unknown_fields = true }, + ); + return try allocator.dupe( + u32, + r.data.viewer.contributionsCollection.contributionYears, + ); +} + pub fn main() !void { var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; defer _ = gpa.deinit(); @@ -29,18 +57,14 @@ pub fn main() !void { // TODO: Parse environment variables // TODO: Parse CLI flags - var client: HttpClient = try .init(allocator); + var client: HttpClient = try .init( + allocator, + "TODO", + ); defer client.deinit(); - std.log.debug("{s}\n", .{ - try client.get("https://jstrieb.github.io", .{}), - }); - std.log.debug("{s}\n", .{ - try client.post( - "https://httpbin.org/post", - "{\"a\": 10, \"b\": [ 1, 2, 3 ]}", - .{ .content_type = .{ .override = "application/json" } }, - ), - }); + const y = try years(&client); + defer allocator.free(y); + std.debug.print("{any}\n", .{y}); // TODO: Download statistics to populate data structures // TODO: Output images from templates From 322b0bdf758534e766e239ddced5699bad6aa620 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 16 Feb 2026 18:26:17 -0500 Subject: [PATCH 121/303] Pull initial repo stats --- src/http_client.zig | 7 +- src/main.zig | 206 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 198 insertions(+), 15 deletions(-) diff --git a/src/http_client.zig b/src/http_client.zig index 5485bac092c..fcdcfe950d2 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -69,18 +69,23 @@ pub fn post( const Query = struct { query: []const u8, + variables: ?[]const u8, }; pub fn graphql( self: *Self, body: []const u8, + variables: ?[]const u8, ) ![]u8 { var arena = std.heap.ArenaAllocator.init(self.arena.allocator()); defer arena.deinit(); const allocator = arena.allocator(); return try self.post( "https://api.github.com/graphql", - try std.json.Stringify.valueAlloc(allocator, Query{ .query = body }, .{}), + try std.json.Stringify.valueAlloc(allocator, Query{ + .query = body, + .variables = variables, + }, .{}), .{ .authorization = .{ .override = self.bearer }, .content_type = .{ .override = "application/json" }, diff --git a/src/main.zig b/src/main.zig index bda0406a0ed..2cf4ccc8668 100644 --- a/src/main.zig +++ b/src/main.zig @@ -21,8 +21,51 @@ fn logFn( } } -fn years(client: *HttpClient) ![]u32 { - const response = try client.graphql( +const Language = struct { + name: []const u8, + size: u32, + color: []const u8, + + pub fn deinit(self: @This()) void { + allocator.free(self.name); + allocator.free(self.color); + } +}; + +const Repository = struct { + name: []const u8, + stars: u32, + forks: u32, + languages: ?[]Language, + + pub fn deinit(self: @This()) void { + allocator.free(self.name); + if (self.languages) |languages| { + for (languages) |language| { + language.deinit(); + } + allocator.free(languages); + } + } +}; + +const Statistics = struct { + contributions: u32, + repositories: []Repository, + + pub fn deinit(self: @This()) void { + for (self.repositories) |repository| { + repository.deinit(); + } + allocator.free(self.repositories); + } +}; + +fn get_repos(client: *HttpClient) !Statistics { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + var response = try client.graphql( \\query { \\ viewer { \\ contributionsCollection { @@ -30,10 +73,8 @@ fn years(client: *HttpClient) ![]u32 { \\ } \\ } \\} - ); - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - const r = try std.json.parseFromSliceLeaky( + , null); + const years = (try std.json.parseFromSliceLeaky( struct { data: struct { viewer: struct { contributionsCollection: struct { contributionYears: []u32, @@ -42,11 +83,132 @@ fn years(client: *HttpClient) ![]u32 { arena.allocator(), response, .{ .ignore_unknown_fields = true }, - ); - return try allocator.dupe( - u32, - r.data.viewer.contributionsCollection.contributionYears, - ); + )).data.viewer.contributionsCollection.contributionYears; + + var result: Statistics = .{ + .contributions = 0, + .repositories = undefined, + }; + var repositories: std.ArrayList(Repository) = try .initCapacity(allocator, 32); + var seen: std.StringHashMap(bool) = .init(arena.allocator()); + defer seen.deinit(); + + for (years) |year| { + response = try client.graphql( + \\query ($from: DateTime, $to: DateTime) { + \\ viewer { + \\ contributionsCollection(from: $from, to: $to) { + \\ totalRepositoryContributions + \\ totalIssueContributions + \\ totalCommitContributions + \\ totalPullRequestContributions + \\ totalPullRequestReviewContributions + \\ commitContributionsByRepository(maxRepositories: 100) { + \\ repository { + \\ nameWithOwner + \\ stargazerCount + \\ forkCount + \\ languages(first: 100, orderBy: { direction: DESC, field: SIZE }) { + \\ edges { + \\ size + \\ node { + \\ name + \\ color + \\ } + \\ } + \\ } + \\ } + \\ } + \\ } + \\ } + \\} + , + // NOTE: Replace with actual JSON serialization if using more + // complex tyeps. This is fine as long as we're only using numbers. + try std.fmt.allocPrint( + arena.allocator(), + \\{{ + \\ "from": "{d}-01-01T00:00:00Z", + \\ "to": "{d}-01-01T00:00:00Z" + \\}} + , + .{ year, year + 1 }, + ), + ); + const stats = (try std.json.parseFromSliceLeaky( + struct { data: struct { viewer: struct { + contributionsCollection: struct { + totalRepositoryContributions: u32, + totalIssueContributions: u32, + totalCommitContributions: u32, + totalPullRequestContributions: u32, + totalPullRequestReviewContributions: u32, + commitContributionsByRepository: []struct { + repository: struct { + nameWithOwner: []const u8, + stargazerCount: u32, + forkCount: u32, + languages: ?struct { + edges: ?[]struct { + size: u32, + node: struct { + name: []const u8, + color: ?[]const u8, + }, + }, + }, + }, + }, + }, + } } }, + arena.allocator(), + response, + .{ .ignore_unknown_fields = true }, + )).data.viewer.contributionsCollection; + + result.contributions += stats.totalRepositoryContributions; + result.contributions += stats.totalIssueContributions; + result.contributions += stats.totalCommitContributions; + result.contributions += stats.totalPullRequestContributions; + result.contributions += stats.totalPullRequestReviewContributions; + + // TODO: if there are 100 ore more repositories, we should subdivide + // the date range in half + + for (stats.commitContributionsByRepository) |x| { + const raw_repo = x.repository; + if (seen.get(raw_repo.nameWithOwner) orelse false) continue; + var repository = Repository{ + .name = try allocator.dupe(u8, raw_repo.nameWithOwner), + .stars = raw_repo.stargazerCount, + .forks = raw_repo.forkCount, + .languages = null, + }; + if (raw_repo.languages) |repo_languages| { + if (repo_languages.edges) |raw_languages| { + repository.languages = try allocator.alloc( + Language, + raw_languages.len, + ); + for (raw_languages, repository.languages.?) |raw, *language| { + language.* = .{ + .name = try allocator.dupe(u8, raw.node.name), + .size = raw.size, + .color = "", + }; + if (raw.node.color) |color| { + language.color = try allocator.dupe(u8, color); + } + } + } + } + try repositories.append(allocator, repository); + try seen.put(raw_repo.nameWithOwner, true); + } + } + + result.repositories = try repositories.toOwnedSlice(allocator); + return result; } pub fn main() !void { @@ -62,10 +224,26 @@ pub fn main() !void { "TODO", ); defer client.deinit(); - const y = try years(&client); - defer allocator.free(y); - std.debug.print("{any}\n", .{y}); + const stats = try get_repos(&client); + defer stats.deinit(); + print(stats); // TODO: Download statistics to populate data structures // TODO: Output images from templates } + +// TODO: Remove +fn print(x: anytype) void { + if (builtin.mode != .Debug) { + @compileError("Do not use JSON print in real code!"); + } + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + std.debug.print("{s}\n", .{ + std.json.Stringify.valueAlloc( + arena.allocator(), + x, + .{ .whitespace = .indent_2 }, + ) catch unreachable, + }); +} From b758e3ad515db384d3291fb4e4d6824f958d7c6e Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 16 Feb 2026 18:26:51 -0500 Subject: [PATCH 122/303] Minor refactor --- src/main.zig | 69 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/src/main.zig b/src/main.zig index 2cf4ccc8668..bcf79cb0e1d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -53,48 +53,63 @@ const Statistics = struct { contributions: u32, repositories: []Repository, - pub fn deinit(self: @This()) void { + const Self = @This(); + + pub const empty = Self{ + .contributions = 0, + .repositories = undefined, + }; + + pub fn deinit(self: Self) void { for (self.repositories) |repository| { repository.deinit(); } allocator.free(self.repositories); } + + pub fn years(client: *HttpClient, alloc: std.mem.Allocator) ![]u32 { + const response = try client.graphql( + \\query { + \\ viewer { + \\ contributionsCollection { + \\ contributionYears + \\ } + \\ } + \\} + , null); + const parsed = try std.json.parseFromSliceLeaky( + struct { + data: struct { + viewer: struct { + contributionsCollection: struct { + contributionYears: []u32, + }, + }, + }, + }, + alloc, + response, + .{ .ignore_unknown_fields = true }, + ); + return parsed + .data + .viewer + .contributionsCollection + .contributionYears; + } }; fn get_repos(client: *HttpClient) !Statistics { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); - var response = try client.graphql( - \\query { - \\ viewer { - \\ contributionsCollection { - \\ contributionYears - \\ } - \\ } - \\} - , null); - const years = (try std.json.parseFromSliceLeaky( - struct { data: struct { viewer: struct { - contributionsCollection: struct { - contributionYears: []u32, - }, - } } }, - arena.allocator(), - response, - .{ .ignore_unknown_fields = true }, - )).data.viewer.contributionsCollection.contributionYears; - - var result: Statistics = .{ - .contributions = 0, - .repositories = undefined, - }; + var result: Statistics = .empty; var repositories: std.ArrayList(Repository) = try .initCapacity(allocator, 32); var seen: std.StringHashMap(bool) = .init(arena.allocator()); defer seen.deinit(); - for (years) |year| { - response = try client.graphql( + for (try Statistics.years(client, arena.allocator())) |year| { + const response = try client.graphql( \\query ($from: DateTime, $to: DateTime) { \\ viewer { \\ contributionsCollection(from: $from, to: $to) { From 8f8b1cd7be33f19ac60f8cf25e58c562930b2ce4 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 17 Feb 2026 03:27:20 +0000 Subject: [PATCH 123/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index a98824f6c4d..f094c0a72c2 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,520 +Stars7,523 -Forks1,162 +Forks1,163 All-time contributions4,543 -Lines of code changed2,778,269 +Lines of code changed2,775,602 -Repository views (past two weeks)1,490 +Repository views (past two weeks)1,448 Repositories with contributions128 From aba93f39cc5f7d7fec76f2b814bf3c3e33752cbe Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 18 Feb 2026 03:07:10 +0000 Subject: [PATCH 124/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index f094c0a72c2..934cfd2ce9d 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,523 +Stars7,524 Forks1,163 All-time contributions4,543 -Lines of code changed2,775,602 +Lines of code changed2,778,269 -Repository views (past two weeks)1,448 +Repository views (past two weeks)1,446 Repositories with contributions128 From 5974224102ed9562b2e2bc8cf0113eae4007b80e Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 18 Feb 2026 16:52:51 -0500 Subject: [PATCH 125/303] Pull repo views --- src/http_client.zig | 35 +++++++++++++++++++++++-------- src/main.zig | 51 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 15 deletions(-) diff --git a/src/http_client.zig b/src/http_client.zig index fcdcfe950d2..a74551b72bf 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -10,6 +10,7 @@ client: std.http.Client, bearer: []const u8, const Self = @This(); +const Response = struct { []const u8, std.http.Status }; pub fn init(allocator: std.mem.Allocator, token: []const u8) !Self { const arena = try allocator.create(std.heap.ArenaAllocator); @@ -33,18 +34,20 @@ pub fn get( self: *Self, url: []const u8, headers: std.http.Client.Request.Headers, -) ![]u8 { + extra_headers: []const std.http.Header, +) !Response { var writer = try std.Io.Writer.Allocating.initCapacity( self.arena.allocator(), 1024, ); defer writer.deinit(); - _ = try self.client.fetch(.{ + const status = (try self.client.fetch(.{ .location = .{ .url = url }, .response_writer = &writer.writer, .headers = headers, - }); - return try writer.toOwnedSlice(); + .extra_headers = extra_headers, + })).status; + return .{ try writer.toOwnedSlice(), status }; } pub fn post( @@ -52,19 +55,19 @@ pub fn post( url: []const u8, body: []const u8, headers: std.http.Client.Request.Headers, -) ![]u8 { +) !Response { var writer = try std.Io.Writer.Allocating.initCapacity( self.arena.allocator(), 1024, ); defer writer.deinit(); - _ = try self.client.fetch(.{ + const status = (try self.client.fetch(.{ .location = .{ .url = url }, .response_writer = &writer.writer, .payload = body, .headers = headers, - }); - return try writer.toOwnedSlice(); + })).status; + return .{ try writer.toOwnedSlice(), status }; } const Query = struct { @@ -76,7 +79,7 @@ pub fn graphql( self: *Self, body: []const u8, variables: ?[]const u8, -) ![]u8 { +) !Response { var arena = std.heap.ArenaAllocator.init(self.arena.allocator()); defer arena.deinit(); const allocator = arena.allocator(); @@ -92,3 +95,17 @@ pub fn graphql( }, ); } + +pub fn rest( + self: *Self, + url: []const u8, +) !Response { + return try self.get( + url, + .{ + .authorization = .{ .override = self.bearer }, + .content_type = .{ .override = "application/json" }, + }, + &.{.{ .name = "X-GitHub-Api-Version", .value = "2022-11-28" }}, + ); +} diff --git a/src/main.zig b/src/main.zig index bcf79cb0e1d..ef62ef0340e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -37,6 +37,7 @@ const Repository = struct { stars: u32, forks: u32, languages: ?[]Language, + views: u32, pub fn deinit(self: @This()) void { allocator.free(self.name); @@ -68,7 +69,7 @@ const Statistics = struct { } pub fn years(client: *HttpClient, alloc: std.mem.Allocator) ![]u32 { - const response = try client.graphql( + const response, const status = try client.graphql( \\query { \\ viewer { \\ contributionsCollection { @@ -77,6 +78,7 @@ const Statistics = struct { \\ } \\} , null); + if (status != .ok) return error.RequestFailed; const parsed = try std.json.parseFromSliceLeaky( struct { data: struct { @@ -104,12 +106,13 @@ fn get_repos(client: *HttpClient) !Statistics { defer arena.deinit(); var result: Statistics = .empty; - var repositories: std.ArrayList(Repository) = try .initCapacity(allocator, 32); + var repositories: std.ArrayList(Repository) = + try .initCapacity(allocator, 32); var seen: std.StringHashMap(bool) = .init(arena.allocator()); defer seen.deinit(); for (try Statistics.years(client, arena.allocator())) |year| { - const response = try client.graphql( + var response, var status = try client.graphql( \\query ($from: DateTime, $to: DateTime) { \\ viewer { \\ contributionsCollection(from: $from, to: $to) { @@ -123,7 +126,10 @@ fn get_repos(client: *HttpClient) !Statistics { \\ nameWithOwner \\ stargazerCount \\ forkCount - \\ languages(first: 100, orderBy: { direction: DESC, field: SIZE }) { + \\ languages( + \\ first: 100, + \\ orderBy: { direction: DESC, field: SIZE } + \\ ) { \\ edges { \\ size \\ node { @@ -150,6 +156,7 @@ fn get_repos(client: *HttpClient) !Statistics { .{ year, year + 1 }, ), ); + if (status != .ok) return error.RequestFailed; const stats = (try std.json.parseFromSliceLeaky( struct { data: struct { viewer: struct { contributionsCollection: struct { @@ -198,6 +205,7 @@ fn get_repos(client: *HttpClient) !Statistics { .stars = raw_repo.stargazerCount, .forks = raw_repo.forkCount, .languages = null, + .views = 0, }; if (raw_repo.languages) |repo_languages| { if (repo_languages.edges) |raw_languages| { @@ -205,7 +213,10 @@ fn get_repos(client: *HttpClient) !Statistics { Language, raw_languages.len, ); - for (raw_languages, repository.languages.?) |raw, *language| { + for ( + raw_languages, + repository.languages.?, + ) |raw, *language| { language.* = .{ .name = try allocator.dupe(u8, raw.node.name), .size = raw.size, @@ -217,12 +228,40 @@ fn get_repos(client: *HttpClient) !Statistics { } } } + response, status = try client.rest( + try std.mem.concat( + arena.allocator(), + u8, + &.{ + "https://api.github.com/repos/", + raw_repo.nameWithOwner, + "/traffic/views", + }, + ), + ); + if (status == .ok) { + repository.views = (try std.json.parseFromSliceLeaky( + struct { count: u32 }, + arena.allocator(), + response, + .{ .ignore_unknown_fields = true }, + )).count; + } try repositories.append(allocator, repository); try seen.put(raw_repo.nameWithOwner, true); } } result.repositories = try repositories.toOwnedSlice(allocator); + std.sort.pdq(Repository, result.repositories, {}, struct { + pub fn lessThanFn(_: void, lhs: Repository, rhs: Repository) bool { + if (rhs.views == lhs.views) { + return rhs.stars + rhs.forks < lhs.stars + lhs.forks; + } + return rhs.views < lhs.views; + } + }.lessThanFn); + return result; } @@ -236,7 +275,7 @@ pub fn main() !void { var client: HttpClient = try .init( allocator, - "TODO", + "", ); defer client.deinit(); const stats = try get_repos(&client); From a6192d5ca2faa371609e90804dc8b68c8aaae7b1 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 18 Feb 2026 18:41:53 -0500 Subject: [PATCH 126/303] Add basic logging --- src/main.zig | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/main.zig b/src/main.zig index ef62ef0340e..3c2ffafc64f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -7,7 +7,10 @@ pub const std_options: std.Options = .{ .logFn = logFn, }; -var log_level = std.log.default_level; +var log_level: std.log.Level = switch (builtin.mode) { + .Debug => .debug, + else => .warn, +}; var allocator: std.mem.Allocator = undefined; fn logFn( @@ -69,6 +72,7 @@ const Statistics = struct { } pub fn years(client: *HttpClient, alloc: std.mem.Allocator) ![]u32 { + std.log.info("Getting contribution years...", .{}); const response, const status = try client.graphql( \\query { \\ viewer { @@ -78,7 +82,10 @@ const Statistics = struct { \\ } \\} , null); - if (status != .ok) return error.RequestFailed; + if (status != .ok) { + std.log.err("Failed to get contribution years ({any})", .{status}); + return error.RequestFailed; + } const parsed = try std.json.parseFromSliceLeaky( struct { data: struct { @@ -112,6 +119,7 @@ fn get_repos(client: *HttpClient) !Statistics { defer seen.deinit(); for (try Statistics.years(client, arena.allocator())) |year| { + std.log.info("Getting data from year {d}...", .{year}); var response, var status = try client.graphql( \\query ($from: DateTime, $to: DateTime) { \\ viewer { @@ -156,7 +164,13 @@ fn get_repos(client: *HttpClient) !Statistics { .{ year, year + 1 }, ), ); - if (status != .ok) return error.RequestFailed; + if (status != .ok) { + std.log.err( + "Failed to get data from year {d} ({any})", + .{ year, status }, + ); + return error.RequestFailed; + } const stats = (try std.json.parseFromSliceLeaky( struct { data: struct { viewer: struct { contributionsCollection: struct { @@ -187,6 +201,10 @@ fn get_repos(client: *HttpClient) !Statistics { response, .{ .ignore_unknown_fields = true }, )).data.viewer.contributionsCollection; + std.log.info( + "Parsed data for {d} total repositories in {d}", + .{ stats.commitContributionsByRepository.len, year }, + ); result.contributions += stats.totalRepositoryContributions; result.contributions += stats.totalIssueContributions; @@ -199,7 +217,13 @@ fn get_repos(client: *HttpClient) !Statistics { for (stats.commitContributionsByRepository) |x| { const raw_repo = x.repository; - if (seen.get(raw_repo.nameWithOwner) orelse false) continue; + if (seen.get(raw_repo.nameWithOwner) orelse false) { + std.log.info( + "Skipping {s} (seen)", + .{raw_repo.nameWithOwner}, + ); + continue; + } var repository = Repository{ .name = try allocator.dupe(u8, raw_repo.nameWithOwner), .stars = raw_repo.stargazerCount, @@ -228,6 +252,10 @@ fn get_repos(client: *HttpClient) !Statistics { } } } + std.log.info( + "Getting views for {s}...", + .{raw_repo.nameWithOwner}, + ); response, status = try client.rest( try std.mem.concat( arena.allocator(), @@ -246,6 +274,11 @@ fn get_repos(client: *HttpClient) !Statistics { response, .{ .ignore_unknown_fields = true }, )).count; + } else { + std.log.warn( + "Failed to get views for {s} ({any})", + .{ raw_repo.nameWithOwner, status }, + ); } try repositories.append(allocator, repository); try seen.put(raw_repo.nameWithOwner, true); From c081a836a89808d5a4d1824838441487c659c1ea Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 19 Feb 2026 02:50:58 +0000 Subject: [PATCH 127/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 934cfd2ce9d..ff11c5d75fe 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,524 +Stars7,525 -Forks1,163 +Forks1,162 All-time contributions4,543 Lines of code changed2,778,269 -Repository views (past two weeks)1,446 +Repository views (past two weeks)1,430 Repositories with contributions128 From 2b9e70bd171a906e35212d402b714ab726bc5a29 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Thu, 19 Feb 2026 12:02:42 -0500 Subject: [PATCH 128/303] Get lines changed --- src/main.zig | 110 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 101 insertions(+), 9 deletions(-) diff --git a/src/main.zig b/src/main.zig index 3c2ffafc64f..c4e122af435 100644 --- a/src/main.zig +++ b/src/main.zig @@ -12,6 +12,7 @@ var log_level: std.log.Level = switch (builtin.mode) { else => .warn, }; var allocator: std.mem.Allocator = undefined; +var user: []const u8 = undefined; fn logFn( comptime message_level: std.log.Level, @@ -41,6 +42,7 @@ const Repository = struct { forks: u32, languages: ?[]Language, views: u32, + lines_changed: u32, pub fn deinit(self: @This()) void { allocator.free(self.name); @@ -83,7 +85,10 @@ const Statistics = struct { \\} , null); if (status != .ok) { - std.log.err("Failed to get contribution years ({any})", .{status}); + std.log.err( + "Failed to get contribution years ({?s})", + .{status.phrase()}, + ); return error.RequestFailed; } const parsed = try std.json.parseFromSliceLeaky( @@ -119,7 +124,7 @@ fn get_repos(client: *HttpClient) !Statistics { defer seen.deinit(); for (try Statistics.years(client, arena.allocator())) |year| { - std.log.info("Getting data from year {d}...", .{year}); + std.log.info("Getting data from {d}...", .{year}); var response, var status = try client.graphql( \\query ($from: DateTime, $to: DateTime) { \\ viewer { @@ -166,8 +171,8 @@ fn get_repos(client: *HttpClient) !Statistics { ); if (status != .ok) { std.log.err( - "Failed to get data from year {d} ({any})", - .{ year, status }, + "Failed to get data from {d} ({?s})", + .{ year, status.phrase() }, ); return error.RequestFailed; } @@ -202,7 +207,7 @@ fn get_repos(client: *HttpClient) !Statistics { .{ .ignore_unknown_fields = true }, )).data.viewer.contributionsCollection; std.log.info( - "Parsed data for {d} total repositories in {d}", + "Parsed {d} total repositories from {d}", .{ stats.commitContributionsByRepository.len, year }, ); @@ -219,7 +224,7 @@ fn get_repos(client: *HttpClient) !Statistics { const raw_repo = x.repository; if (seen.get(raw_repo.nameWithOwner) orelse false) { std.log.info( - "Skipping {s} (seen)", + "Skipping view count for {s} (seen)", .{raw_repo.nameWithOwner}, ); continue; @@ -230,6 +235,7 @@ fn get_repos(client: *HttpClient) !Statistics { .forks = raw_repo.forkCount, .languages = null, .views = 0, + .lines_changed = 0, }; if (raw_repo.languages) |repo_languages| { if (repo_languages.edges) |raw_languages| { @@ -276,8 +282,8 @@ fn get_repos(client: *HttpClient) !Statistics { )).count; } else { std.log.warn( - "Failed to get views for {s} ({any})", - .{ raw_repo.nameWithOwner, status }, + "Failed to get views for {s} ({?s})", + .{ raw_repo.nameWithOwner, status.phrase() }, ); } try repositories.append(allocator, repository); @@ -295,6 +301,92 @@ fn get_repos(client: *HttpClient) !Statistics { } }.lessThanFn); + const T = struct { + repo: *Repository, + delay: i64, + timestamp: i64, + }; + var q: std.PriorityQueue(T, void, struct { + pub fn compareFn(_: void, lhs: T, rhs: T) std.math.Order { + return std.math.order(lhs.timestamp, rhs.timestamp); + } + }.compareFn) = .init(arena.allocator(), {}); + defer q.deinit(); + for (result.repositories) |*repo| { + try q.add(.{ + .repo = repo, + .delay = 16, + .timestamp = std.time.timestamp(), + }); + } + while (q.count() > 0) { + var item = q.remove(); + const now = std.time.timestamp(); + if (item.timestamp > now) { + std.Thread.sleep( + @as(u64, @intCast( + item.timestamp - now, + )) * std.time.ns_per_s, + ); + } + std.log.info( + "Trying to get lines of code changed for {s}...", + .{item.repo.name}, + ); + const response, const status = try client.rest( + try std.mem.concat( + arena.allocator(), + u8, + &.{ + "https://api.github.com/repos/", + item.repo.name, + "/stats/contributors", + }, + ), + ); + switch (status) { + .ok => { + const authors = (try std.json.parseFromSliceLeaky( + []struct { + author: struct { login: []const u8 }, + weeks: []struct { + a: u32, + d: u32, + }, + }, + arena.allocator(), + response, + .{ .ignore_unknown_fields = true }, + )); + for (authors) |o| { + if (!std.mem.eql(u8, o.author.login, user)) { + continue; + } + for (o.weeks) |week| { + item.repo.lines_changed += week.a; + item.repo.lines_changed += week.d; + } + } + std.log.info( + "Got {d} lines changed by {s} in {s}", + .{ item.repo.lines_changed, user, item.repo.name }, + ); + }, + .accepted => { + item.timestamp = std.time.timestamp() + item.delay; + item.delay *= 2; + try q.add(item); + }, + else => { + std.log.err( + "Failed to get contribution data for {s} ({?s})", + .{ item.repo.name, status.phrase() }, + ); + return error.RequestFailed; + }, + } + } + return result; } @@ -310,12 +402,12 @@ pub fn main() !void { allocator, "", ); + user = ""; defer client.deinit(); const stats = try get_repos(&client); defer stats.deinit(); print(stats); - // TODO: Download statistics to populate data structures // TODO: Output images from templates } From 8900d4dc7a8a9165a5bf6435b8f5cd4ce712fe8a Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 20 Feb 2026 02:58:16 +0000 Subject: [PATCH 129/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index ff11c5d75fe..230f2ac6d9f 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,525 +Stars7,527 Forks1,162 -All-time contributions4,543 +All-time contributions4,547 -Lines of code changed2,778,269 +Lines of code changed2,778,314 -Repository views (past two weeks)1,430 +Repository views (past two weeks)1,400 Repositories with contributions128 From e1710b97b03405178950e4389f2870d66be1bc36 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 21 Feb 2026 02:56:20 +0000 Subject: [PATCH 130/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 230f2ac6d9f..1a34aa86424 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,527 +Stars7,526 -Forks1,162 +Forks1,161 All-time contributions4,547 -Lines of code changed2,778,314 +Lines of code changed2,778,129 -Repository views (past two weeks)1,400 +Repository views (past two weeks)1,384 Repositories with contributions128 From 25035562b73abdd8201d0b3803283abfceb0d6fa Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 22 Feb 2026 03:03:40 +0000 Subject: [PATCH 131/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 1a34aa86424..9fc40f134e7 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,526 +Stars7,527 Forks1,161 All-time contributions4,547 -Lines of code changed2,778,129 +Lines of code changed2,778,314 -Repository views (past two weeks)1,384 +Repository views (past two weeks)1,408 Repositories with contributions128 From 3bcfea36a9268c4f21f188fe745ba776f9c33e85 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 23 Feb 2026 03:07:14 +0000 Subject: [PATCH 132/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 9fc40f134e7..e15381102ba 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,7 +91,7 @@ tr { -Stars7,527 +Stars7,528 Forks1,161 @@ -99,7 +99,7 @@ tr { Lines of code changed2,778,314 -Repository views (past two weeks)1,408 +Repository views (past two weeks)1,418 Repositories with contributions128 From b6ef8ff5bfe28ca798b07af46788d80ac44cf443 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 23 Feb 2026 21:35:38 -0500 Subject: [PATCH 133/303] Deallocate correctly on errors in get_repos --- src/main.zig | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/main.zig b/src/main.zig index c4e122af435..32bd0cca593 100644 --- a/src/main.zig +++ b/src/main.zig @@ -56,21 +56,18 @@ const Repository = struct { }; const Statistics = struct { - contributions: u32, - repositories: []Repository, + contributions: u32 = 0, + repositories: ?[]Repository = null, const Self = @This(); - pub const empty = Self{ - .contributions = 0, - .repositories = undefined, - }; - pub fn deinit(self: Self) void { - for (self.repositories) |repository| { - repository.deinit(); + if (self.repositories) |repositories| { + for (repositories) |repository| { + repository.deinit(); + } + allocator.free(repositories); } - allocator.free(self.repositories); } pub fn years(client: *HttpClient, alloc: std.mem.Allocator) ![]u32 { @@ -117,9 +114,19 @@ fn get_repos(client: *HttpClient) !Statistics { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); - var result: Statistics = .empty; + var result: Statistics = .{}; var repositories: std.ArrayList(Repository) = try .initCapacity(allocator, 32); + errdefer { + if (result.repositories) |_| { + result.deinit(); + } else { + for (repositories.items) |repo| { + repo.deinit(); + } + repositories.deinit(allocator); + } + } var seen: std.StringHashMap(bool) = .init(arena.allocator()); defer seen.deinit(); @@ -292,7 +299,7 @@ fn get_repos(client: *HttpClient) !Statistics { } result.repositories = try repositories.toOwnedSlice(allocator); - std.sort.pdq(Repository, result.repositories, {}, struct { + std.sort.pdq(Repository, result.repositories.?, {}, struct { pub fn lessThanFn(_: void, lhs: Repository, rhs: Repository) bool { if (rhs.views == lhs.views) { return rhs.stars + rhs.forks < lhs.stars + lhs.forks; @@ -312,7 +319,7 @@ fn get_repos(client: *HttpClient) !Statistics { } }.compareFn) = .init(arena.allocator(), {}); defer q.deinit(); - for (result.repositories) |*repo| { + for (result.repositories.?) |*repo| { try q.add(.{ .repo = repo, .delay = 16, From 6a59cde4669eeb62aacdd442b789580ae425dda5 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 24 Feb 2026 03:21:06 +0000 Subject: [PATCH 134/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index e15381102ba..c9f97046bf8 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,528 +Stars7,530 Forks1,161 All-time contributions4,547 -Lines of code changed2,778,314 +Lines of code changed2,777,018 -Repository views (past two weeks)1,418 +Repository views (past two weeks)1,396 Repositories with contributions128 From 312e45d8ed9f33548988efb54f2493713c9e1cb2 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Feb 2026 01:56:54 -0500 Subject: [PATCH 135/303] Work around keep alive timeouts --- src/http_client.zig | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/http_client.zig b/src/http_client.zig index a74551b72bf..4d1df4dbf31 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -8,9 +8,11 @@ gpa: std.mem.Allocator, arena: *std.heap.ArenaAllocator, client: std.http.Client, bearer: []const u8, +last_request: ?i64 = null, const Self = @This(); const Response = struct { []const u8, std.http.Status }; +const KEEP_ALIVE_TIMEOUT: i64 = 16; pub fn init(allocator: std.mem.Allocator, token: []const u8) !Self { const arena = try allocator.create(std.heap.ArenaAllocator); @@ -41,12 +43,20 @@ pub fn get( 1024, ); defer writer.deinit(); + const now = std.time.timestamp(); const status = (try self.client.fetch(.{ .location = .{ .url = url }, .response_writer = &writer.writer, .headers = headers, .extra_headers = extra_headers, + // Work around failures from keep alive connections closing after + // timeout and not being automatically reopened by Zig + .keep_alive = if (self.last_request) |last| + now - last > KEEP_ALIVE_TIMEOUT + else + true, })).status; + self.last_request = now; return .{ try writer.toOwnedSlice(), status }; } @@ -61,12 +71,20 @@ pub fn post( 1024, ); defer writer.deinit(); + const now = std.time.timestamp(); const status = (try self.client.fetch(.{ .location = .{ .url = url }, .response_writer = &writer.writer, .payload = body, .headers = headers, + // Work around failures from keep alive connections closing after + // timeout and not being automatically reopened by Zig + .keep_alive = if (self.last_request) |last| + now - last > KEEP_ALIVE_TIMEOUT + else + true, })).status; + self.last_request = now; return .{ try writer.toOwnedSlice(), status }; } From 82c4407b3240bcb2db5ceee6315eceaa99ff19ae Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Feb 2026 01:57:32 -0500 Subject: [PATCH 136/303] Tweak API request delay values --- src/main.zig | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main.zig b/src/main.zig index 32bd0cca593..fbc8473addf 100644 --- a/src/main.zig +++ b/src/main.zig @@ -322,7 +322,7 @@ fn get_repos(client: *HttpClient) !Statistics { for (result.repositories.?) |*repo| { try q.add(.{ .repo = repo, - .delay = 16, + .delay = 2, .timestamp = std.time.timestamp(), }); } @@ -330,6 +330,10 @@ fn get_repos(client: *HttpClient) !Statistics { var item = q.remove(); const now = std.time.timestamp(); if (item.timestamp > now) { + std.log.debug("Sleeping for {d}s. Waiting for {d} repos.", .{ + item.timestamp - now, + q.count() + 1, + }); std.Thread.sleep( @as(u64, @intCast( item.timestamp - now, @@ -381,7 +385,8 @@ fn get_repos(client: *HttpClient) !Statistics { }, .accepted => { item.timestamp = std.time.timestamp() + item.delay; - item.delay *= 2; + const _delay: f64 = @floatFromInt(item.delay); + item.delay = @intFromFloat(_delay * 1.5); try q.add(item); }, else => { From ed4cca88e727d004d785a71c3eadbc3d5419b7a9 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Feb 2026 10:43:50 -0500 Subject: [PATCH 137/303] Partial refactor to split up big function --- src/main.zig | 72 +++++++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/main.zig b/src/main.zig index fbc8473addf..bcabf3babae 100644 --- a/src/main.zig +++ b/src/main.zig @@ -56,18 +56,16 @@ const Repository = struct { }; const Statistics = struct { + repositories: []Repository, contributions: u32 = 0, - repositories: ?[]Repository = null, const Self = @This(); pub fn deinit(self: Self) void { - if (self.repositories) |repositories| { - for (repositories) |repository| { - repository.deinit(); - } - allocator.free(repositories); + for (self.repositories) |repository| { + repository.deinit(); } + allocator.free(self.repositories); } pub fn years(client: *HttpClient, alloc: std.mem.Allocator) ![]u32 { @@ -110,22 +108,18 @@ const Statistics = struct { } }; -fn get_repos(client: *HttpClient) !Statistics { - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - - var result: Statistics = .{}; +fn repo_list( + arena: *std.heap.ArenaAllocator, + client: *HttpClient, +) !struct { u32, []Repository } { + var contributions: u32 = 0; var repositories: std.ArrayList(Repository) = try .initCapacity(allocator, 32); errdefer { - if (result.repositories) |_| { - result.deinit(); - } else { - for (repositories.items) |repo| { - repo.deinit(); - } - repositories.deinit(allocator); + for (repositories.items) |repo| { + repo.deinit(); } + repositories.deinit(allocator); } var seen: std.StringHashMap(bool) = .init(arena.allocator()); defer seen.deinit(); @@ -218,11 +212,11 @@ fn get_repos(client: *HttpClient) !Statistics { .{ stats.commitContributionsByRepository.len, year }, ); - result.contributions += stats.totalRepositoryContributions; - result.contributions += stats.totalIssueContributions; - result.contributions += stats.totalCommitContributions; - result.contributions += stats.totalPullRequestContributions; - result.contributions += stats.totalPullRequestReviewContributions; + contributions += stats.totalRepositoryContributions; + contributions += stats.totalIssueContributions; + contributions += stats.totalCommitContributions; + contributions += stats.totalPullRequestContributions; + contributions += stats.totalPullRequestReviewContributions; // TODO: if there are 100 ore more repositories, we should subdivide // the date range in half @@ -288,7 +282,7 @@ fn get_repos(client: *HttpClient) !Statistics { .{ .ignore_unknown_fields = true }, )).count; } else { - std.log.warn( + std.log.info( "Failed to get views for {s} ({?s})", .{ raw_repo.nameWithOwner, status.phrase() }, ); @@ -298,8 +292,8 @@ fn get_repos(client: *HttpClient) !Statistics { } } - result.repositories = try repositories.toOwnedSlice(allocator); - std.sort.pdq(Repository, result.repositories.?, {}, struct { + const list = try repositories.toOwnedSlice(allocator); + std.sort.pdq(Repository, list, {}, struct { pub fn lessThanFn(_: void, lhs: Repository, rhs: Repository) bool { if (rhs.views == lhs.views) { return rhs.stars + rhs.forks < lhs.stars + lhs.forks; @@ -308,6 +302,17 @@ fn get_repos(client: *HttpClient) !Statistics { } }.lessThanFn); + return .{ contributions, list }; +} + +fn get_repos(client: *HttpClient) !Statistics { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + var result: Statistics = .{ .repositories = undefined }; + result.contributions, result.repositories = try repo_list(&arena, client); + errdefer result.deinit(); + const T = struct { repo: *Repository, delay: i64, @@ -319,7 +324,7 @@ fn get_repos(client: *HttpClient) !Statistics { } }.compareFn) = .init(arena.allocator(), {}); defer q.deinit(); - for (result.repositories.?) |*repo| { + for (result.repositories) |*repo| { try q.add(.{ .repo = repo, .delay = 2, @@ -330,15 +335,12 @@ fn get_repos(client: *HttpClient) !Statistics { var item = q.remove(); const now = std.time.timestamp(); if (item.timestamp > now) { + const delay: u64 = @intCast(item.timestamp - now); std.log.debug("Sleeping for {d}s. Waiting for {d} repos.", .{ - item.timestamp - now, + delay, q.count() + 1, }); - std.Thread.sleep( - @as(u64, @intCast( - item.timestamp - now, - )) * std.time.ns_per_s, - ); + std.Thread.sleep(delay * std.time.ns_per_s); } std.log.info( "Trying to get lines of code changed for {s}...", @@ -385,8 +387,8 @@ fn get_repos(client: *HttpClient) !Statistics { }, .accepted => { item.timestamp = std.time.timestamp() + item.delay; - const _delay: f64 = @floatFromInt(item.delay); - item.delay = @intFromFloat(_delay * 1.5); + const old_delay: f64 = @floatFromInt(item.delay); + item.delay = @intFromFloat(old_delay * 1.5); try q.add(item); }, else => { From e07c2886acf8a3bc590e5e97152e7269ad34175b Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Feb 2026 20:09:39 -0500 Subject: [PATCH 138/303] Refactor statistics struct into separate file --- src/main.zig | 382 +------------------------------------------- src/statistics.zig | 385 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 387 insertions(+), 380 deletions(-) create mode 100644 src/statistics.zig diff --git a/src/main.zig b/src/main.zig index bcabf3babae..243b78ceb8b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,6 +2,7 @@ const builtin = @import("builtin"); const std = @import("std"); const HttpClient = @import("http_client.zig"); +const Statistics = @import("statistics.zig"); pub const std_options: std.Options = .{ .logFn = logFn, @@ -25,385 +26,6 @@ fn logFn( } } -const Language = struct { - name: []const u8, - size: u32, - color: []const u8, - - pub fn deinit(self: @This()) void { - allocator.free(self.name); - allocator.free(self.color); - } -}; - -const Repository = struct { - name: []const u8, - stars: u32, - forks: u32, - languages: ?[]Language, - views: u32, - lines_changed: u32, - - pub fn deinit(self: @This()) void { - allocator.free(self.name); - if (self.languages) |languages| { - for (languages) |language| { - language.deinit(); - } - allocator.free(languages); - } - } -}; - -const Statistics = struct { - repositories: []Repository, - contributions: u32 = 0, - - const Self = @This(); - - pub fn deinit(self: Self) void { - for (self.repositories) |repository| { - repository.deinit(); - } - allocator.free(self.repositories); - } - - pub fn years(client: *HttpClient, alloc: std.mem.Allocator) ![]u32 { - std.log.info("Getting contribution years...", .{}); - const response, const status = try client.graphql( - \\query { - \\ viewer { - \\ contributionsCollection { - \\ contributionYears - \\ } - \\ } - \\} - , null); - if (status != .ok) { - std.log.err( - "Failed to get contribution years ({?s})", - .{status.phrase()}, - ); - return error.RequestFailed; - } - const parsed = try std.json.parseFromSliceLeaky( - struct { - data: struct { - viewer: struct { - contributionsCollection: struct { - contributionYears: []u32, - }, - }, - }, - }, - alloc, - response, - .{ .ignore_unknown_fields = true }, - ); - return parsed - .data - .viewer - .contributionsCollection - .contributionYears; - } -}; - -fn repo_list( - arena: *std.heap.ArenaAllocator, - client: *HttpClient, -) !struct { u32, []Repository } { - var contributions: u32 = 0; - var repositories: std.ArrayList(Repository) = - try .initCapacity(allocator, 32); - errdefer { - for (repositories.items) |repo| { - repo.deinit(); - } - repositories.deinit(allocator); - } - var seen: std.StringHashMap(bool) = .init(arena.allocator()); - defer seen.deinit(); - - for (try Statistics.years(client, arena.allocator())) |year| { - std.log.info("Getting data from {d}...", .{year}); - var response, var status = try client.graphql( - \\query ($from: DateTime, $to: DateTime) { - \\ viewer { - \\ contributionsCollection(from: $from, to: $to) { - \\ totalRepositoryContributions - \\ totalIssueContributions - \\ totalCommitContributions - \\ totalPullRequestContributions - \\ totalPullRequestReviewContributions - \\ commitContributionsByRepository(maxRepositories: 100) { - \\ repository { - \\ nameWithOwner - \\ stargazerCount - \\ forkCount - \\ languages( - \\ first: 100, - \\ orderBy: { direction: DESC, field: SIZE } - \\ ) { - \\ edges { - \\ size - \\ node { - \\ name - \\ color - \\ } - \\ } - \\ } - \\ } - \\ } - \\ } - \\ } - \\} - , - // NOTE: Replace with actual JSON serialization if using more - // complex tyeps. This is fine as long as we're only using numbers. - try std.fmt.allocPrint( - arena.allocator(), - \\{{ - \\ "from": "{d}-01-01T00:00:00Z", - \\ "to": "{d}-01-01T00:00:00Z" - \\}} - , - .{ year, year + 1 }, - ), - ); - if (status != .ok) { - std.log.err( - "Failed to get data from {d} ({?s})", - .{ year, status.phrase() }, - ); - return error.RequestFailed; - } - const stats = (try std.json.parseFromSliceLeaky( - struct { data: struct { viewer: struct { - contributionsCollection: struct { - totalRepositoryContributions: u32, - totalIssueContributions: u32, - totalCommitContributions: u32, - totalPullRequestContributions: u32, - totalPullRequestReviewContributions: u32, - commitContributionsByRepository: []struct { - repository: struct { - nameWithOwner: []const u8, - stargazerCount: u32, - forkCount: u32, - languages: ?struct { - edges: ?[]struct { - size: u32, - node: struct { - name: []const u8, - color: ?[]const u8, - }, - }, - }, - }, - }, - }, - } } }, - arena.allocator(), - response, - .{ .ignore_unknown_fields = true }, - )).data.viewer.contributionsCollection; - std.log.info( - "Parsed {d} total repositories from {d}", - .{ stats.commitContributionsByRepository.len, year }, - ); - - contributions += stats.totalRepositoryContributions; - contributions += stats.totalIssueContributions; - contributions += stats.totalCommitContributions; - contributions += stats.totalPullRequestContributions; - contributions += stats.totalPullRequestReviewContributions; - - // TODO: if there are 100 ore more repositories, we should subdivide - // the date range in half - - for (stats.commitContributionsByRepository) |x| { - const raw_repo = x.repository; - if (seen.get(raw_repo.nameWithOwner) orelse false) { - std.log.info( - "Skipping view count for {s} (seen)", - .{raw_repo.nameWithOwner}, - ); - continue; - } - var repository = Repository{ - .name = try allocator.dupe(u8, raw_repo.nameWithOwner), - .stars = raw_repo.stargazerCount, - .forks = raw_repo.forkCount, - .languages = null, - .views = 0, - .lines_changed = 0, - }; - if (raw_repo.languages) |repo_languages| { - if (repo_languages.edges) |raw_languages| { - repository.languages = try allocator.alloc( - Language, - raw_languages.len, - ); - for ( - raw_languages, - repository.languages.?, - ) |raw, *language| { - language.* = .{ - .name = try allocator.dupe(u8, raw.node.name), - .size = raw.size, - .color = "", - }; - if (raw.node.color) |color| { - language.color = try allocator.dupe(u8, color); - } - } - } - } - std.log.info( - "Getting views for {s}...", - .{raw_repo.nameWithOwner}, - ); - response, status = try client.rest( - try std.mem.concat( - arena.allocator(), - u8, - &.{ - "https://api.github.com/repos/", - raw_repo.nameWithOwner, - "/traffic/views", - }, - ), - ); - if (status == .ok) { - repository.views = (try std.json.parseFromSliceLeaky( - struct { count: u32 }, - arena.allocator(), - response, - .{ .ignore_unknown_fields = true }, - )).count; - } else { - std.log.info( - "Failed to get views for {s} ({?s})", - .{ raw_repo.nameWithOwner, status.phrase() }, - ); - } - try repositories.append(allocator, repository); - try seen.put(raw_repo.nameWithOwner, true); - } - } - - const list = try repositories.toOwnedSlice(allocator); - std.sort.pdq(Repository, list, {}, struct { - pub fn lessThanFn(_: void, lhs: Repository, rhs: Repository) bool { - if (rhs.views == lhs.views) { - return rhs.stars + rhs.forks < lhs.stars + lhs.forks; - } - return rhs.views < lhs.views; - } - }.lessThanFn); - - return .{ contributions, list }; -} - -fn get_repos(client: *HttpClient) !Statistics { - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - - var result: Statistics = .{ .repositories = undefined }; - result.contributions, result.repositories = try repo_list(&arena, client); - errdefer result.deinit(); - - const T = struct { - repo: *Repository, - delay: i64, - timestamp: i64, - }; - var q: std.PriorityQueue(T, void, struct { - pub fn compareFn(_: void, lhs: T, rhs: T) std.math.Order { - return std.math.order(lhs.timestamp, rhs.timestamp); - } - }.compareFn) = .init(arena.allocator(), {}); - defer q.deinit(); - for (result.repositories) |*repo| { - try q.add(.{ - .repo = repo, - .delay = 2, - .timestamp = std.time.timestamp(), - }); - } - while (q.count() > 0) { - var item = q.remove(); - const now = std.time.timestamp(); - if (item.timestamp > now) { - const delay: u64 = @intCast(item.timestamp - now); - std.log.debug("Sleeping for {d}s. Waiting for {d} repos.", .{ - delay, - q.count() + 1, - }); - std.Thread.sleep(delay * std.time.ns_per_s); - } - std.log.info( - "Trying to get lines of code changed for {s}...", - .{item.repo.name}, - ); - const response, const status = try client.rest( - try std.mem.concat( - arena.allocator(), - u8, - &.{ - "https://api.github.com/repos/", - item.repo.name, - "/stats/contributors", - }, - ), - ); - switch (status) { - .ok => { - const authors = (try std.json.parseFromSliceLeaky( - []struct { - author: struct { login: []const u8 }, - weeks: []struct { - a: u32, - d: u32, - }, - }, - arena.allocator(), - response, - .{ .ignore_unknown_fields = true }, - )); - for (authors) |o| { - if (!std.mem.eql(u8, o.author.login, user)) { - continue; - } - for (o.weeks) |week| { - item.repo.lines_changed += week.a; - item.repo.lines_changed += week.d; - } - } - std.log.info( - "Got {d} lines changed by {s} in {s}", - .{ item.repo.lines_changed, user, item.repo.name }, - ); - }, - .accepted => { - item.timestamp = std.time.timestamp() + item.delay; - const old_delay: f64 = @floatFromInt(item.delay); - item.delay = @intFromFloat(old_delay * 1.5); - try q.add(item); - }, - else => { - std.log.err( - "Failed to get contribution data for {s} ({?s})", - .{ item.repo.name, status.phrase() }, - ); - return error.RequestFailed; - }, - } - } - - return result; -} - pub fn main() !void { var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; defer _ = gpa.deinit(); @@ -418,7 +40,7 @@ pub fn main() !void { ); user = ""; defer client.deinit(); - const stats = try get_repos(&client); + const stats = try Statistics.init(&client, user, allocator); defer stats.deinit(); print(stats); diff --git a/src/statistics.zig b/src/statistics.zig new file mode 100644 index 00000000000..083b027d532 --- /dev/null +++ b/src/statistics.zig @@ -0,0 +1,385 @@ +const std = @import("std"); +const HttpClient = @import("http_client.zig"); + +repositories: []Repository, +contributions: u32 = 0, + +var allocator: std.mem.Allocator = undefined; +const Statistics = @This(); + +const Language = struct { + name: []const u8, + size: u32, + color: []const u8, + + pub fn deinit(self: @This()) void { + allocator.free(self.name); + allocator.free(self.color); + } +}; + +const Repository = struct { + name: []const u8, + stars: u32, + forks: u32, + languages: ?[]Language, + views: u32, + lines_changed: u32, + + pub fn deinit(self: @This()) void { + allocator.free(self.name); + if (self.languages) |languages| { + for (languages) |language| { + language.deinit(); + } + allocator.free(languages); + } + } +}; + +pub fn deinit(self: Statistics) void { + for (self.repositories) |repository| { + repository.deinit(); + } + allocator.free(self.repositories); +} + +fn years(client: *HttpClient, alloc: std.mem.Allocator) ![]u32 { + std.log.info("Getting contribution years...", .{}); + const response, const status = try client.graphql( + \\query { + \\ viewer { + \\ contributionsCollection { + \\ contributionYears + \\ } + \\ } + \\} + , null); + if (status != .ok) { + std.log.err( + "Failed to get contribution years ({?s})", + .{status.phrase()}, + ); + return error.RequestFailed; + } + const parsed = try std.json.parseFromSliceLeaky( + struct { + data: struct { + viewer: struct { + contributionsCollection: struct { + contributionYears: []u32, + }, + }, + }, + }, + alloc, + response, + .{ .ignore_unknown_fields = true }, + ); + return parsed + .data + .viewer + .contributionsCollection + .contributionYears; +} + +fn repo_list( + arena: *std.heap.ArenaAllocator, + client: *HttpClient, +) !struct { u32, []Repository } { + var contributions: u32 = 0; + var repositories: std.ArrayList(Repository) = + try .initCapacity(allocator, 32); + errdefer { + for (repositories.items) |repo| { + repo.deinit(); + } + repositories.deinit(allocator); + } + var seen: std.StringHashMap(bool) = .init(arena.allocator()); + defer seen.deinit(); + + for (try Statistics.years(client, arena.allocator())) |year| { + std.log.info("Getting data from {d}...", .{year}); + var response, var status = try client.graphql( + \\query ($from: DateTime, $to: DateTime) { + \\ viewer { + \\ contributionsCollection(from: $from, to: $to) { + \\ totalRepositoryContributions + \\ totalIssueContributions + \\ totalCommitContributions + \\ totalPullRequestContributions + \\ totalPullRequestReviewContributions + \\ commitContributionsByRepository(maxRepositories: 100) { + \\ repository { + \\ nameWithOwner + \\ stargazerCount + \\ forkCount + \\ languages( + \\ first: 100, + \\ orderBy: { direction: DESC, field: SIZE } + \\ ) { + \\ edges { + \\ size + \\ node { + \\ name + \\ color + \\ } + \\ } + \\ } + \\ } + \\ } + \\ } + \\ } + \\} + , + // NOTE: Replace with actual JSON serialization if using more + // complex tyeps. This is fine as long as we're only using numbers. + try std.fmt.allocPrint( + arena.allocator(), + \\{{ + \\ "from": "{d}-01-01T00:00:00Z", + \\ "to": "{d}-01-01T00:00:00Z" + \\}} + , + .{ year, year + 1 }, + ), + ); + if (status != .ok) { + std.log.err( + "Failed to get data from {d} ({?s})", + .{ year, status.phrase() }, + ); + return error.RequestFailed; + } + const stats = (try std.json.parseFromSliceLeaky( + struct { data: struct { viewer: struct { + contributionsCollection: struct { + totalRepositoryContributions: u32, + totalIssueContributions: u32, + totalCommitContributions: u32, + totalPullRequestContributions: u32, + totalPullRequestReviewContributions: u32, + commitContributionsByRepository: []struct { + repository: struct { + nameWithOwner: []const u8, + stargazerCount: u32, + forkCount: u32, + languages: ?struct { + edges: ?[]struct { + size: u32, + node: struct { + name: []const u8, + color: ?[]const u8, + }, + }, + }, + }, + }, + }, + } } }, + arena.allocator(), + response, + .{ .ignore_unknown_fields = true }, + )).data.viewer.contributionsCollection; + std.log.info( + "Parsed {d} total repositories from {d}", + .{ stats.commitContributionsByRepository.len, year }, + ); + + contributions += stats.totalRepositoryContributions; + contributions += stats.totalIssueContributions; + contributions += stats.totalCommitContributions; + contributions += stats.totalPullRequestContributions; + contributions += stats.totalPullRequestReviewContributions; + + // TODO: if there are 100 ore more repositories, we should subdivide + // the date range in half + + for (stats.commitContributionsByRepository) |x| { + const raw_repo = x.repository; + if (seen.get(raw_repo.nameWithOwner) orelse false) { + std.log.info( + "Skipping view count for {s} (seen)", + .{raw_repo.nameWithOwner}, + ); + continue; + } + var repository = Repository{ + .name = try allocator.dupe(u8, raw_repo.nameWithOwner), + .stars = raw_repo.stargazerCount, + .forks = raw_repo.forkCount, + .languages = null, + .views = 0, + .lines_changed = 0, + }; + if (raw_repo.languages) |repo_languages| { + if (repo_languages.edges) |raw_languages| { + repository.languages = try allocator.alloc( + Language, + raw_languages.len, + ); + for ( + raw_languages, + repository.languages.?, + ) |raw, *language| { + language.* = .{ + .name = try allocator.dupe(u8, raw.node.name), + .size = raw.size, + .color = "", + }; + if (raw.node.color) |color| { + language.color = try allocator.dupe(u8, color); + } + } + } + } + std.log.info( + "Getting views for {s}...", + .{raw_repo.nameWithOwner}, + ); + response, status = try client.rest( + try std.mem.concat( + arena.allocator(), + u8, + &.{ + "https://api.github.com/repos/", + raw_repo.nameWithOwner, + "/traffic/views", + }, + ), + ); + if (status == .ok) { + repository.views = (try std.json.parseFromSliceLeaky( + struct { count: u32 }, + arena.allocator(), + response, + .{ .ignore_unknown_fields = true }, + )).count; + } else { + std.log.info( + "Failed to get views for {s} ({?s})", + .{ raw_repo.nameWithOwner, status.phrase() }, + ); + } + try repositories.append(allocator, repository); + try seen.put(raw_repo.nameWithOwner, true); + } + } + + const list = try repositories.toOwnedSlice(allocator); + std.sort.pdq(Repository, list, {}, struct { + pub fn lessThanFn(_: void, lhs: Repository, rhs: Repository) bool { + if (rhs.views == lhs.views) { + return rhs.stars + rhs.forks < lhs.stars + lhs.forks; + } + return rhs.views < lhs.views; + } + }.lessThanFn); + + return .{ contributions, list }; +} + +pub fn init( + client: *HttpClient, + user: []const u8, + alloc: std.mem.Allocator, +) !Statistics { + allocator = alloc; + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + var result: Statistics = .{ .repositories = undefined }; + result.contributions, result.repositories = try repo_list(&arena, client); + errdefer result.deinit(); + + const T = struct { + repo: *Repository, + delay: i64, + timestamp: i64, + }; + var q: std.PriorityQueue(T, void, struct { + pub fn compareFn(_: void, lhs: T, rhs: T) std.math.Order { + return std.math.order(lhs.timestamp, rhs.timestamp); + } + }.compareFn) = .init(arena.allocator(), {}); + defer q.deinit(); + for (result.repositories) |*repo| { + try q.add(.{ + .repo = repo, + .delay = 2, + .timestamp = std.time.timestamp(), + }); + } + while (q.count() > 0) { + var item = q.remove(); + const now = std.time.timestamp(); + if (item.timestamp > now) { + const delay: u64 = @intCast(item.timestamp - now); + std.log.debug("Sleeping for {d}s. Waiting for {d} repos.", .{ + delay, + q.count() + 1, + }); + std.Thread.sleep(delay * std.time.ns_per_s); + } + std.log.info( + "Trying to get lines of code changed for {s}...", + .{item.repo.name}, + ); + const response, const status = try client.rest( + try std.mem.concat( + arena.allocator(), + u8, + &.{ + "https://api.github.com/repos/", + item.repo.name, + "/stats/contributors", + }, + ), + ); + switch (status) { + .ok => { + const authors = (try std.json.parseFromSliceLeaky( + []struct { + author: struct { login: []const u8 }, + weeks: []struct { + a: u32, + d: u32, + }, + }, + arena.allocator(), + response, + .{ .ignore_unknown_fields = true }, + )); + for (authors) |o| { + if (!std.mem.eql(u8, o.author.login, user)) { + continue; + } + for (o.weeks) |week| { + item.repo.lines_changed += week.a; + item.repo.lines_changed += week.d; + } + } + std.log.info( + "Got {d} lines changed by {s} in {s}", + .{ item.repo.lines_changed, user, item.repo.name }, + ); + }, + .accepted => { + item.timestamp = std.time.timestamp() + item.delay; + const old_delay: f64 = @floatFromInt(item.delay); + item.delay = @intFromFloat(old_delay * 1.5); + try q.add(item); + }, + else => { + std.log.err( + "Failed to get contribution data for {s} ({?s})", + .{ item.repo.name, status.phrase() }, + ); + return error.RequestFailed; + }, + } + } + + return result; +} From ce45bf2c0de01c26579073c2acb42628125ca273 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Feb 2026 20:23:19 -0500 Subject: [PATCH 139/303] Use probabilistic exponential backoff with jitter --- src/statistics.zig | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/statistics.zig b/src/statistics.zig index 083b027d532..ed973cb44b6 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -367,8 +367,12 @@ pub fn init( }, .accepted => { item.timestamp = std.time.timestamp() + item.delay; - const old_delay: f64 = @floatFromInt(item.delay); - item.delay = @intFromFloat(old_delay * 1.5); + // Exponential backoff (in expectation) with jitter + item.delay += std.crypto.random.intRangeAtMost( + i64, + 1, + item.delay, + ); try q.add(item); }, else => { From 02d06a1bcb3efd0f3e0bc17240098d570c488c68 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 25 Feb 2026 03:19:41 +0000 Subject: [PATCH 140/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index c9f97046bf8..037180e90fb 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -97,9 +97,9 @@ tr { All-time contributions4,547 -Lines of code changed2,777,018 +Lines of code changed2,774,867 -Repository views (past two weeks)1,396 +Repository views (past two weeks)1,452 Repositories with contributions128 From 4c2a3a324e106bb3c432652c1063ab6101705261 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 25 Feb 2026 00:52:17 -0500 Subject: [PATCH 141/303] Fix keep alive failure retry logic once and for all --- src/http_client.zig | 49 +++++++++++++++++++++++++++++++-------------- src/statistics.zig | 6 +++--- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/http_client.zig b/src/http_client.zig index 4d1df4dbf31..d3ea8c4860d 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -12,7 +12,6 @@ last_request: ?i64 = null, const Self = @This(); const Response = struct { []const u8, std.http.Status }; -const KEEP_ALIVE_TIMEOUT: i64 = 16; pub fn init(allocator: std.mem.Allocator, token: []const u8) !Self { const arena = try allocator.create(std.heap.ArenaAllocator); @@ -44,17 +43,27 @@ pub fn get( ); defer writer.deinit(); const now = std.time.timestamp(); - const status = (try self.client.fetch(.{ + const status = (try (self.client.fetch(.{ .location = .{ .url = url }, .response_writer = &writer.writer, .headers = headers, .extra_headers = extra_headers, - // Work around failures from keep alive connections closing after - // timeout and not being automatically reopened by Zig - .keep_alive = if (self.last_request) |last| - now - last > KEEP_ALIVE_TIMEOUT - else - true, + }) catch |err| switch (err) { + error.HttpConnectionClosing => { + // Handle a Zig HTTP bug where keep-alive connections are closed by + // the server after a timeout, but the client doesn't handle it + // properly. For now we nuke the whole client (and associate + // connection pool) and make a new one, but there might be a better + // way to handle this. + std.log.debug( + "Keep alive connection closed. Initializing a new client.", + .{}, + ); + self.client.deinit(); + self.client = .{ .allocator = self.arena.allocator() }; + return self.get(url, headers, extra_headers); + }, + else => err, })).status; self.last_request = now; return .{ try writer.toOwnedSlice(), status }; @@ -72,17 +81,27 @@ pub fn post( ); defer writer.deinit(); const now = std.time.timestamp(); - const status = (try self.client.fetch(.{ + const status = (try (self.client.fetch(.{ .location = .{ .url = url }, .response_writer = &writer.writer, .payload = body, .headers = headers, - // Work around failures from keep alive connections closing after - // timeout and not being automatically reopened by Zig - .keep_alive = if (self.last_request) |last| - now - last > KEEP_ALIVE_TIMEOUT - else - true, + }) catch |err| switch (err) { + error.HttpConnectionClosing => { + // Handle a Zig HTTP bug where keep-alive connections are closed by + // the server after a timeout, but the client doesn't handle it + // properly. For now we nuke the whole client (and associate + // connection pool) and make a new one, but there might be a better + // way to handle this. + std.log.debug( + "Keep alive connection closed. Initializing a new client.", + .{}, + ); + self.client.deinit(); + self.client = .{ .allocator = self.arena.allocator() }; + return self.post(url, body, headers); + }, + else => err, })).status; self.last_request = now; return .{ try writer.toOwnedSlice(), status }; diff --git a/src/statistics.zig b/src/statistics.zig index ed973cb44b6..ba5395ea627 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -199,7 +199,7 @@ fn repo_list( for (stats.commitContributionsByRepository) |x| { const raw_repo = x.repository; if (seen.get(raw_repo.nameWithOwner) orelse false) { - std.log.info( + std.log.debug( "Skipping view count for {s} (seen)", .{raw_repo.nameWithOwner}, ); @@ -307,7 +307,7 @@ pub fn init( for (result.repositories) |*repo| { try q.add(.{ .repo = repo, - .delay = 2, + .delay = 8, .timestamp = std.time.timestamp(), }); } @@ -370,7 +370,7 @@ pub fn init( // Exponential backoff (in expectation) with jitter item.delay += std.crypto.random.intRangeAtMost( i64, - 1, + 2, item.delay, ); try q.add(item); From adf437ddbd0053d7743750582a9040b9d55d3a75 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 25 Feb 2026 10:47:02 -0500 Subject: [PATCH 142/303] Refactor lines changed into separate function --- src/main.zig | 3 +- src/statistics.zig | 79 +++++++++++++++++++++++++++------------------- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/src/main.zig b/src/main.zig index 243b78ceb8b..adeae4327b9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -38,9 +38,8 @@ pub fn main() !void { allocator, "", ); - user = ""; defer client.deinit(); - const stats = try Statistics.init(&client, user, allocator); + const stats = try Statistics.init(&client, allocator); defer stats.deinit(); print(stats); diff --git a/src/statistics.zig b/src/statistics.zig index ba5395ea627..e9abc73068a 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -2,6 +2,7 @@ const std = @import("std"); const HttpClient = @import("http_client.zig"); repositories: []Repository, +user: []const u8, contributions: u32 = 0, var allocator: std.mem.Allocator = undefined; @@ -37,11 +38,23 @@ const Repository = struct { } }; +pub fn init(client: *HttpClient, a: std.mem.Allocator) !Statistics { + allocator = a; + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + var self: Statistics = try get_repos(&arena, client); + errdefer self.deinit(); + try self.get_lines_changed(&arena, client); + return self; +} + pub fn deinit(self: Statistics) void { for (self.repositories) |repository| { repository.deinit(); } allocator.free(self.repositories); + allocator.free(self.user); } fn years(client: *HttpClient, alloc: std.mem.Allocator) ![]u32 { @@ -63,15 +76,11 @@ fn years(client: *HttpClient, alloc: std.mem.Allocator) ![]u32 { return error.RequestFailed; } const parsed = try std.json.parseFromSliceLeaky( - struct { - data: struct { - viewer: struct { - contributionsCollection: struct { - contributionYears: []u32, - }, - }, + struct { data: struct { viewer: struct { + contributionsCollection: struct { + contributionYears: []u32, }, - }, + } } }, alloc, response, .{ .ignore_unknown_fields = true }, @@ -83,11 +92,12 @@ fn years(client: *HttpClient, alloc: std.mem.Allocator) ![]u32 { .contributionYears; } -fn repo_list( +fn get_repos( arena: *std.heap.ArenaAllocator, client: *HttpClient, -) !struct { u32, []Repository } { +) !Statistics { var contributions: u32 = 0; + var user: []const u8 = undefined; var repositories: std.ArrayList(Repository) = try .initCapacity(allocator, 32); errdefer { @@ -104,6 +114,7 @@ fn repo_list( var response, var status = try client.graphql( \\query ($from: DateTime, $to: DateTime) { \\ viewer { + \\ login \\ contributionsCollection(from: $from, to: $to) { \\ totalRepositoryContributions \\ totalIssueContributions @@ -152,8 +163,9 @@ fn repo_list( ); return error.RequestFailed; } - const stats = (try std.json.parseFromSliceLeaky( + const viewer = (try std.json.parseFromSliceLeaky( struct { data: struct { viewer: struct { + login: []const u8, contributionsCollection: struct { totalRepositoryContributions: u32, totalIssueContributions: u32, @@ -181,7 +193,9 @@ fn repo_list( arena.allocator(), response, .{ .ignore_unknown_fields = true }, - )).data.viewer.contributionsCollection; + )).data.viewer; + user = viewer.login; + const stats = viewer.contributionsCollection; std.log.info( "Parsed {d} total repositories from {d}", .{ stats.commitContributionsByRepository.len, year }, @@ -226,6 +240,7 @@ fn repo_list( language.* = .{ .name = try allocator.dupe(u8, raw.node.name), .size = raw.size, + // TODO: Add sensible default color .color = "", }; if (raw.node.color) |color| { @@ -277,22 +292,18 @@ fn repo_list( } }.lessThanFn); - return .{ contributions, list }; + return .{ + .contributions = contributions, + .user = try allocator.dupe(u8, user), + .repositories = list, + }; } -pub fn init( +fn get_lines_changed( + self: *Statistics, + arena: *std.heap.ArenaAllocator, client: *HttpClient, - user: []const u8, - alloc: std.mem.Allocator, -) !Statistics { - allocator = alloc; - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - - var result: Statistics = .{ .repositories = undefined }; - result.contributions, result.repositories = try repo_list(&arena, client); - errdefer result.deinit(); - +) !void { const T = struct { repo: *Repository, delay: i64, @@ -304,7 +315,7 @@ pub fn init( } }.compareFn) = .init(arena.allocator(), {}); defer q.deinit(); - for (result.repositories) |*repo| { + for (self.repositories) |*repo| { try q.add(.{ .repo = repo, .delay = 8, @@ -316,9 +327,10 @@ pub fn init( const now = std.time.timestamp(); if (item.timestamp > now) { const delay: u64 = @intCast(item.timestamp - now); - std.log.debug("Sleeping for {d}s. Waiting for {d} repos.", .{ + std.log.debug("Sleeping for {d}s. Waiting for {d} repo{s}.", .{ delay, q.count() + 1, + if (q.count() != 0) "s" else "", }); std.Thread.sleep(delay * std.time.ns_per_s); } @@ -352,7 +364,7 @@ pub fn init( .{ .ignore_unknown_fields = true }, )); for (authors) |o| { - if (!std.mem.eql(u8, o.author.login, user)) { + if (!std.mem.eql(u8, o.author.login, self.user)) { continue; } for (o.weeks) |week| { @@ -361,8 +373,13 @@ pub fn init( } } std.log.info( - "Got {d} lines changed by {s} in {s}", - .{ item.repo.lines_changed, user, item.repo.name }, + "Got {d} line{s} changed by {s} in {s}", + .{ + item.repo.lines_changed, + if (item.repo.lines_changed != 1) "s" else "", + self.user, + item.repo.name, + }, ); }, .accepted => { @@ -384,6 +401,4 @@ pub fn init( }, } } - - return result; } From f5bbf6a160b8e945f912f2b6231f410c1d22e4a6 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 25 Feb 2026 17:15:06 -0500 Subject: [PATCH 143/303] Get contribution lines early for faster runtime --- src/statistics.zig | 126 +++++++++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 55 deletions(-) diff --git a/src/statistics.zig b/src/statistics.zig index e9abc73068a..8902ca29aee 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -36,6 +36,62 @@ const Repository = struct { allocator.free(languages); } } + + pub fn get_lines_changed( + self: *@This(), + arena: *std.heap.ArenaAllocator, + client: *HttpClient, + user: []const u8, + ) !std.http.Status { + std.log.debug( + "Trying to get lines of code changed for {s}...", + .{self.name}, + ); + const response, const status = try client.rest( + try std.mem.concat( + arena.allocator(), + u8, + &.{ + "https://api.github.com/repos/", + self.name, + "/stats/contributors", + }, + ), + ); + if (status == .ok) { + const authors = (try std.json.parseFromSliceLeaky( + []struct { + author: struct { login: []const u8 }, + weeks: []struct { + a: u32, + d: u32, + }, + }, + arena.allocator(), + response, + .{ .ignore_unknown_fields = true }, + )); + for (authors) |o| { + if (!std.mem.eql(u8, o.author.login, user)) { + continue; + } + for (o.weeks) |week| { + self.lines_changed += week.a; + self.lines_changed += week.d; + } + } + std.log.info( + "Got {d} line{s} changed by {s} in {s}", + .{ + self.lines_changed, + if (self.lines_changed != 1) "s" else "", + user, + self.name, + }, + ); + } + return status; + } }; pub fn init(client: *HttpClient, a: std.mem.Allocator) !Statistics { @@ -214,7 +270,7 @@ fn get_repos( const raw_repo = x.repository; if (seen.get(raw_repo.nameWithOwner) orelse false) { std.log.debug( - "Skipping view count for {s} (seen)", + "Skipping {s} (seen)", .{raw_repo.nameWithOwner}, ); continue; @@ -227,6 +283,7 @@ fn get_repos( .views = 0, .lines_changed = 0, }; + if (raw_repo.languages) |repo_languages| { if (repo_languages.edges) |raw_languages| { repository.languages = try allocator.alloc( @@ -249,6 +306,8 @@ fn get_repos( } } } + errdefer repository.deinit(); + std.log.info( "Getting views for {s}...", .{raw_repo.nameWithOwner}, @@ -277,6 +336,9 @@ fn get_repos( .{ raw_repo.nameWithOwner, status.phrase() }, ); } + + _ = try repository.get_lines_changed(arena, client, user); + try repositories.append(allocator, repository); try seen.put(raw_repo.nameWithOwner, true); } @@ -316,6 +378,9 @@ fn get_lines_changed( }.compareFn) = .init(arena.allocator(), {}); defer q.deinit(); for (self.repositories) |*repo| { + if (repo.lines_changed > 0) { + continue; + } try q.add(.{ .repo = repo, .delay = 8, @@ -334,65 +399,16 @@ fn get_lines_changed( }); std.Thread.sleep(delay * std.time.ns_per_s); } - std.log.info( - "Trying to get lines of code changed for {s}...", - .{item.repo.name}, - ); - const response, const status = try client.rest( - try std.mem.concat( - arena.allocator(), - u8, - &.{ - "https://api.github.com/repos/", - item.repo.name, - "/stats/contributors", - }, - ), - ); - switch (status) { - .ok => { - const authors = (try std.json.parseFromSliceLeaky( - []struct { - author: struct { login: []const u8 }, - weeks: []struct { - a: u32, - d: u32, - }, - }, - arena.allocator(), - response, - .{ .ignore_unknown_fields = true }, - )); - for (authors) |o| { - if (!std.mem.eql(u8, o.author.login, self.user)) { - continue; - } - for (o.weeks) |week| { - item.repo.lines_changed += week.a; - item.repo.lines_changed += week.d; - } - } - std.log.info( - "Got {d} line{s} changed by {s} in {s}", - .{ - item.repo.lines_changed, - if (item.repo.lines_changed != 1) "s" else "", - self.user, - item.repo.name, - }, - ); - }, + switch (try item.repo.get_lines_changed(arena, client, self.user)) { + .ok => {}, .accepted => { item.timestamp = std.time.timestamp() + item.delay; // Exponential backoff (in expectation) with jitter - item.delay += std.crypto.random.intRangeAtMost( - i64, - 2, - item.delay, - ); + item.delay += + std.crypto.random.intRangeAtMost(i64, 2, item.delay); try q.add(item); }, - else => { + else => |status| { std.log.err( "Failed to get contribution data for {s} ({?s})", .{ item.repo.name, status.phrase() }, From c3f013308170a5d9cf6be0598d3fe5a50eb3d1e5 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 26 Feb 2026 02:59:44 +0000 Subject: [PATCH 144/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 037180e90fb..4d62c6f5a9e 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,530 +Stars7,531 -Forks1,161 +Forks1,162 All-time contributions4,547 -Lines of code changed2,774,867 +Lines of code changed2,778,314 -Repository views (past two weeks)1,452 +Repository views (past two weeks)1,511 Repositories with contributions128 From fc0a03e002c68d4a2c410ae763f5c72bae789ebc Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 27 Feb 2026 03:27:56 +0000 Subject: [PATCH 145/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 4d62c6f5a9e..d62fa364c90 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,7 +91,7 @@ tr { -Stars7,531 +Stars7,532 Forks1,162 @@ -99,7 +99,7 @@ tr { Lines of code changed2,778,314 -Repository views (past two weeks)1,511 +Repository views (past two weeks)1,521 Repositories with contributions128 From 871a2f8f0ceb704bea982262b3c29d640c06a908 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Fri, 27 Feb 2026 12:45:23 -0500 Subject: [PATCH 146/303] Get username only once --- src/statistics.zig | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/statistics.zig b/src/statistics.zig index 8902ca29aee..a42817a1396 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -113,11 +113,15 @@ pub fn deinit(self: Statistics) void { allocator.free(self.user); } -fn years(client: *HttpClient, alloc: std.mem.Allocator) ![]u32 { +fn get_years( + client: *HttpClient, + alloc: std.mem.Allocator, +) !struct { []u32, []const u8 } { std.log.info("Getting contribution years...", .{}); const response, const status = try client.graphql( \\query { \\ viewer { + \\ login \\ contributionsCollection { \\ contributionYears \\ } @@ -131,8 +135,9 @@ fn years(client: *HttpClient, alloc: std.mem.Allocator) ![]u32 { ); return error.RequestFailed; } - const parsed = try std.json.parseFromSliceLeaky( + const parsed = (try std.json.parseFromSliceLeaky( struct { data: struct { viewer: struct { + login: []const u8, contributionsCollection: struct { contributionYears: []u32, }, @@ -140,12 +145,8 @@ fn years(client: *HttpClient, alloc: std.mem.Allocator) ![]u32 { alloc, response, .{ .ignore_unknown_fields = true }, - ); - return parsed - .data - .viewer - .contributionsCollection - .contributionYears; + )).data.viewer; + return .{ parsed.contributionsCollection.contributionYears, parsed.login }; } fn get_repos( @@ -153,7 +154,6 @@ fn get_repos( client: *HttpClient, ) !Statistics { var contributions: u32 = 0; - var user: []const u8 = undefined; var repositories: std.ArrayList(Repository) = try .initCapacity(allocator, 32); errdefer { @@ -165,12 +165,13 @@ fn get_repos( var seen: std.StringHashMap(bool) = .init(arena.allocator()); defer seen.deinit(); - for (try Statistics.years(client, arena.allocator())) |year| { + const years, const user = try get_years(client, arena.allocator()); + std.log.info("Getting data for user {s}...", .{user}); + for (years) |year| { std.log.info("Getting data from {d}...", .{year}); var response, var status = try client.graphql( \\query ($from: DateTime, $to: DateTime) { \\ viewer { - \\ login \\ contributionsCollection(from: $from, to: $to) { \\ totalRepositoryContributions \\ totalIssueContributions @@ -221,7 +222,6 @@ fn get_repos( } const viewer = (try std.json.parseFromSliceLeaky( struct { data: struct { viewer: struct { - login: []const u8, contributionsCollection: struct { totalRepositoryContributions: u32, totalIssueContributions: u32, @@ -250,7 +250,7 @@ fn get_repos( response, .{ .ignore_unknown_fields = true }, )).data.viewer; - user = viewer.login; + const stats = viewer.contributionsCollection; std.log.info( "Parsed {d} total repositories from {d}", From 729d4215a227fd4d4d5cccab77f166ee6ae3fe1c Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Fri, 27 Feb 2026 12:57:14 -0500 Subject: [PATCH 147/303] Add very basic arg and env parsing --- src/argparse.zig | 95 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 18 ++++++--- 2 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 src/argparse.zig diff --git a/src/argparse.zig b/src/argparse.zig new file mode 100644 index 00000000000..29bb00dd931 --- /dev/null +++ b/src/argparse.zig @@ -0,0 +1,95 @@ +const std = @import("std"); + +pub fn parse(allocator: std.mem.Allocator, T: type) !T { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const a = arena.allocator(); + + const fields = @typeInfo(T).@"struct".fields; + var seen = [_]bool{false} ** fields.len; + var result: T = undefined; + // TODO: An error when some of the fields are set but not others will + // leave dangling pointers + + inline for (fields, &seen) |field, *seen_field| { + if (field.defaultValue()) |default| { + @field(result, field.name) = default; + seen_field.* = true; + } + } + + { + var env = try std.process.getEnvMap(a); + defer env.deinit(); + var iterator = env.iterator(); + while (iterator.next()) |entry| { + const key = try a.dupe(u8, entry.key_ptr.*); + defer a.free(key); + std.mem.replaceScalar(u8, key, '-', '_'); + inline for (fields, &seen) |field, *seen_field| { + if (std.ascii.eqlIgnoreCase(key, field.name)) { + // TODO: Switch on field type and parse if applicable + @field(result, field.name) = try allocator.dupe( + u8, + entry.value_ptr.*, + ); + seen_field.* = true; + } + } + } + } + + { + const args = try std.process.argsAlloc(a); + defer std.process.argsFree(a, args); + var j: usize = 1; + args: while (j < args.len) : (j += 1) { + const raw_arg = args[j]; + if (std.mem.eql(u8, raw_arg, "-h") or + std.mem.eql(u8, raw_arg, "--help")) + { + printUsage(T, args[0]); + std.process.exit(0); + } + // TODO: Handle one-letter arguments + if (!std.mem.startsWith(u8, raw_arg, "--")) { + // TODO: Use actual printing + std.debug.print("Unknown argument: '{s}'\n", .{raw_arg}); + printUsage(T, args[0]); + std.process.exit(1); + } + const arg = try a.dupe(u8, raw_arg[2..]); + defer a.free(arg); + std.mem.replaceScalar(u8, arg, '-', '_'); + inline for (fields, &seen) |field, *seen_field| { + if (std.ascii.eqlIgnoreCase(arg, field.name)) { + // TODO: Switch on field type and parse if applicable + j += 1; + // TODO: Fix possible memory leak + @field(result, field.name) = try allocator.dupe(u8, args[j]); + seen_field.* = true; + continue :args; + } + } + // TODO: Use actual printing + std.debug.print("Unknown argument: '{s}'\n", .{raw_arg}); + printUsage(T, args[0]); + std.process.exit(1); + } + } + + inline for (fields, seen) |field, seen_field| { + if (!seen_field) { + std.log.err("Missing required argument {s}", .{field.name}); + return error.MissingArgument; + } + } + + return result; +} + +pub fn printUsage(T: type, argv0: []const u8) void { + // TODO: Improve + std.debug.print("Usage: {s}\n", .{argv0}); + _ = T; +} diff --git a/src/main.zig b/src/main.zig index adeae4327b9..54210d0e8ea 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,5 +1,6 @@ const builtin = @import("builtin"); const std = @import("std"); +const argparse = @import("argparse.zig"); const HttpClient = @import("http_client.zig"); const Statistics = @import("statistics.zig"); @@ -26,18 +27,23 @@ fn logFn( } } +const Args = struct { + api_key: []const u8, + + pub fn deinit(self: @This()) void { + allocator.free(self.api_key); + } +}; + pub fn main() !void { var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; defer _ = gpa.deinit(); allocator = gpa.allocator(); - // TODO: Parse environment variables - // TODO: Parse CLI flags + const args = try argparse.parse(allocator, Args); + defer args.deinit(); - var client: HttpClient = try .init( - allocator, - "", - ); + var client: HttpClient = try .init(allocator, args.api_key); defer client.deinit(); const stats = try Statistics.init(&client, allocator); defer stats.deinit(); From cea522297864c8e60efb410e9c6d65c99a8f4711 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 28 Feb 2026 02:48:32 +0000 Subject: [PATCH 148/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index d62fa364c90..b8fbcbd6427 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,7 +91,7 @@ tr { -Stars7,532 +Stars7,535 Forks1,162 @@ -99,7 +99,7 @@ tr { Lines of code changed2,778,314 -Repository views (past two weeks)1,521 +Repository views (past two weeks)1,517 Repositories with contributions128 From a0bd4c8bfa62f72720632f4d1ddb0271bbcb3f1a Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 1 Mar 2026 03:11:15 +0000 Subject: [PATCH 149/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index b8fbcbd6427..f8d608017a4 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,7 +91,7 @@ tr { -Stars7,535 +Stars7,538 Forks1,162 @@ -99,7 +99,7 @@ tr { Lines of code changed2,778,314 -Repository views (past two weeks)1,517 +Repository views (past two weeks)1,584 Repositories with contributions128 From d95d0ba3c16c9041f294fea1aa8b5974f7fe1a3e Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 2 Mar 2026 02:54:48 +0000 Subject: [PATCH 150/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index f8d608017a4..c28c2289f21 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,538 +Stars7,540 Forks1,162 -All-time contributions4,547 +All-time contributions4,548 -Lines of code changed2,778,314 +Lines of code changed2,778,563 -Repository views (past two weeks)1,584 +Repository views (past two weeks)1,620 Repositories with contributions128 From 1520a6cbb7882af5aa031a617d500832a84ffb4d Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 1 Mar 2026 22:16:07 -0500 Subject: [PATCH 151/303] Fix memory leaks --- src/argparse.zig | 89 ++++++++++++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/src/argparse.zig b/src/argparse.zig index 29bb00dd931..a248af54a82 100644 --- a/src/argparse.zig +++ b/src/argparse.zig @@ -1,5 +1,20 @@ const std = @import("std"); +fn strip_optional(T: type) type { + const info = @typeInfo(T); + if (info != .optional) return T; + return strip_optional(info.optional.child); +} + +fn free_field(allocator: std.mem.Allocator, field: anytype) void { + switch (@typeInfo(@TypeOf(field))) { + .array => allocator.free(field), + .optional => free_field(allocator, field.?), + .bool, .int, .float, .@"enum" => {}, + else => unreachable, + } +} + pub fn parse(allocator: std.mem.Allocator, T: type) !T { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); @@ -8,33 +23,10 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { const fields = @typeInfo(T).@"struct".fields; var seen = [_]bool{false} ** fields.len; var result: T = undefined; - // TODO: An error when some of the fields are set but not others will - // leave dangling pointers - - inline for (fields, &seen) |field, *seen_field| { - if (field.defaultValue()) |default| { - @field(result, field.name) = default; - seen_field.* = true; - } - } - - { - var env = try std.process.getEnvMap(a); - defer env.deinit(); - var iterator = env.iterator(); - while (iterator.next()) |entry| { - const key = try a.dupe(u8, entry.key_ptr.*); - defer a.free(key); - std.mem.replaceScalar(u8, key, '-', '_'); - inline for (fields, &seen) |field, *seen_field| { - if (std.ascii.eqlIgnoreCase(key, field.name)) { - // TODO: Switch on field type and parse if applicable - @field(result, field.name) = try allocator.dupe( - u8, - entry.value_ptr.*, - ); - seen_field.* = true; - } + errdefer { + inline for (fields, seen) |field, seen_field| { + if (seen_field) { + free_field(allocator, @field(result, field.name)); } } } @@ -42,9 +34,9 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { { const args = try std.process.argsAlloc(a); defer std.process.argsFree(a, args); - var j: usize = 1; - args: while (j < args.len) : (j += 1) { - const raw_arg = args[j]; + var i: usize = 1; + args: while (i < args.len) : (i += 1) { + const raw_arg = args[i]; if (std.mem.eql(u8, raw_arg, "-h") or std.mem.eql(u8, raw_arg, "--help")) { @@ -62,11 +54,11 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { defer a.free(arg); std.mem.replaceScalar(u8, arg, '-', '_'); inline for (fields, &seen) |field, *seen_field| { - if (std.ascii.eqlIgnoreCase(arg, field.name)) { + if (!seen_field.* and std.ascii.eqlIgnoreCase(arg, field.name)) { // TODO: Switch on field type and parse if applicable - j += 1; + i += 1; // TODO: Fix possible memory leak - @field(result, field.name) = try allocator.dupe(u8, args[j]); + @field(result, field.name) = try allocator.dupe(u8, args[i]); seen_field.* = true; continue :args; } @@ -78,6 +70,37 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { } } + { + var env = try std.process.getEnvMap(a); + defer env.deinit(); + var iterator = env.iterator(); + while (iterator.next()) |entry| { + const key = try a.dupe(u8, entry.key_ptr.*); + defer a.free(key); + std.mem.replaceScalar(u8, key, '-', '_'); + inline for (fields, &seen) |field, *seen_field| { + if (!seen_field.* and std.ascii.eqlIgnoreCase(key, field.name)) { + // TODO: Switch on field type and parse if applicable + @field(result, field.name) = try allocator.dupe( + u8, + entry.value_ptr.*, + ); + seen_field.* = true; + } + } + } + } + + inline for (fields, &seen) |field, *seen_field| { + if (!seen_field.*) { + if (field.defaultValue()) |default| { + // TODO: Switch on field type and duplicate if applicable + @field(result, field.name) = default; + seen_field.* = true; + } + } + } + inline for (fields, seen) |field, seen_field| { if (!seen_field) { std.log.err("Missing required argument {s}", .{field.name}); From 1713e8d1bce5b7abe705c6d3c5ee84f125843639 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 1 Mar 2026 23:18:32 -0500 Subject: [PATCH 152/303] Print usage --- src/argparse.zig | 45 +++++++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/argparse.zig b/src/argparse.zig index a248af54a82..31411d0ed36 100644 --- a/src/argparse.zig +++ b/src/argparse.zig @@ -15,8 +15,14 @@ fn free_field(allocator: std.mem.Allocator, field: anytype) void { } } +var stdout: *std.Io.Writer = undefined; +var arena: std.heap.ArenaAllocator = undefined; + pub fn parse(allocator: std.mem.Allocator, T: type) !T { - var arena = std.heap.ArenaAllocator.init(allocator); + var stdout_writer = std.fs.File.stdout().writer(&.{}); + stdout = &stdout_writer.interface; + + arena = .init(allocator); defer arena.deinit(); const a = arena.allocator(); @@ -40,14 +46,13 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { if (std.mem.eql(u8, raw_arg, "-h") or std.mem.eql(u8, raw_arg, "--help")) { - printUsage(T, args[0]); + try printUsage(T, args[0]); std.process.exit(0); } // TODO: Handle one-letter arguments if (!std.mem.startsWith(u8, raw_arg, "--")) { - // TODO: Use actual printing - std.debug.print("Unknown argument: '{s}'\n", .{raw_arg}); - printUsage(T, args[0]); + try stdout.print("Unknown argument: '{s}'\n", .{raw_arg}); + try printUsage(T, args[0]); std.process.exit(1); } const arg = try a.dupe(u8, raw_arg[2..]); @@ -63,9 +68,8 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { continue :args; } } - // TODO: Use actual printing - std.debug.print("Unknown argument: '{s}'\n", .{raw_arg}); - printUsage(T, args[0]); + try stdout.print("Unknown argument: '{s}'\n", .{raw_arg}); + try printUsage(T, args[0]); std.process.exit(1); } } @@ -111,8 +115,25 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { return result; } -pub fn printUsage(T: type, argv0: []const u8) void { - // TODO: Improve - std.debug.print("Usage: {s}\n", .{argv0}); - _ = T; +fn printUsage(T: type, argv0: []const u8) !void { + const a = arena.allocator(); + try stdout.print("Usage: {s} [options]\n\n", .{argv0}); + try stdout.print("Options:\n", .{}); + const fields = @typeInfo(T).@"struct".fields; + inline for (fields) |field| { + switch (@typeInfo(strip_optional(field.type))) { + .bool => { + const flag_version = try a.dupe(u8, field.name); + defer a.free(flag_version); + std.mem.replaceScalar(u8, flag_version, '_', '-'); + try stdout.print("--{s}\n", .{flag_version}); + }, + else => { + const flag_version = try a.dupe(u8, field.name); + defer a.free(flag_version); + std.mem.replaceScalar(u8, flag_version, '_', '-'); + try stdout.print("--{s} {s}\n", .{ flag_version, field.name }); + }, + } + } } From 68cd472d21bfa501c4907fbd201f12feb9acff13 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 1 Mar 2026 23:56:27 -0500 Subject: [PATCH 153/303] Parse different types of struct fields --- src/argparse.zig | 75 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/src/argparse.zig b/src/argparse.zig index 31411d0ed36..d06e1643c2b 100644 --- a/src/argparse.zig +++ b/src/argparse.zig @@ -8,10 +8,10 @@ fn strip_optional(T: type) type { fn free_field(allocator: std.mem.Allocator, field: anytype) void { switch (@typeInfo(@TypeOf(field))) { - .array => allocator.free(field), + .pointer => allocator.free(field), .optional => free_field(allocator, field.?), .bool, .int, .float, .@"enum" => {}, - else => unreachable, + else => @compileError("Disallowed struct field type."), } } @@ -37,9 +37,9 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { } } + const args = try std.process.argsAlloc(a); + defer std.process.argsFree(a, args); { - const args = try std.process.argsAlloc(a); - defer std.process.argsFree(a, args); var i: usize = 1; args: while (i < args.len) : (i += 1) { const raw_arg = args[i]; @@ -60,10 +60,30 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { std.mem.replaceScalar(u8, arg, '-', '_'); inline for (fields, &seen) |field, *seen_field| { if (!seen_field.* and std.ascii.eqlIgnoreCase(arg, field.name)) { - // TODO: Switch on field type and parse if applicable - i += 1; - // TODO: Fix possible memory leak - @field(result, field.name) = try allocator.dupe(u8, args[i]); + const t = @typeInfo(strip_optional(field.type)); + if (t == .bool) { + @field(result, field.name) = true; + } else { + i += 1; + if (i >= args.len) { + try stdout.print( + "Missing required value for argument {s} {s}\n", + .{ raw_arg, field.name }, + ); + try printUsage(T, args[0]); + std.process.exit(1); + } + switch (t) { + // TODO + .int, .float, .@"enum" => comptime unreachable, + .pointer => @field( + result, + field.name, + ) = try allocator.dupe(u8, args[i]), + .bool => comptime unreachable, + else => @compileError("Disallowed struct field type."), + } + } seen_field.* = true; continue :args; } @@ -84,11 +104,21 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { std.mem.replaceScalar(u8, key, '-', '_'); inline for (fields, &seen) |field, *seen_field| { if (!seen_field.* and std.ascii.eqlIgnoreCase(key, field.name)) { - // TODO: Switch on field type and parse if applicable - @field(result, field.name) = try allocator.dupe( - u8, - entry.value_ptr.*, - ); + switch (@typeInfo(strip_optional(field.type))) { + .bool => { + const value = try a.dupe(u8, entry.value_ptr.*); + defer a.free(value); + @field(result, field.name) = value.len > 0 and + !std.ascii.eqlIgnoreCase(value, "false"); + }, + // TODO + .int, .float, .@"enum" => comptime unreachable, + .pointer => @field( + result, + field.name, + ) = try allocator.dupe(u8, entry.value_ptr.*), + else => @compileError("Disallowed struct field type."), + } seen_field.* = true; } } @@ -98,8 +128,14 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { inline for (fields, &seen) |field, *seen_field| { if (!seen_field.*) { if (field.defaultValue()) |default| { - // TODO: Switch on field type and duplicate if applicable - @field(result, field.name) = default; + switch (@typeInfo(strip_optional(field.type))) { + .bool, .int, .float, .@"enum" => @field(result, field.name) = default, + .pointer => @field( + result, + field.name, + ) = try allocator.dupe(u8, default), + else => @compileError("Disallowed struct field type."), + } seen_field.* = true; } } @@ -107,8 +143,13 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { inline for (fields, seen) |field, seen_field| { if (!seen_field) { - std.log.err("Missing required argument {s}", .{field.name}); - return error.MissingArgument; + if (@typeInfo(strip_optional(field.type)) == .bool) { + @field(result, field.name) = false; + } else { + try stdout.print("Missing required argument {s}\n", .{field.name}); + try printUsage(T, args[0]); + std.process.exit(1); + } } } From 6aca7f1fed55a88406f4e706f73d8b4ad7d8b65b Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 2 Mar 2026 21:42:02 -0500 Subject: [PATCH 154/303] Fix free_field bug, split long lines --- src/argparse.zig | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/argparse.zig b/src/argparse.zig index d06e1643c2b..414c5157fe9 100644 --- a/src/argparse.zig +++ b/src/argparse.zig @@ -9,7 +9,7 @@ fn strip_optional(T: type) type { fn free_field(allocator: std.mem.Allocator, field: anytype) void { switch (@typeInfo(@TypeOf(field))) { .pointer => allocator.free(field), - .optional => free_field(allocator, field.?), + .optional => if (field) |v| free_field(allocator, v), .bool, .int, .float, .@"enum" => {}, else => @compileError("Disallowed struct field type."), } @@ -49,17 +49,21 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { try printUsage(T, args[0]); std.process.exit(0); } + // TODO: Handle one-letter arguments if (!std.mem.startsWith(u8, raw_arg, "--")) { try stdout.print("Unknown argument: '{s}'\n", .{raw_arg}); try printUsage(T, args[0]); std.process.exit(1); } + const arg = try a.dupe(u8, raw_arg[2..]); defer a.free(arg); std.mem.replaceScalar(u8, arg, '-', '_'); inline for (fields, &seen) |field, *seen_field| { - if (!seen_field.* and std.ascii.eqlIgnoreCase(arg, field.name)) { + if (!seen_field.* and + std.ascii.eqlIgnoreCase(arg, field.name)) + { const t = @typeInfo(strip_optional(field.type)); if (t == .bool) { @field(result, field.name) = true; @@ -81,13 +85,16 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { field.name, ) = try allocator.dupe(u8, args[i]), .bool => comptime unreachable, - else => @compileError("Disallowed struct field type."), + else => @compileError( + "Disallowed struct field type.", + ), } } seen_field.* = true; continue :args; } } + try stdout.print("Unknown argument: '{s}'\n", .{raw_arg}); try printUsage(T, args[0]); std.process.exit(1); @@ -103,7 +110,9 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { defer a.free(key); std.mem.replaceScalar(u8, key, '-', '_'); inline for (fields, &seen) |field, *seen_field| { - if (!seen_field.* and std.ascii.eqlIgnoreCase(key, field.name)) { + if (!seen_field.* and + std.ascii.eqlIgnoreCase(key, field.name)) + { switch (@typeInfo(strip_optional(field.type))) { .bool => { const value = try a.dupe(u8, entry.value_ptr.*); @@ -129,11 +138,13 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { if (!seen_field.*) { if (field.defaultValue()) |default| { switch (@typeInfo(strip_optional(field.type))) { - .bool, .int, .float, .@"enum" => @field(result, field.name) = default, + .bool, .int, .float, .@"enum" => { + @field(result, field.name) = default; + }, .pointer => @field( result, field.name, - ) = try allocator.dupe(u8, default), + ) = if (default) |p| try allocator.dupe(u8, p) else null, else => @compileError("Disallowed struct field type."), } seen_field.* = true; @@ -146,7 +157,10 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { if (@typeInfo(strip_optional(field.type)) == .bool) { @field(result, field.name) = false; } else { - try stdout.print("Missing required argument {s}\n", .{field.name}); + try stdout.print( + "Missing required argument {s}\n", + .{field.name}, + ); try printUsage(T, args[0]); std.process.exit(1); } From d072da7b862c51c6620570f69f93d61ce3341c98 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 3 Mar 2026 03:10:38 +0000 Subject: [PATCH 155/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index c28c2289f21..fe8f91e8df0 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,540 +Stars7,544 -Forks1,162 +Forks1,164 All-time contributions4,548 -Lines of code changed2,778,563 +Lines of code changed2,778,482 -Repository views (past two weeks)1,620 +Repository views (past two weeks)1,732 Repositories with contributions128 From 7b1d9d25547584829e7b9f4f2f134161891900bd Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 2 Mar 2026 23:12:10 -0500 Subject: [PATCH 156/303] Print raw data to stdout or a file based on CLI --- src/main.zig | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/main.zig b/src/main.zig index 54210d0e8ea..c2b0e95adce 100644 --- a/src/main.zig +++ b/src/main.zig @@ -29,9 +29,11 @@ fn logFn( const Args = struct { api_key: []const u8, + json_output_file: ?[]const u8 = null, pub fn deinit(self: @This()) void { allocator.free(self.api_key); + if (self.json_output_file) |output| allocator.free(output); } }; @@ -47,23 +49,28 @@ pub fn main() !void { defer client.deinit(); const stats = try Statistics.init(&client, allocator); defer stats.deinit(); - print(stats); - // TODO: Output images from templates -} + if (args.json_output_file) |path| { + const out = + if (std.mem.eql(u8, path, "-")) + std.fs.File.stdout() + else + try std.fs.cwd().createFile(path, .{}); + defer out.close(); + var write_buffer: [0x100]u8 = undefined; + var _writer = out.writer(&write_buffer); + const writer = &_writer.interface; -// TODO: Remove -fn print(x: anytype) void { - if (builtin.mode != .Debug) { - @compileError("Do not use JSON print in real code!"); + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + try writer.writeAll( + try std.json.Stringify.valueAlloc( + arena.allocator(), + stats, + .{ .whitespace = .indent_2 }, + ), + ); } - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - std.debug.print("{s}\n", .{ - std.json.Stringify.valueAlloc( - arena.allocator(), - x, - .{ .whitespace = .indent_2 }, - ) catch unreachable, - }); + + // TODO: Output images from templates } From 0675ceac02315fd8af8c93bca6cbf8635c663a2e Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 2 Mar 2026 23:26:22 -0500 Subject: [PATCH 157/303] Refactor parts of arg parsing into helpers --- src/argparse.zig | 229 +++++++++++++++++++++++++---------------------- 1 file changed, 121 insertions(+), 108 deletions(-) diff --git a/src/argparse.zig b/src/argparse.zig index 414c5157fe9..093ffce76e9 100644 --- a/src/argparse.zig +++ b/src/argparse.zig @@ -1,27 +1,16 @@ const std = @import("std"); -fn strip_optional(T: type) type { - const info = @typeInfo(T); - if (info != .optional) return T; - return strip_optional(info.optional.child); -} - -fn free_field(allocator: std.mem.Allocator, field: anytype) void { - switch (@typeInfo(@TypeOf(field))) { - .pointer => allocator.free(field), - .optional => if (field) |v| free_field(allocator, v), - .bool, .int, .float, .@"enum" => {}, - else => @compileError("Disallowed struct field type."), - } -} - +// Since parse is the only public function, these variables can be set there and +// used globally. var stdout: *std.Io.Writer = undefined; var arena: std.heap.ArenaAllocator = undefined; +var allocator: std.mem.Allocator = undefined; -pub fn parse(allocator: std.mem.Allocator, T: type) !T { +pub fn parse(gpa: std.mem.Allocator, T: type) !T { var stdout_writer = std.fs.File.stdout().writer(&.{}); stdout = &stdout_writer.interface; + allocator = gpa; arena = .init(allocator); defer arena.deinit(); const a = arena.allocator(); @@ -32,109 +21,135 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { errdefer { inline for (fields, seen) |field, seen_field| { if (seen_field) { - free_field(allocator, @field(result, field.name)); + free_field(@field(result, field.name)); } } } const args = try std.process.argsAlloc(a); defer std.process.argsFree(a, args); - { - var i: usize = 1; - args: while (i < args.len) : (i += 1) { - const raw_arg = args[i]; - if (std.mem.eql(u8, raw_arg, "-h") or - std.mem.eql(u8, raw_arg, "--help")) - { - try printUsage(T, args[0]); - std.process.exit(0); - } + try setFromCli(T, args, &seen, &result); + try setFromEnv(T, &seen, &result); + try setFromDefaults(T, &seen, &result); - // TODO: Handle one-letter arguments - if (!std.mem.startsWith(u8, raw_arg, "--")) { - try stdout.print("Unknown argument: '{s}'\n", .{raw_arg}); + inline for (fields, seen) |field, seen_field| { + if (!seen_field) { + if (@typeInfo(strip_optional(field.type)) == .bool) { + @field(result, field.name) = false; + } else { + try stdout.print( + "Missing required argument {s}\n", + .{field.name}, + ); try printUsage(T, args[0]); std.process.exit(1); } + } + } - const arg = try a.dupe(u8, raw_arg[2..]); - defer a.free(arg); - std.mem.replaceScalar(u8, arg, '-', '_'); - inline for (fields, &seen) |field, *seen_field| { - if (!seen_field.* and - std.ascii.eqlIgnoreCase(arg, field.name)) - { - const t = @typeInfo(strip_optional(field.type)); - if (t == .bool) { - @field(result, field.name) = true; - } else { - i += 1; - if (i >= args.len) { - try stdout.print( - "Missing required value for argument {s} {s}\n", - .{ raw_arg, field.name }, - ); - try printUsage(T, args[0]); - std.process.exit(1); - } - switch (t) { - // TODO - .int, .float, .@"enum" => comptime unreachable, - .pointer => @field( - result, - field.name, - ) = try allocator.dupe(u8, args[i]), - .bool => comptime unreachable, - else => @compileError( - "Disallowed struct field type.", - ), - } - } - seen_field.* = true; - continue :args; - } - } + return result; +} +fn setFromCli( + T: type, + args: []const []const u8, + seen: []bool, + result: *T, +) !void { + const a = arena.allocator(); + var i: usize = 1; + args: while (i < args.len) : (i += 1) { + const raw_arg = args[i]; + if (std.mem.eql(u8, raw_arg, "-h") or + std.mem.eql(u8, raw_arg, "--help")) + { + try printUsage(T, args[0]); + std.process.exit(0); + } + + // TODO: Handle one-letter arguments + if (!std.mem.startsWith(u8, raw_arg, "--")) { try stdout.print("Unknown argument: '{s}'\n", .{raw_arg}); try printUsage(T, args[0]); std.process.exit(1); } - } - { - var env = try std.process.getEnvMap(a); - defer env.deinit(); - var iterator = env.iterator(); - while (iterator.next()) |entry| { - const key = try a.dupe(u8, entry.key_ptr.*); - defer a.free(key); - std.mem.replaceScalar(u8, key, '-', '_'); - inline for (fields, &seen) |field, *seen_field| { - if (!seen_field.* and - std.ascii.eqlIgnoreCase(key, field.name)) - { - switch (@typeInfo(strip_optional(field.type))) { - .bool => { - const value = try a.dupe(u8, entry.value_ptr.*); - defer a.free(value); - @field(result, field.name) = value.len > 0 and - !std.ascii.eqlIgnoreCase(value, "false"); - }, + const arg = try a.dupe(u8, raw_arg[2..]); + defer a.free(arg); + std.mem.replaceScalar(u8, arg, '-', '_'); + inline for (@typeInfo(T).@"struct".fields, seen) |field, *seen_field| { + if (!seen_field.* and std.ascii.eqlIgnoreCase(arg, field.name)) { + const t = @typeInfo(strip_optional(field.type)); + if (t == .bool) { + @field(result, field.name) = true; + } else { + i += 1; + if (i >= args.len) { + try stdout.print( + "Missing required value for argument {s} {s}\n", + .{ raw_arg, field.name }, + ); + try printUsage(T, args[0]); + std.process.exit(1); + } + switch (t) { // TODO .int, .float, .@"enum" => comptime unreachable, .pointer => @field( result, field.name, - ) = try allocator.dupe(u8, entry.value_ptr.*), - else => @compileError("Disallowed struct field type."), + ) = try allocator.dupe(u8, args[i]), + .bool => comptime unreachable, + else => @compileError( + "Disallowed struct field type.", + ), } - seen_field.* = true; } + seen_field.* = true; + continue :args; + } + } + + try stdout.print("Unknown argument: '{s}'\n", .{raw_arg}); + try printUsage(T, args[0]); + std.process.exit(1); + } +} + +fn setFromEnv(T: type, seen: []bool, result: *T) !void { + const a = arena.allocator(); + var env = try std.process.getEnvMap(a); + defer env.deinit(); + var iterator = env.iterator(); + while (iterator.next()) |entry| { + const key = try a.dupe(u8, entry.key_ptr.*); + defer a.free(key); + std.mem.replaceScalar(u8, key, '-', '_'); + inline for (@typeInfo(T).@"struct".fields, seen) |field, *seen_field| { + if (!seen_field.* and std.ascii.eqlIgnoreCase(key, field.name)) { + switch (@typeInfo(strip_optional(field.type))) { + .bool => { + const value = try a.dupe(u8, entry.value_ptr.*); + defer a.free(value); + @field(result, field.name) = value.len > 0 and + !std.ascii.eqlIgnoreCase(value, "false"); + }, + // TODO + .int, .float, .@"enum" => comptime unreachable, + .pointer => @field( + result, + field.name, + ) = try allocator.dupe(u8, entry.value_ptr.*), + else => @compileError("Disallowed struct field type."), + } + seen_field.* = true; } } } +} - inline for (fields, &seen) |field, *seen_field| { +fn setFromDefaults(T: type, seen: []bool, result: *T) !void { + inline for (@typeInfo(T).@"struct".fields, seen) |field, *seen_field| { if (!seen_field.*) { if (field.defaultValue()) |default| { switch (@typeInfo(strip_optional(field.type))) { @@ -151,23 +166,6 @@ pub fn parse(allocator: std.mem.Allocator, T: type) !T { } } } - - inline for (fields, seen) |field, seen_field| { - if (!seen_field) { - if (@typeInfo(strip_optional(field.type)) == .bool) { - @field(result, field.name) = false; - } else { - try stdout.print( - "Missing required argument {s}\n", - .{field.name}, - ); - try printUsage(T, args[0]); - std.process.exit(1); - } - } - } - - return result; } fn printUsage(T: type, argv0: []const u8) !void { @@ -192,3 +190,18 @@ fn printUsage(T: type, argv0: []const u8) !void { } } } + +fn strip_optional(T: type) type { + const info = @typeInfo(T); + if (info != .optional) return T; + return strip_optional(info.optional.child); +} + +fn free_field(field: anytype) void { + switch (@typeInfo(@TypeOf(field))) { + .pointer => allocator.free(field), + .optional => if (field) |v| free_field(v), + .bool, .int, .float, .@"enum" => {}, + else => @compileError("Disallowed struct field type."), + } +} From c8d3bb3904187ee696561465b511dc064e6a9cc1 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 4 Mar 2026 03:07:32 +0000 Subject: [PATCH 158/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index fe8f91e8df0..42c2e78dcfc 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,544 -Forks1,164 +Forks1,165 -All-time contributions4,548 +All-time contributions4,549 -Lines of code changed2,778,482 +Lines of code changed2,757,204 -Repository views (past two weeks)1,732 +Repository views (past two weeks)1,762 Repositories with contributions128 From b57e8ce15f6dfc8ac08cfe908f90b3185e0643f3 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 5 Mar 2026 03:02:17 +0000 Subject: [PATCH 159/303] Update generated files --- generated/languages.svg | 12 ++++++------ generated/overview.svg | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index f6875e3bef8..7b2add83d9a 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.26% +29.24% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.69% +17.67% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.44% +7.43% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.68% +1.71% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.41% +1.44% diff --git a/generated/overview.svg b/generated/overview.svg index 42c2e78dcfc..106f569459d 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,17 +91,17 @@ tr { -Stars7,544 +Stars7,547 -Forks1,165 +Forks1,167 -All-time contributions4,549 +All-time contributions4,551 -Lines of code changed2,757,204 +Lines of code changed2,780,266 -Repository views (past two weeks)1,762 +Repository views (past two weeks)1,835 -Repositories with contributions128 +Repositories with contributions129 From 0f5a713d4a7e1078de531d1307f42dd00e14a76a Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 6 Mar 2026 03:18:16 +0000 Subject: [PATCH 160/303] Update generated files --- generated/languages.svg | 30 +++++++++++++++--------------- generated/overview.svg | 8 ++++---- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 7b2add83d9a..eac2dbda0d4 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.24% +29.08% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.67% +17.58% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -11.16% +11.47% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.40% +9.35% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.43% +7.54% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.66% +6.62% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.33% +5.30% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.40% +2.39% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.04% +2.03% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.88% +1.87% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.71% +1.70% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.44% +1.43% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Java -1.31% +1.30% @@ -261,7 +261,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C++ -0.66% +0.65% diff --git a/generated/overview.svg b/generated/overview.svg index 106f569459d..04f1df67bb7 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,547 +Stars7,548 Forks1,167 -All-time contributions4,551 +All-time contributions4,555 -Lines of code changed2,780,266 +Lines of code changed2,778,896 -Repository views (past two weeks)1,835 +Repository views (past two weeks)1,867 Repositories with contributions129 From 8df15d689094b90c54a1cea169c65a59cf6400fe Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 7 Mar 2026 03:00:23 +0000 Subject: [PATCH 161/303] Update generated files --- generated/languages.svg | 22 +++++++++++----------- generated/overview.svg | 6 +++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index eac2dbda0d4..3f886b31a62 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -29.08% +28.98% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.58% +17.52% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -11.47% +11.78% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.35% +9.32% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.54% +7.51% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.62% +6.60% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.30% +5.28% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.39% +2.38% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.03% +2.02% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.87% +1.86% diff --git a/generated/overview.svg b/generated/overview.svg index 04f1df67bb7..fcc38cd2522 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,548 -Forks1,167 +Forks1,168 All-time contributions4,555 -Lines of code changed2,778,896 +Lines of code changed2,780,582 -Repository views (past two weeks)1,867 +Repository views (past two weeks)1,862 Repositories with contributions129 From d5a609bffcb0cd1aa649c1a2690e237a3323f70e Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 8 Mar 2026 03:24:59 +0000 Subject: [PATCH 162/303] Update generated files --- generated/languages.svg | 12 ++++++------ generated/overview.svg | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 3f886b31a62..50e2378f681 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -28.98% +28.97% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.52% +17.51% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -11.78% +11.79% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.60% +6.59% @@ -342,7 +342,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Just -0.09% +0.10% diff --git a/generated/overview.svg b/generated/overview.svg index fcc38cd2522..e86a98a472a 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,548 +Stars7,552 Forks1,168 All-time contributions4,555 -Lines of code changed2,780,582 +Lines of code changed2,779,866 -Repository views (past two weeks)1,862 +Repository views (past two weeks)1,845 Repositories with contributions129 From 9ec956f398640e63e5a6ccc2cb39d920da663b7e Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 9 Mar 2026 02:59:54 +0000 Subject: [PATCH 163/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index e86a98a472a..bf15598d96c 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,552 -Forks1,168 +Forks1,169 All-time contributions4,555 -Lines of code changed2,779,866 +Lines of code changed2,780,582 -Repository views (past two weeks)1,845 +Repository views (past two weeks)1,819 Repositories with contributions129 From 0a3f0df4a5367e0fab614c6105662ae90dec8b12 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 10 Mar 2026 03:15:43 +0000 Subject: [PATCH 164/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index bf15598d96c..412068a02d3 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,552 +Stars7,554 -Forks1,169 +Forks1,170 All-time contributions4,555 Lines of code changed2,780,582 -Repository views (past two weeks)1,819 +Repository views (past two weeks)1,763 Repositories with contributions129 From 3a1f604507b2e092ca162ff0eb2226a203eaa80c Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 11 Mar 2026 03:01:26 +0000 Subject: [PATCH 165/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 412068a02d3..0c5e4f2e4ee 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,554 +Stars7,555 -Forks1,170 +Forks1,169 All-time contributions4,555 Lines of code changed2,780,582 -Repository views (past two weeks)1,763 +Repository views (past two weeks)1,744 Repositories with contributions129 From dce37aeffe32a603eeb4354bdf0047f4ac475fcd Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 12 Mar 2026 03:35:53 +0000 Subject: [PATCH 166/303] Update generated files --- generated/languages.svg | 16 ++++++++-------- generated/overview.svg | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 50e2378f681..8abb24ab0aa 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -28.97% +28.93% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.51% +17.49% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -11.79% +11.91% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.32% +9.31% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.51% +7.50% @@ -243,7 +243,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Makefile -1.43% +1.42% @@ -252,7 +252,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Java -1.30% +1.29% diff --git a/generated/overview.svg b/generated/overview.svg index 0c5e4f2e4ee..ef8a52d9d4c 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,555 +Stars7,557 -Forks1,169 +Forks1,168 All-time contributions4,555 -Lines of code changed2,780,582 +Lines of code changed2,757,580 -Repository views (past two weeks)1,744 +Repository views (past two weeks)1,677 Repositories with contributions129 From d02a2bb8778082357487e513c91f138773a5448d Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 13 Mar 2026 03:31:31 +0000 Subject: [PATCH 167/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index ef8a52d9d4c..49caa811600 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,557 +Stars7,558 Forks1,168 All-time contributions4,555 -Lines of code changed2,757,580 +Lines of code changed2,751,162 -Repository views (past two weeks)1,677 +Repository views (past two weeks)1,662 Repositories with contributions129 From 3fbcd128e0d01690abc8e21be031a6e28f689558 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 14 Mar 2026 03:15:36 +0000 Subject: [PATCH 168/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 49caa811600..8dddd2985e5 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,558 +Stars7,561 -Forks1,168 +Forks1,167 All-time contributions4,555 -Lines of code changed2,751,162 +Lines of code changed2,780,582 -Repository views (past two weeks)1,662 +Repository views (past two weeks)1,711 Repositories with contributions129 From 3a7f21e6713e47b811811301c98665fa23f5fc3a Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 14 Mar 2026 12:31:50 -0400 Subject: [PATCH 169/303] Track different contribution types separately --- src/main.zig | 5 ++--- src/statistics.zig | 36 ++++++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/main.zig b/src/main.zig index c2b0e95adce..1f5dcdd3a1a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -58,12 +58,11 @@ pub fn main() !void { try std.fs.cwd().createFile(path, .{}); defer out.close(); var write_buffer: [0x100]u8 = undefined; - var _writer = out.writer(&write_buffer); - const writer = &_writer.interface; + var writer = out.writer(&write_buffer); var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); - try writer.writeAll( + try writer.interface.writeAll( try std.json.Stringify.valueAlloc( arena.allocator(), stats, diff --git a/src/statistics.zig b/src/statistics.zig index a42817a1396..69248fbc841 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -3,7 +3,11 @@ const HttpClient = @import("http_client.zig"); repositories: []Repository, user: []const u8, -contributions: u32 = 0, +repo_contributions: u32 = 0, +issue_contributions: u32 = 0, +commit_contributions: u32 = 0, +pr_contributions: u32 = 0, +review_contributions: u32 = 0, var allocator: std.mem.Allocator = undefined; const Statistics = @This(); @@ -153,7 +157,16 @@ fn get_repos( arena: *std.heap.ArenaAllocator, client: *HttpClient, ) !Statistics { - var contributions: u32 = 0; + var result: Statistics = .{ + .repo_contributions = 0, + .issue_contributions = 0, + .commit_contributions = 0, + .pr_contributions = 0, + .review_contributions = 0, + + .user = undefined, + .repositories = undefined, + }; var repositories: std.ArrayList(Repository) = try .initCapacity(allocator, 32); errdefer { @@ -257,11 +270,12 @@ fn get_repos( .{ stats.commitContributionsByRepository.len, year }, ); - contributions += stats.totalRepositoryContributions; - contributions += stats.totalIssueContributions; - contributions += stats.totalCommitContributions; - contributions += stats.totalPullRequestContributions; - contributions += stats.totalPullRequestReviewContributions; + result.repo_contributions += stats.totalRepositoryContributions; + result.issue_contributions += stats.totalIssueContributions; + result.commit_contributions += stats.totalCommitContributions; + result.pr_contributions += stats.totalPullRequestContributions; + result.review_contributions += + stats.totalPullRequestReviewContributions; // TODO: if there are 100 ore more repositories, we should subdivide // the date range in half @@ -354,11 +368,9 @@ fn get_repos( } }.lessThanFn); - return .{ - .contributions = contributions, - .user = try allocator.dupe(u8, user), - .repositories = list, - }; + result.user = try allocator.dupe(u8, user); + result.repositories = list; + return result; } fn get_lines_changed( From 12191b2017a9ef192974694903e2110db6df8b0f Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 14 Mar 2026 19:01:57 -0400 Subject: [PATCH 170/303] Add verbose CLI flag --- src/main.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 1f5dcdd3a1a..b85ec9a4799 100644 --- a/src/main.zig +++ b/src/main.zig @@ -30,6 +30,7 @@ fn logFn( const Args = struct { api_key: []const u8, json_output_file: ?[]const u8 = null, + verbose: bool = false, pub fn deinit(self: @This()) void { allocator.free(self.api_key); @@ -44,6 +45,9 @@ pub fn main() !void { const args = try argparse.parse(allocator, Args); defer args.deinit(); + if (args.verbose) { + log_level = .debug; + } var client: HttpClient = try .init(allocator, args.api_key); defer client.deinit(); @@ -57,7 +61,7 @@ pub fn main() !void { else try std.fs.cwd().createFile(path, .{}); defer out.close(); - var write_buffer: [0x100]u8 = undefined; + var write_buffer: [64 * 1024]u8 = undefined; var writer = out.writer(&write_buffer); var arena = std.heap.ArenaAllocator.init(allocator); From 08f3295e9e6a75c3e6589d5ac3217e3c17c35040 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 14 Mar 2026 19:25:48 -0400 Subject: [PATCH 171/303] Don't forget to flush --- src/main.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.zig b/src/main.zig index b85ec9a4799..01674649155 100644 --- a/src/main.zig +++ b/src/main.zig @@ -73,6 +73,7 @@ pub fn main() !void { .{ .whitespace = .indent_2 }, ), ); + try writer.interface.flush(); } // TODO: Output images from templates From 7b8c6826c275db3f19cb2af85747e2210d59b5a3 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 14 Mar 2026 19:50:05 -0400 Subject: [PATCH 172/303] Pull user name in addition to username --- src/statistics.zig | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/statistics.zig b/src/statistics.zig index 69248fbc841..d56ab87f5bd 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -3,6 +3,7 @@ const HttpClient = @import("http_client.zig"); repositories: []Repository, user: []const u8, +name: []const u8, repo_contributions: u32 = 0, issue_contributions: u32 = 0, commit_contributions: u32 = 0, @@ -115,17 +116,19 @@ pub fn deinit(self: Statistics) void { } allocator.free(self.repositories); allocator.free(self.user); + allocator.free(self.name); } -fn get_years( +fn get_basic_info( client: *HttpClient, alloc: std.mem.Allocator, -) !struct { []u32, []const u8 } { +) !struct { []u32, []const u8, ?[]const u8 } { std.log.info("Getting contribution years...", .{}); const response, const status = try client.graphql( \\query { \\ viewer { \\ login + \\ name \\ contributionsCollection { \\ contributionYears \\ } @@ -142,6 +145,7 @@ fn get_years( const parsed = (try std.json.parseFromSliceLeaky( struct { data: struct { viewer: struct { login: []const u8, + name: ?[]const u8, contributionsCollection: struct { contributionYears: []u32, }, @@ -150,7 +154,11 @@ fn get_years( response, .{ .ignore_unknown_fields = true }, )).data.viewer; - return .{ parsed.contributionsCollection.contributionYears, parsed.login }; + return .{ + parsed.contributionsCollection.contributionYears, + parsed.login, + parsed.name, + }; } fn get_repos( @@ -158,13 +166,8 @@ fn get_repos( client: *HttpClient, ) !Statistics { var result: Statistics = .{ - .repo_contributions = 0, - .issue_contributions = 0, - .commit_contributions = 0, - .pr_contributions = 0, - .review_contributions = 0, - .user = undefined, + .name = undefined, .repositories = undefined, }; var repositories: std.ArrayList(Repository) = @@ -178,8 +181,13 @@ fn get_repos( var seen: std.StringHashMap(bool) = .init(arena.allocator()); defer seen.deinit(); - const years, const user = try get_years(client, arena.allocator()); - std.log.info("Getting data for user {s}...", .{user}); + const years, const user, const name = + try get_basic_info(client, arena.allocator()); + if (name) |n| { + std.log.info("Getting data for {s} ({s})...", .{ n, user }); + } else { + std.log.info("Getting data for user {s}...", .{user}); + } for (years) |year| { std.log.info("Getting data from {d}...", .{year}); var response, var status = try client.graphql( @@ -369,6 +377,9 @@ fn get_repos( }.lessThanFn); result.user = try allocator.dupe(u8, user); + errdefer allocator.free(result.user); + result.name = try allocator.dupe(u8, name orelse user); + errdefer allocator.free(result.name); result.repositories = list; return result; } From 6ea8c8e5f3be528456f66a94ebf94119dc9dc399 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 15 Mar 2026 03:41:44 +0000 Subject: [PATCH 173/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 8dddd2985e5..006e804a465 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,561 -Forks1,167 +Forks1,168 All-time contributions4,555 -Lines of code changed2,780,582 +Lines of code changed2,778,256 -Repository views (past two weeks)1,711 +Repository views (past two weeks)1,680 Repositories with contributions129 From d365a40f213e5e38613b43219d43a5504fb260c6 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 16 Mar 2026 03:33:11 +0000 Subject: [PATCH 174/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 006e804a465..9095dd61dd8 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -97,9 +97,9 @@ tr { All-time contributions4,555 -Lines of code changed2,778,256 +Lines of code changed2,780,582 -Repository views (past two weeks)1,680 +Repository views (past two weeks)1,682 Repositories with contributions129 From 2a85d6da455831df63263be4af29d1991eba06f7 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 17 Mar 2026 03:14:04 +0000 Subject: [PATCH 175/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 9095dd61dd8..c86b9b73bbc 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -97,9 +97,9 @@ tr { All-time contributions4,555 -Lines of code changed2,780,582 +Lines of code changed2,780,301 -Repository views (past two weeks)1,682 +Repository views (past two weeks)1,560 Repositories with contributions129 From 714fe16dbc20949678809b305c16c4e11b182909 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 18 Mar 2026 03:00:45 +0000 Subject: [PATCH 176/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index c86b9b73bbc..662dba828a6 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,561 -Forks1,168 +Forks1,169 All-time contributions4,555 -Lines of code changed2,780,301 +Lines of code changed2,780,582 -Repository views (past two weeks)1,560 +Repository views (past two weeks)1,567 Repositories with contributions129 From 79e05974156814bd7cb0c5fc299904f607e93010 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 19 Mar 2026 03:05:59 +0000 Subject: [PATCH 177/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 662dba828a6..70101aaac01 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,7 +91,7 @@ tr { -Stars7,561 +Stars7,560 Forks1,169 @@ -99,7 +99,7 @@ tr { Lines of code changed2,780,582 -Repository views (past two weeks)1,567 +Repository views (past two weeks)1,527 Repositories with contributions129 From c963e025c416560eab304969bbb96f6c7ae88827 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 18 Mar 2026 22:20:37 -0400 Subject: [PATCH 178/303] Fix Chrome/Safari rendering bug (close #57, #71) --- templates/languages.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/languages.svg b/templates/languages.svg index 66b9b62844a..a3754df18be 100644 --- a/templates/languages.svg +++ b/templates/languages.svg @@ -65,7 +65,7 @@ li { } div.ellipsis { - height: 100%; + height: 176px; overflow: hidden; text-overflow: ellipsis; } From 9edf953cde65827fa2485c757ffdf88826f162dc Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 20 Mar 2026 02:56:14 +0000 Subject: [PATCH 179/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 70101aaac01..a2ae338caae 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,7 +91,7 @@ tr { -Stars7,560 +Stars7,563 Forks1,169 @@ -99,7 +99,7 @@ tr { Lines of code changed2,780,582 -Repository views (past two weeks)1,527 +Repository views (past two weeks)1,515 Repositories with contributions129 From 44576f266a2a1244331a8fbfd86c2a3ccb104ab5 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Thu, 19 Mar 2026 23:12:57 -0400 Subject: [PATCH 180/303] Add glob matching with tests --- build.zig | 6 ++++ src/glob.zig | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 6 ++++ 3 files changed, 101 insertions(+) create mode 100644 src/glob.zig diff --git a/build.zig b/build.zig index cc6b9c6dd23..40721988646 100644 --- a/build.zig +++ b/build.zig @@ -23,4 +23,10 @@ pub fn build(b: *std.Build) void { if (b.args) |args| { run_cmd.addArgs(args); } + + const tests = b.addTest(.{ .root_module = exe.root_module }); + const run_tests = b.addRunArtifact(tests); + run_tests.step.dependOn(b.getInstallStep()); + const test_step = b.step("test", "Run the tests"); + test_step.dependOn(&run_tests.step); } diff --git a/src/glob.zig b/src/glob.zig new file mode 100644 index 00000000000..8a5e049dc96 --- /dev/null +++ b/src/glob.zig @@ -0,0 +1,89 @@ +const std = @import("std"); + +/// Recursive-backtracking glob matching. Potentially very slow if there are a +/// lot of globs. Good enough for now, though. (If it's good enough for the GNU +/// glob function, it's good enough for me.) +/// +/// Max recursion depth is the number of asterisks in the globbing pattern plus +/// one. +pub fn match(pattern: []const u8, s: []const u8) bool { + if (std.mem.indexOfScalar(u8, pattern, '*')) |star_offset| { + if (!std.mem.startsWith(u8, s, pattern[0..star_offset])) { + return false; + } + const rest = pattern[star_offset + 1 ..]; + for (0..s.len + 1) |glob_end| { + if (match(rest, s[glob_end..])) { + return true; + } + } + return false; + } else { + return std.mem.eql(u8, pattern, s); + } +} + +pub fn matchAny(patterns: []const []const u8, s: []const u8) bool { + for (patterns) |pattern| { + if (match(pattern, s)) { + return true; + } + } + return false; +} + +test match { + const testing = std.testing; + + try testing.expect(match("", "")); + try testing.expect(match("*", "")); + try testing.expect(match("**", "")); + try testing.expect(match("***", "")); + + try testing.expect(match("*", "a")); + try testing.expect(match("**", "a")); + try testing.expect(match("***", "a")); + + try testing.expect(match("*", "abcd")); + try testing.expect(match("**", "abcd")); + try testing.expect(match("****", "abcd")); + try testing.expect(match("****d", "abcd")); + try testing.expect(match("a****", "abcd")); + try testing.expect(match("a****d", "abcd")); + try testing.expect(!match("****c", "abcd")); + + try testing.expect(match("abc", "abc")); + try testing.expect(!match("abc", "abcd")); + try testing.expect(!match("abc", "dabc")); + try testing.expect(!match("abc", "dabcd")); + + try testing.expect(match("*abc", "dabc")); + try testing.expect(!match("*abc", "dabcd")); + + try testing.expect(match("abc*", "abcd")); + try testing.expect(!match("abc*", "dabcd")); + + try testing.expect(match("*abc*", "abc")); + try testing.expect(match("*abc*", "dabc")); + try testing.expect(match("*abc*", "abcd")); + try testing.expect(match("*abc*", "dabcd")); + + try testing.expect(!match("*c*", "this is a test")); + try testing.expect(match("*e*", "this is a test")); + + try testing.expect(match("som*thing", "something")); + try testing.expect(match("som*thing", "someeeething")); + try testing.expect(match("som*thing", "som thing")); + try testing.expect(match("som*thing", "somabcthing")); + try testing.expect(match("som*thing", "somthing")); + + try testing.expect(match("s*a*s*s*s*s*s*s*s*s", "sssssssssssssassssssssss")); + try testing.expect(match("s*s*s*s*s*s*s*s*s*s", "sssssssssssssassssssssss")); + try testing.expect(match("s*s*s*s*s*s*s*s*a*s", "sssssssssssssassssssssss")); + + // Globbing here doesn't separate on slashes like globbing in the shell + try testing.expect(match("*", "///")); + try testing.expect(match("*", "/asdf//")); + try testing.expect(match("/*sdf/*/*", "/asdf//")); + try testing.expect(match("/*sdf/*", "/asdf//")); +} diff --git a/src/main.zig b/src/main.zig index 01674649155..6444ae40128 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,7 @@ const builtin = @import("builtin"); const std = @import("std"); const argparse = @import("argparse.zig"); +const glob = @import("glob.zig"); const HttpClient = @import("http_client.zig"); const Statistics = @import("statistics.zig"); @@ -77,4 +78,9 @@ pub fn main() !void { } // TODO: Output images from templates + _ = glob; +} + +test { + std.testing.refAllDecls(@This()); } From 8d45af07562ed0af4d175a40ec512ecf914d4f86 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Thu, 19 Mar 2026 23:12:57 -0400 Subject: [PATCH 181/303] Add glob matching with tests --- build.zig | 5 +++ src/glob.zig | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 6 ++++ 3 files changed, 100 insertions(+) create mode 100644 src/glob.zig diff --git a/build.zig b/build.zig index cc6b9c6dd23..f69ae1fce35 100644 --- a/build.zig +++ b/build.zig @@ -23,4 +23,9 @@ pub fn build(b: *std.Build) void { if (b.args) |args| { run_cmd.addArgs(args); } + + const tests = b.addTest(.{ .root_module = exe.root_module }); + const run_tests = b.addRunArtifact(tests); + const test_step = b.step("test", "Run the tests"); + test_step.dependOn(&run_tests.step); } diff --git a/src/glob.zig b/src/glob.zig new file mode 100644 index 00000000000..8a5e049dc96 --- /dev/null +++ b/src/glob.zig @@ -0,0 +1,89 @@ +const std = @import("std"); + +/// Recursive-backtracking glob matching. Potentially very slow if there are a +/// lot of globs. Good enough for now, though. (If it's good enough for the GNU +/// glob function, it's good enough for me.) +/// +/// Max recursion depth is the number of asterisks in the globbing pattern plus +/// one. +pub fn match(pattern: []const u8, s: []const u8) bool { + if (std.mem.indexOfScalar(u8, pattern, '*')) |star_offset| { + if (!std.mem.startsWith(u8, s, pattern[0..star_offset])) { + return false; + } + const rest = pattern[star_offset + 1 ..]; + for (0..s.len + 1) |glob_end| { + if (match(rest, s[glob_end..])) { + return true; + } + } + return false; + } else { + return std.mem.eql(u8, pattern, s); + } +} + +pub fn matchAny(patterns: []const []const u8, s: []const u8) bool { + for (patterns) |pattern| { + if (match(pattern, s)) { + return true; + } + } + return false; +} + +test match { + const testing = std.testing; + + try testing.expect(match("", "")); + try testing.expect(match("*", "")); + try testing.expect(match("**", "")); + try testing.expect(match("***", "")); + + try testing.expect(match("*", "a")); + try testing.expect(match("**", "a")); + try testing.expect(match("***", "a")); + + try testing.expect(match("*", "abcd")); + try testing.expect(match("**", "abcd")); + try testing.expect(match("****", "abcd")); + try testing.expect(match("****d", "abcd")); + try testing.expect(match("a****", "abcd")); + try testing.expect(match("a****d", "abcd")); + try testing.expect(!match("****c", "abcd")); + + try testing.expect(match("abc", "abc")); + try testing.expect(!match("abc", "abcd")); + try testing.expect(!match("abc", "dabc")); + try testing.expect(!match("abc", "dabcd")); + + try testing.expect(match("*abc", "dabc")); + try testing.expect(!match("*abc", "dabcd")); + + try testing.expect(match("abc*", "abcd")); + try testing.expect(!match("abc*", "dabcd")); + + try testing.expect(match("*abc*", "abc")); + try testing.expect(match("*abc*", "dabc")); + try testing.expect(match("*abc*", "abcd")); + try testing.expect(match("*abc*", "dabcd")); + + try testing.expect(!match("*c*", "this is a test")); + try testing.expect(match("*e*", "this is a test")); + + try testing.expect(match("som*thing", "something")); + try testing.expect(match("som*thing", "someeeething")); + try testing.expect(match("som*thing", "som thing")); + try testing.expect(match("som*thing", "somabcthing")); + try testing.expect(match("som*thing", "somthing")); + + try testing.expect(match("s*a*s*s*s*s*s*s*s*s", "sssssssssssssassssssssss")); + try testing.expect(match("s*s*s*s*s*s*s*s*s*s", "sssssssssssssassssssssss")); + try testing.expect(match("s*s*s*s*s*s*s*s*a*s", "sssssssssssssassssssssss")); + + // Globbing here doesn't separate on slashes like globbing in the shell + try testing.expect(match("*", "///")); + try testing.expect(match("*", "/asdf//")); + try testing.expect(match("/*sdf/*/*", "/asdf//")); + try testing.expect(match("/*sdf/*", "/asdf//")); +} diff --git a/src/main.zig b/src/main.zig index 01674649155..6444ae40128 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,7 @@ const builtin = @import("builtin"); const std = @import("std"); const argparse = @import("argparse.zig"); +const glob = @import("glob.zig"); const HttpClient = @import("http_client.zig"); const Statistics = @import("statistics.zig"); @@ -77,4 +78,9 @@ pub fn main() !void { } // TODO: Output images from templates + _ = glob; +} + +test { + std.testing.refAllDecls(@This()); } From 8034efae77df85a47a52f8ee60c427d30e6a3b85 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 21 Mar 2026 02:48:59 +0000 Subject: [PATCH 182/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index a2ae338caae..4d2e091c0c8 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -97,9 +97,9 @@ tr { All-time contributions4,555 -Lines of code changed2,780,582 +Lines of code changed2,780,566 -Repository views (past two weeks)1,515 +Repository views (past two weeks)1,498 Repositories with contributions129 From 58b198be68a1f2061ac4297d1f62de6d75f7f272 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 21 Mar 2026 11:55:23 -0400 Subject: [PATCH 183/303] Clean up and add some globbing tests --- src/glob.zig | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/glob.zig b/src/glob.zig index 8a5e049dc96..01de192828d 100644 --- a/src/glob.zig +++ b/src/glob.zig @@ -4,8 +4,7 @@ const std = @import("std"); /// lot of globs. Good enough for now, though. (If it's good enough for the GNU /// glob function, it's good enough for me.) /// -/// Max recursion depth is the number of asterisks in the globbing pattern plus -/// one. +/// Max recursion depth is the number of stars in the globbing pattern plus one. pub fn match(pattern: []const u8, s: []const u8) bool { if (std.mem.indexOfScalar(u8, pattern, '*')) |star_offset| { if (!std.mem.startsWith(u8, s, pattern[0..star_offset])) { @@ -77,9 +76,18 @@ test match { try testing.expect(match("som*thing", "somabcthing")); try testing.expect(match("som*thing", "somthing")); - try testing.expect(match("s*a*s*s*s*s*s*s*s*s", "sssssssssssssassssssssss")); - try testing.expect(match("s*s*s*s*s*s*s*s*s*s", "sssssssssssssassssssssss")); - try testing.expect(match("s*s*s*s*s*s*s*s*a*s", "sssssssssssssassssssssss")); + try testing.expect(match( + "s*a" ++ "*s" ** 8, + "s" ** 10 ++ "a" ++ "s" ** 10, + )); + try testing.expect(match( + "s" ++ "*s" ** 8, + "s" ** 10 ++ "a" ++ "s" ** 10, + )); + try testing.expect(match( + "s*" ** 8 ++ "a*s", + "s" ** 10 ++ "a" ++ "s" ** 10, + )); // Globbing here doesn't separate on slashes like globbing in the shell try testing.expect(match("*", "///")); @@ -87,3 +95,11 @@ test match { try testing.expect(match("/*sdf/*/*", "/asdf//")); try testing.expect(match("/*sdf/*", "/asdf//")); } + +test matchAny { + const testing = std.testing; + + try testing.expect(matchAny(&.{ "*waw", "wew*", "wow", "www" }, "wow")); + try testing.expect(!matchAny(&.{ "*waw", "wew*", "www" }, "wow")); + try testing.expect(matchAny(&.{ "w*w", "www" }, "wow")); +} From 38b2de5f5b01762ef28c56eccebfac879ca3dd99 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 21 Mar 2026 12:52:03 -0400 Subject: [PATCH 184/303] Add --silent CLI arg --- src/main.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 6444ae40128..d79665ad832 100644 --- a/src/main.zig +++ b/src/main.zig @@ -31,6 +31,7 @@ fn logFn( const Args = struct { api_key: []const u8, json_output_file: ?[]const u8 = null, + silent: bool = false, verbose: bool = false, pub fn deinit(self: @This()) void { @@ -46,7 +47,9 @@ pub fn main() !void { const args = try argparse.parse(allocator, Args); defer args.deinit(); - if (args.verbose) { + if (args.silent) { + log_level = .err; + } else if (args.verbose) { log_level = .debug; } From 6f895bbcae016d67171e0972176581d70128a800 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 21 Mar 2026 13:21:40 -0400 Subject: [PATCH 185/303] Print CLI arg errors to stderror --- src/argparse.zig | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/argparse.zig b/src/argparse.zig index 093ffce76e9..13366b48fee 100644 --- a/src/argparse.zig +++ b/src/argparse.zig @@ -3,12 +3,15 @@ const std = @import("std"); // Since parse is the only public function, these variables can be set there and // used globally. var stdout: *std.Io.Writer = undefined; +var stderr: *std.Io.Writer = undefined; var arena: std.heap.ArenaAllocator = undefined; var allocator: std.mem.Allocator = undefined; pub fn parse(gpa: std.mem.Allocator, T: type) !T { var stdout_writer = std.fs.File.stdout().writer(&.{}); stdout = &stdout_writer.interface; + var stderr_writer = std.fs.File.stderr().writer(&.{}); + stderr = &stderr_writer.interface; allocator = gpa; arena = .init(allocator); @@ -37,7 +40,7 @@ pub fn parse(gpa: std.mem.Allocator, T: type) !T { if (@typeInfo(strip_optional(field.type)) == .bool) { @field(result, field.name) = false; } else { - try stdout.print( + try stderr.print( "Missing required argument {s}\n", .{field.name}, ); @@ -69,7 +72,7 @@ fn setFromCli( // TODO: Handle one-letter arguments if (!std.mem.startsWith(u8, raw_arg, "--")) { - try stdout.print("Unknown argument: '{s}'\n", .{raw_arg}); + try stderr.print("Unknown argument: '{s}'\n", .{raw_arg}); try printUsage(T, args[0]); std.process.exit(1); } @@ -85,7 +88,7 @@ fn setFromCli( } else { i += 1; if (i >= args.len) { - try stdout.print( + try stderr.print( "Missing required value for argument {s} {s}\n", .{ raw_arg, field.name }, ); @@ -110,7 +113,7 @@ fn setFromCli( } } - try stdout.print("Unknown argument: '{s}'\n", .{raw_arg}); + try stderr.print("Unknown argument: '{s}'\n", .{raw_arg}); try printUsage(T, args[0]); std.process.exit(1); } From 5a627a0f7cb4dba1c4ee979dc7fa8c3d886536f6 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 21 Mar 2026 13:50:27 -0400 Subject: [PATCH 186/303] Add additional error checking to argparse --- src/argparse.zig | 13 ++++++++++++- src/main.zig | 31 +++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/argparse.zig b/src/argparse.zig index 13366b48fee..8631cdb99f3 100644 --- a/src/argparse.zig +++ b/src/argparse.zig @@ -7,7 +7,11 @@ var stderr: *std.Io.Writer = undefined; var arena: std.heap.ArenaAllocator = undefined; var allocator: std.mem.Allocator = undefined; -pub fn parse(gpa: std.mem.Allocator, T: type) !T { +pub fn parse( + gpa: std.mem.Allocator, + T: type, + errorCheck: ?fn (args: T, stderr: *std.Io.Writer) anyerror!bool, +) !T { var stdout_writer = std.fs.File.stdout().writer(&.{}); stdout = &stdout_writer.interface; var stderr_writer = std.fs.File.stderr().writer(&.{}); @@ -50,6 +54,13 @@ pub fn parse(gpa: std.mem.Allocator, T: type) !T { } } + if (errorCheck) |check| { + if (!(try check(result, stderr))) { + try printUsage(T, args[0]); + std.process.exit(1); + } + } + return result; } diff --git a/src/main.zig b/src/main.zig index d79665ad832..b7011023cfd 100644 --- a/src/main.zig +++ b/src/main.zig @@ -29,13 +29,14 @@ fn logFn( } const Args = struct { - api_key: []const u8, + api_key: ?[]const u8 = null, + json_input_file: ?[]const u8 = null, json_output_file: ?[]const u8 = null, silent: bool = false, verbose: bool = false, pub fn deinit(self: @This()) void { - allocator.free(self.api_key); + if (self.api_key) |key| allocator.free(key); if (self.json_output_file) |output| allocator.free(output); } }; @@ -45,7 +46,18 @@ pub fn main() !void { defer _ = gpa.deinit(); allocator = gpa.allocator(); - const args = try argparse.parse(allocator, Args); + const args = try argparse.parse(allocator, Args, struct { + fn errorCheck(a: Args, stderr: *std.Io.Writer) !bool { + if (a.api_key == null and a.json_input_file == null) { + try stderr.print( + "You must pass either an input file or an API key.\n", + .{}, + ); + return false; + } + return true; + } + }.errorCheck); defer args.deinit(); if (args.silent) { log_level = .err; @@ -53,9 +65,16 @@ pub fn main() !void { log_level = .debug; } - var client: HttpClient = try .init(allocator, args.api_key); - defer client.deinit(); - const stats = try Statistics.init(&client, allocator); + var stats: Statistics = undefined; + if (args.json_input_file) |infile| { + // TODO + _ = infile; + return error.NotImplementedYet; + } else if (args.api_key) |api_key| { + var client: HttpClient = try .init(allocator, api_key); + defer client.deinit(); + stats = try Statistics.init(&client, allocator); + } else unreachable; defer stats.deinit(); if (args.json_output_file) |path| { From 44214fa3571a370c6a2d2e1e621fc9a306e3fb95 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 21 Mar 2026 17:26:12 -0400 Subject: [PATCH 187/303] Optionally initialize stats from JSON file --- src/main.zig | 20 ++++++++++++--- src/statistics.zig | 62 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/src/main.zig b/src/main.zig index b7011023cfd..20dbd1c8c12 100644 --- a/src/main.zig +++ b/src/main.zig @@ -37,6 +37,7 @@ const Args = struct { pub fn deinit(self: @This()) void { if (self.api_key) |key| allocator.free(key); + if (self.json_input_file) |input| allocator.free(input); if (self.json_output_file) |output| allocator.free(output); } }; @@ -66,10 +67,21 @@ pub fn main() !void { } var stats: Statistics = undefined; - if (args.json_input_file) |infile| { - // TODO - _ = infile; - return error.NotImplementedYet; + if (args.json_input_file) |path| { + const in = + if (std.mem.eql(u8, path, "-")) + std.fs.File.stdin() + else + try std.fs.cwd().openFile(path, .{}); + defer in.close(); + var read_buffer: [64 * 1024]u8 = undefined; + var reader = in.reader(&read_buffer); + // TODO: Create a scanner from the reader instead of reading the whole + // file into memory + const data = + try (&reader.interface).allocRemaining(allocator, .unlimited); + defer allocator.free(data); + stats = try Statistics.initFromJson(allocator, data); } else if (args.api_key) |api_key| { var client: HttpClient = try .init(allocator, api_key); defer client.deinit(); diff --git a/src/statistics.zig b/src/statistics.zig index d56ab87f5bd..e9c7d24644a 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -13,17 +13,6 @@ review_contributions: u32 = 0, var allocator: std.mem.Allocator = undefined; const Statistics = @This(); -const Language = struct { - name: []const u8, - size: u32, - color: []const u8, - - pub fn deinit(self: @This()) void { - allocator.free(self.name); - allocator.free(self.color); - } -}; - const Repository = struct { name: []const u8, stars: u32, @@ -99,6 +88,17 @@ const Repository = struct { } }; +const Language = struct { + name: []const u8, + size: u32, + color: []const u8, + + pub fn deinit(self: @This()) void { + allocator.free(self.name); + allocator.free(self.color); + } +}; + pub fn init(client: *HttpClient, a: std.mem.Allocator) !Statistics { allocator = a; var arena = std.heap.ArenaAllocator.init(allocator); @@ -110,6 +110,20 @@ pub fn init(client: *HttpClient, a: std.mem.Allocator) !Statistics { return self; } +pub fn initFromJson(a: std.mem.Allocator, s: []const u8) !Statistics { + allocator = a; + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + const parsed = try std.json.parseFromSliceLeaky( + Statistics, + arena.allocator(), + s, + .{ .ignore_unknown_fields = true }, + ); + return try deepcopy(allocator, parsed); +} + pub fn deinit(self: Statistics) void { for (self.repositories) |repository| { repository.deinit(); @@ -441,3 +455,29 @@ fn get_lines_changed( } } } + +fn deepcopy(a: std.mem.Allocator, o: anytype) !@TypeOf(o) { + return switch (@typeInfo(@TypeOf(o))) { + .pointer => |p| switch (p.size) { + .slice => v: { + const result = try a.dupe(p.child, o); + for (o, result) |src, *dest| { + dest.* = try deepcopy(a, src); + } + break :v result; + }, + // Only slices in this struct + else => comptime unreachable, + }, + .@"struct" => |s| v: { + var result = o; + inline for (s.fields) |field| { + @field(result, field.name) = + try deepcopy(a, @field(o, field.name)); + } + break :v result; + }, + .optional => if (o) |v| try deepcopy(a, v) else null, + else => o, + }; +} From 675c47a8f7a66c173650034b61fc6008de0bdf81 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 22 Mar 2026 03:07:00 +0000 Subject: [PATCH 188/303] Update generated files --- generated/languages.svg | 28 ++++++++++++++-------------- generated/overview.svg | 6 +++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 8abb24ab0aa..20c2ef27811 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -28.93% +28.91% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.49% +17.48% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.31% +9.30% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.50% +7.49% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.59% +6.58% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.28% +5.27% @@ -270,24 +270,24 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> OpenSCAD -0.35% +0.34%
  • - -Vim Script -0.19% +TypeScript +0.23%
  • - -TypeScript +Vim Script 0.19%
  • @@ -342,7 +342,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Just -0.10% +0.11% diff --git a/generated/overview.svg b/generated/overview.svg index 4d2e091c0c8..e830da5bce5 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,563 +Stars7,566 Forks1,169 All-time contributions4,555 -Lines of code changed2,780,566 +Lines of code changed2,780,582 -Repository views (past two weeks)1,498 +Repository views (past two weeks)1,433 Repositories with contributions129 From f9e8a65d8e92120a42f3171e419861000fcee862 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 23 Mar 2026 03:09:56 +0000 Subject: [PATCH 189/303] Update generated files --- generated/languages.svg | 14 +++++++------- generated/overview.svg | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 20c2ef27811..c996af234ee 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -28.91% +28.90% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.48% +17.46% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -11.91% +11.98% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.30% +9.29% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.49% +7.48% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.38% +2.37% diff --git a/generated/overview.svg b/generated/overview.svg index e830da5bce5..16553c22edf 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,566 -Forks1,169 +Forks1,170 All-time contributions4,555 -Lines of code changed2,780,582 +Lines of code changed2,780,490 -Repository views (past two weeks)1,433 +Repository views (past two weeks)1,477 Repositories with contributions129 From 03a74b05174596aba169dfc85d5a6ff81ce0232a Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 23 Mar 2026 01:12:11 -0400 Subject: [PATCH 190/303] Fix minor bugs --- src/http_client.zig | 9 ++------- src/statistics.zig | 5 ++++- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/http_client.zig b/src/http_client.zig index d3ea8c4860d..8b2c3a9dacc 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -8,7 +8,6 @@ gpa: std.mem.Allocator, arena: *std.heap.ArenaAllocator, client: std.http.Client, bearer: []const u8, -last_request: ?i64 = null, const Self = @This(); const Response = struct { []const u8, std.http.Status }; @@ -41,8 +40,7 @@ pub fn get( self.arena.allocator(), 1024, ); - defer writer.deinit(); - const now = std.time.timestamp(); + errdefer writer.deinit(); const status = (try (self.client.fetch(.{ .location = .{ .url = url }, .response_writer = &writer.writer, @@ -65,7 +63,6 @@ pub fn get( }, else => err, })).status; - self.last_request = now; return .{ try writer.toOwnedSlice(), status }; } @@ -79,8 +76,7 @@ pub fn post( self.arena.allocator(), 1024, ); - defer writer.deinit(); - const now = std.time.timestamp(); + errdefer writer.deinit(); const status = (try (self.client.fetch(.{ .location = .{ .url = url }, .response_writer = &writer.writer, @@ -103,7 +99,6 @@ pub fn post( }, else => err, })).status; - self.last_request = now; return .{ try writer.toOwnedSlice(), status }; } diff --git a/src/statistics.zig b/src/statistics.zig index e9c7d24644a..5845b4a9af1 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -321,6 +321,8 @@ fn get_repos( }; if (raw_repo.languages) |repo_languages| { + // TODO: Properly free partially initialized memory when any try + // fails in this block if (repo_languages.edges) |raw_languages| { repository.languages = try allocator.alloc( Language, @@ -334,7 +336,7 @@ fn get_repos( .name = try allocator.dupe(u8, raw.node.name), .size = raw.size, // TODO: Add sensible default color - .color = "", + .color = try allocator.dupe(u8, ""), }; if (raw.node.color) |color| { language.color = try allocator.dupe(u8, color); @@ -443,6 +445,7 @@ fn get_lines_changed( // Exponential backoff (in expectation) with jitter item.delay += std.crypto.random.intRangeAtMost(i64, 2, item.delay); + item.delay = @min(item.delay, 240); try q.add(item); }, else => |status| { From 673b4bbc5a020b749016378979562462a1b0c1a0 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 24 Mar 2026 03:05:01 +0000 Subject: [PATCH 191/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 16553c22edf..f6e010ff52e 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,566 +Stars7,567 Forks1,170 All-time contributions4,555 -Lines of code changed2,780,490 +Lines of code changed2,779,976 -Repository views (past two weeks)1,477 +Repository views (past two weeks)1,498 Repositories with contributions129 From 4017075ded494b110cb9f302e106ca408f735bd0 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Mar 2026 09:35:49 -0400 Subject: [PATCH 192/303] Add HTTP client retry limit --- src/http_client.zig | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/http_client.zig b/src/http_client.zig index 8b2c3a9dacc..36b6c8eb1da 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -35,7 +35,12 @@ pub fn get( url: []const u8, headers: std.http.Client.Request.Headers, extra_headers: []const std.http.Header, + retries: isize, ) !Response { + if (retries <= -1) { + return error.TooManyRetries; + } + var writer = try std.Io.Writer.Allocating.initCapacity( self.arena.allocator(), 1024, @@ -50,7 +55,7 @@ pub fn get( error.HttpConnectionClosing => { // Handle a Zig HTTP bug where keep-alive connections are closed by // the server after a timeout, but the client doesn't handle it - // properly. For now we nuke the whole client (and associate + // properly. For now we nuke the whole client (and associated // connection pool) and make a new one, but there might be a better // way to handle this. std.log.debug( @@ -59,7 +64,7 @@ pub fn get( ); self.client.deinit(); self.client = .{ .allocator = self.arena.allocator() }; - return self.get(url, headers, extra_headers); + return self.get(url, headers, extra_headers, retries - 1); }, else => err, })).status; @@ -71,7 +76,12 @@ pub fn post( url: []const u8, body: []const u8, headers: std.http.Client.Request.Headers, + retries: isize, ) !Response { + if (retries <= -1) { + return error.TooManyRetries; + } + var writer = try std.Io.Writer.Allocating.initCapacity( self.arena.allocator(), 1024, @@ -86,7 +96,7 @@ pub fn post( error.HttpConnectionClosing => { // Handle a Zig HTTP bug where keep-alive connections are closed by // the server after a timeout, but the client doesn't handle it - // properly. For now we nuke the whole client (and associate + // properly. For now we nuke the whole client (and associated // connection pool) and make a new one, but there might be a better // way to handle this. std.log.debug( @@ -95,7 +105,7 @@ pub fn post( ); self.client.deinit(); self.client = .{ .allocator = self.arena.allocator() }; - return self.post(url, body, headers); + return self.post(url, body, headers, retries - 1); }, else => err, })).status; @@ -125,6 +135,7 @@ pub fn graphql( .authorization = .{ .override = self.bearer }, .content_type = .{ .override = "application/json" }, }, + 8, ); } @@ -139,5 +150,6 @@ pub fn rest( .content_type = .{ .override = "application/json" }, }, &.{.{ .name = "X-GitHub-Api-Version", .value = "2022-11-28" }}, + 8, ); } From 804234d0dacb811fa309946a88c4151a50ac7b9d Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Mar 2026 09:46:53 -0400 Subject: [PATCH 193/303] Improve serialization of GraphQL variables structs --- src/http_client.zig | 10 +++------- src/statistics.zig | 23 ++++++++++++----------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/http_client.zig b/src/http_client.zig index 36b6c8eb1da..f735593b1eb 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -112,22 +112,18 @@ pub fn post( return .{ try writer.toOwnedSlice(), status }; } -const Query = struct { - query: []const u8, - variables: ?[]const u8, -}; - pub fn graphql( self: *Self, body: []const u8, - variables: ?[]const u8, + variables: anytype, ) !Response { var arena = std.heap.ArenaAllocator.init(self.arena.allocator()); defer arena.deinit(); const allocator = arena.allocator(); + return try self.post( "https://api.github.com/graphql", - try std.json.Stringify.valueAlloc(allocator, Query{ + try std.json.Stringify.valueAlloc(allocator, .{ .query = body, .variables = variables, }, .{}), diff --git a/src/statistics.zig b/src/statistics.zig index 5845b4a9af1..2ca1653107d 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -236,17 +236,18 @@ fn get_repos( \\ } \\} , - // NOTE: Replace with actual JSON serialization if using more - // complex tyeps. This is fine as long as we're only using numbers. - try std.fmt.allocPrint( - arena.allocator(), - \\{{ - \\ "from": "{d}-01-01T00:00:00Z", - \\ "to": "{d}-01-01T00:00:00Z" - \\}} - , - .{ year, year + 1 }, - ), + .{ + .from = try std.fmt.allocPrint( + arena.allocator(), + "{d}-01-01T00:00:00Z", + .{year}, + ), + .to = try std.fmt.allocPrint( + arena.allocator(), + "{d}-01-01T00:00:00Z", + .{year + 1}, + ), + }, ); if (status != .ok) { std.log.err( From ff92a409744b8578f6588e81a47f7d36fe4e8d23 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Mar 2026 10:01:49 -0400 Subject: [PATCH 194/303] Add pathologically slow test case --- src/glob.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/glob.zig b/src/glob.zig index 01de192828d..5315f9a38ec 100644 --- a/src/glob.zig +++ b/src/glob.zig @@ -88,6 +88,8 @@ test match { "s*" ** 8 ++ "a*s", "s" ** 10 ++ "a" ++ "s" ** 10, )); + // Trigger slow (exponential) worst-case + try testing.expect(!match("s*" ** 8 ++ "a", "s" ** 30)); // Globbing here doesn't separate on slashes like globbing in the shell try testing.expect(match("*", "///")); From 854504eef7d904169c22cdf601ae705c4928547c Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Mar 2026 10:01:56 -0400 Subject: [PATCH 195/303] Fix color memory leak --- src/statistics.zig | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/statistics.zig b/src/statistics.zig index 2ca1653107d..21761deca0a 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -91,11 +91,11 @@ const Repository = struct { const Language = struct { name: []const u8, size: u32, - color: []const u8, + color: ?[]const u8 = null, pub fn deinit(self: @This()) void { allocator.free(self.name); - allocator.free(self.color); + if (self.color) |color| allocator.free(color); } }; @@ -300,7 +300,7 @@ fn get_repos( result.review_contributions += stats.totalPullRequestReviewContributions; - // TODO: if there are 100 ore more repositories, we should subdivide + // TODO: if there are 100 or more repositories, we should subdivide // the date range in half for (stats.commitContributionsByRepository) |x| { @@ -336,8 +336,6 @@ fn get_repos( language.* = .{ .name = try allocator.dupe(u8, raw.node.name), .size = raw.size, - // TODO: Add sensible default color - .color = try allocator.dupe(u8, ""), }; if (raw.node.color) |color| { language.color = try allocator.dupe(u8, color); From 0a737f5f87b0e1eaa73be520b90ece1756cae87e Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Mar 2026 14:07:10 -0400 Subject: [PATCH 196/303] Parse excluded repos and languages --- src/main.zig | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/main.zig b/src/main.zig index 20dbd1c8c12..a4d1a589c01 100644 --- a/src/main.zig +++ b/src/main.zig @@ -34,11 +34,15 @@ const Args = struct { json_output_file: ?[]const u8 = null, silent: bool = false, verbose: bool = false, + excluded_repos: ?[]const u8 = null, + excluded_langs: ?[]const u8 = null, pub fn deinit(self: @This()) void { - if (self.api_key) |key| allocator.free(key); - if (self.json_input_file) |input| allocator.free(input); - if (self.json_output_file) |output| allocator.free(output); + if (self.api_key) |s| allocator.free(s); + if (self.json_input_file) |s| allocator.free(s); + if (self.json_output_file) |s| allocator.free(s); + if (self.excluded_repos) |s| allocator.free(s); + if (self.excluded_langs) |s| allocator.free(s); } }; @@ -65,6 +69,26 @@ pub fn main() !void { } else if (args.verbose) { log_level = .debug; } + const excluded_repos = if (args.excluded_repos) |excluded| excluded: { + var list = try std.ArrayList([]const u8).initCapacity(allocator, 16); + errdefer list.deinit(allocator); + var iterator = std.mem.tokenizeAny(u8, excluded, ", \t\r\n|\"'\x00"); + while (iterator.next()) |pattern| { + try list.append(allocator, pattern); + } + break :excluded try list.toOwnedSlice(allocator); + } else null; + defer if (excluded_repos) |excluded| allocator.free(excluded); + const excluded_langs = if (args.excluded_langs) |excluded| excluded: { + var list = try std.ArrayList([]const u8).initCapacity(allocator, 16); + errdefer list.deinit(allocator); + var iterator = std.mem.tokenizeAny(u8, excluded, ", \t\r\n|\"'\x00"); + while (iterator.next()) |pattern| { + try list.append(allocator, pattern); + } + break :excluded try list.toOwnedSlice(allocator); + } else null; + defer if (excluded_langs) |excluded| allocator.free(excluded); var stats: Statistics = undefined; if (args.json_input_file) |path| { From 9fb4ffa4a44995834f5e1248acc7f70f53e49acf Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Mar 2026 14:49:03 -0400 Subject: [PATCH 197/303] Calculate language totals with exclusions --- src/glob.zig | 7 +++++-- src/main.zig | 26 ++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/glob.zig b/src/glob.zig index 5315f9a38ec..a296b16c5cf 100644 --- a/src/glob.zig +++ b/src/glob.zig @@ -7,7 +7,10 @@ const std = @import("std"); /// Max recursion depth is the number of stars in the globbing pattern plus one. pub fn match(pattern: []const u8, s: []const u8) bool { if (std.mem.indexOfScalar(u8, pattern, '*')) |star_offset| { - if (!std.mem.startsWith(u8, s, pattern[0..star_offset])) { + if (!(star_offset <= s.len and std.ascii.eqlIgnoreCase( + s[0..star_offset], + pattern[0..star_offset], + ))) { return false; } const rest = pattern[star_offset + 1 ..]; @@ -18,7 +21,7 @@ pub fn match(pattern: []const u8, s: []const u8) bool { } return false; } else { - return std.mem.eql(u8, pattern, s); + return std.ascii.eqlIgnoreCase(pattern, s); } } diff --git a/src/main.zig b/src/main.zig index a4d1a589c01..a9623c34a59 100644 --- a/src/main.zig +++ b/src/main.zig @@ -82,9 +82,9 @@ pub fn main() !void { const excluded_langs = if (args.excluded_langs) |excluded| excluded: { var list = try std.ArrayList([]const u8).initCapacity(allocator, 16); errdefer list.deinit(allocator); - var iterator = std.mem.tokenizeAny(u8, excluded, ", \t\r\n|\"'\x00"); + var iterator = std.mem.tokenizeAny(u8, excluded, ",\t\r\n|\"'\x00"); while (iterator.next()) |pattern| { - try list.append(allocator, pattern); + try list.append(allocator, std.mem.trim(u8, pattern, " ")); } break :excluded try list.toOwnedSlice(allocator); } else null; @@ -135,6 +135,28 @@ pub fn main() !void { try writer.interface.flush(); } + var languages = std.StringArrayHashMap(u64).init(allocator); + defer languages.deinit(); + for (stats.repositories) |repository| { + if (glob.matchAny(excluded_repos orelse &.{}, repository.name)) { + continue; + } + if (repository.languages) |langs| for (langs) |language| { + if (glob.matchAny(excluded_langs orelse &.{}, language.name)) { + continue; + } + var total = languages.get(language.name) orelse 0; + total += language.size; + try languages.put(language.name, total); + }; + } + for ( + languages.unmanaged.entries.slice().items(.key), + languages.unmanaged.entries.slice().items(.value), + ) |key, value| { + std.debug.print("{s}: {any}\n", .{ key, value }); + } + // TODO: Output images from templates _ = glob; } From ffa8760686488934defeefca4350a7f2c398f9ce Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Mar 2026 15:34:16 -0400 Subject: [PATCH 198/303] Aggregate statistics --- src/main.zig | 50 +++++++++++++++++++++++++++++++++++++--------- src/statistics.zig | 2 +- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/main.zig b/src/main.zig index a9623c34a59..b5c7612b24e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -135,8 +135,43 @@ pub fn main() !void { try writer.interface.flush(); } - var languages = std.StringArrayHashMap(u64).init(allocator); - defer languages.deinit(); + var aggregate_stats: struct { + languages: std.StringArrayHashMap(u64), + contributions: usize, + stars: usize = 0, + forks: usize = 0, + lines_changed: usize = 0, + views: usize = 0, + repos: usize = 0, + } = .{ + .contributions = stats.repo_contributions + + stats.issue_contributions + + stats.commit_contributions + + stats.pr_contributions + + stats.review_contributions, + .languages = .init(allocator), + }; + defer aggregate_stats.languages.deinit(); + for (stats.repositories) |repository| { + if (glob.matchAny(excluded_repos orelse &.{}, repository.name)) { + continue; + } + aggregate_stats.stars += repository.stars; + aggregate_stats.forks += repository.forks; + aggregate_stats.lines_changed += repository.lines_changed; + aggregate_stats.views += repository.views; + aggregate_stats.repos += 1; + } + inline for (@typeInfo(@TypeOf(aggregate_stats)).@"struct".fields) |field| { + if (!std.mem.eql(u8, field.name, "languages")) { + std.debug.print("{s}: {any}\n", .{ + field.name, + @field(aggregate_stats, field.name), + }); + } + } + std.debug.print("\n", .{}); + for (stats.repositories) |repository| { if (glob.matchAny(excluded_repos orelse &.{}, repository.name)) { continue; @@ -145,20 +180,17 @@ pub fn main() !void { if (glob.matchAny(excluded_langs orelse &.{}, language.name)) { continue; } - var total = languages.get(language.name) orelse 0; + var total = aggregate_stats.languages.get(language.name) orelse 0; total += language.size; - try languages.put(language.name, total); + try aggregate_stats.languages.put(language.name, total); }; } for ( - languages.unmanaged.entries.slice().items(.key), - languages.unmanaged.entries.slice().items(.value), + aggregate_stats.languages.keys(), + aggregate_stats.languages.values(), ) |key, value| { std.debug.print("{s}: {any}\n", .{ key, value }); } - - // TODO: Output images from templates - _ = glob; } test { diff --git a/src/statistics.zig b/src/statistics.zig index 21761deca0a..d267c9f3bb1 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -18,8 +18,8 @@ const Repository = struct { stars: u32, forks: u32, languages: ?[]Language, - views: u32, lines_changed: u32, + views: u32, pub fn deinit(self: @This()) void { allocator.free(self.name); From 66a717d9c25d0da2d3adfcab8412978ce6712dbc Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Mar 2026 15:59:45 -0400 Subject: [PATCH 199/303] Remove unused variable --- src/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index b5c7612b24e..249cde60f53 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,5 +1,6 @@ const builtin = @import("builtin"); const std = @import("std"); + const argparse = @import("argparse.zig"); const glob = @import("glob.zig"); @@ -15,7 +16,6 @@ var log_level: std.log.Level = switch (builtin.mode) { else => .warn, }; var allocator: std.mem.Allocator = undefined; -var user: []const u8 = undefined; fn logFn( comptime message_level: std.log.Level, From a4d51eddc8bd5f3a93be4b0c971e90ef4f300857 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Mar 2026 16:29:38 -0400 Subject: [PATCH 200/303] Allow excluding private repos from stats --- src/main.zig | 27 +++++++++++++-------------- src/statistics.zig | 4 ++++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/main.zig b/src/main.zig index 249cde60f53..722e09ff4a8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -36,6 +36,7 @@ const Args = struct { verbose: bool = false, excluded_repos: ?[]const u8 = null, excluded_langs: ?[]const u8 = null, + exclude_private: bool = false, pub fn deinit(self: @This()) void { if (self.api_key) |s| allocator.free(s); @@ -153,7 +154,9 @@ pub fn main() !void { }; defer aggregate_stats.languages.deinit(); for (stats.repositories) |repository| { - if (glob.matchAny(excluded_repos orelse &.{}, repository.name)) { + if (glob.matchAny(excluded_repos orelse &.{}, repository.name) or + (args.exclude_private and repository.private)) + { continue; } aggregate_stats.stars += repository.stars; @@ -161,7 +164,16 @@ pub fn main() !void { aggregate_stats.lines_changed += repository.lines_changed; aggregate_stats.views += repository.views; aggregate_stats.repos += 1; + if (repository.languages) |langs| for (langs) |language| { + if (glob.matchAny(excluded_langs orelse &.{}, language.name)) { + continue; + } + var total = aggregate_stats.languages.get(language.name) orelse 0; + total += language.size; + try aggregate_stats.languages.put(language.name, total); + }; } + inline for (@typeInfo(@TypeOf(aggregate_stats)).@"struct".fields) |field| { if (!std.mem.eql(u8, field.name, "languages")) { std.debug.print("{s}: {any}\n", .{ @@ -172,19 +184,6 @@ pub fn main() !void { } std.debug.print("\n", .{}); - for (stats.repositories) |repository| { - if (glob.matchAny(excluded_repos orelse &.{}, repository.name)) { - continue; - } - if (repository.languages) |langs| for (langs) |language| { - if (glob.matchAny(excluded_langs orelse &.{}, language.name)) { - continue; - } - var total = aggregate_stats.languages.get(language.name) orelse 0; - total += language.size; - try aggregate_stats.languages.put(language.name, total); - }; - } for ( aggregate_stats.languages.keys(), aggregate_stats.languages.values(), diff --git a/src/statistics.zig b/src/statistics.zig index d267c9f3bb1..bdffb3d0110 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -20,6 +20,7 @@ const Repository = struct { languages: ?[]Language, lines_changed: u32, views: u32, + private: bool, pub fn deinit(self: @This()) void { allocator.free(self.name); @@ -218,6 +219,7 @@ fn get_repos( \\ nameWithOwner \\ stargazerCount \\ forkCount + \\ isPrivate \\ languages( \\ first: 100, \\ orderBy: { direction: DESC, field: SIZE } @@ -269,6 +271,7 @@ fn get_repos( nameWithOwner: []const u8, stargazerCount: u32, forkCount: u32, + isPrivate: bool, languages: ?struct { edges: ?[]struct { size: u32, @@ -316,6 +319,7 @@ fn get_repos( .name = try allocator.dupe(u8, raw_repo.nameWithOwner), .stars = raw_repo.stargazerCount, .forks = raw_repo.forkCount, + .private = raw_repo.isPrivate, .languages = null, .views = 0, .lines_changed = 0, From 510f668d09bc50ae24d925f48d9f39c2487237b4 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Mar 2026 18:15:12 -0400 Subject: [PATCH 201/303] Tweak README --- README.md | 154 +++++++++++++++++++----------------------------------- 1 file changed, 54 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index a24323b1cdc..865513595b7 100644 --- a/README.md +++ b/README.md @@ -11,135 +11,73 @@ https://github.community/t/support-theme-context-for-images-in-light-vs-dark-mod Generate visualizations of GitHub user and repository statistics with GitHub -Actions. Visualizations can include data for both private repositories, and for +Actions. Visualizations can include data from private repositories, and from repositories you have contributed to, but do not own. Generated images automatically switch between GitHub light theme and GitHub dark theme. + ## Background -When someone views a profile on GitHub, it is often because they are curious -about a user's open source projects and contributions. Unfortunately, that -user's stars, forks, and pinned repositories do not necessarily reflect the -contributions they make to private repositories. The data likewise does not -present a complete picture of the user's total contributions beyond the current -year. +When someone views a GitHub profile, it is often because they are curious about +the user's open source contributions. Unfortunately, that user's stars, forks, +and pinned repositories do not necessarily reflect the contributions they make +to private repositories. The data likewise does not present a complete picture +of the user's total contributions beyond the current year. This project aims to collect a variety of profile and repository statistics using the GitHub API. It then generates images that can be displayed in repository READMEs, or in a user's [Profile README](https://docs.github.com/en/github/setting-up-and-managing-your-github-profile/managing-your-profile-readme). +It also dumps all statistics to a JSON file that can be used for further data +analysis. + +Since this project runs on GitHub Actions, no server is required to regularly +regenerate the images with updated statistics. Likewise, since the user runs the +analysis code themselves via GitHub Actions, they can use their GitHub access +token to collect statistics on private repositories that an external service +would be unable to access. -Since the project runs on GitHub Actions, no server is required to regularly -regenerate the images with updated statistics. Likewise, since the user runs -the analysis code themselves via GitHub Actions, they can use their GitHub -access token to collect statistics on private repositories that an external -service would be unable to access. ## Disclaimer -If the project is used with an access token that has sufficient permissions to -read private repositories, it may leak details about those repositories in -error messages. For example, the `aiohttp` library—used for asynchronous API -requests—may include the requested URL in exceptions, which can leak the name -of private repositories. If there is an exception caused by `aiohttp`, this -exception will be viewable in the Actions tab of the repository fork, and -anyone may be able to see the name of one or more private repositories. - -Due to some issues with the GitHub statistics API, there are some situations -where it returns inaccurate results. Specifically, the repository view count -statistics and total lines of code modified are probably somewhat inaccurate. -Unexpectedly, these values will become more accurate over time as GitHub -caches statistics for your repositories. Additionally, repositories that were -last contributed to more than a year ago may not be included in the statistics -due to limitations in the results returned by the API. - -For more information on inaccuracies, see issue -[#2](https://github.com/jstrieb/github-stats/issues/2), -[#3](https://github.com/jstrieb/github-stats/issues/3), and -[#13](https://github.com/jstrieb/github-stats/issues/13). +The GitHub statistics API returns inaccurate results in some situations: + +- Repository view count statistics often seem too low, and many referring sites + are not captured + - If you lack permissions to access the view count for a repository, it will + be tallied as zero views – this is common for external repositories where + your only contribution is making a pull request +- Total lines of code modified may be inflated – it counts changes to files like + `package.json` that may impact the line count in surprising ways +- Only repositories with commit contributions are counted, so if you only open + an issue on a repo, it will not show up in the statistics + - Repos you created and own may not be counted if you never commit to them, or + if the committer email is not connected to your GitHub account + # Installation - - -1. Create a personal access token (not the default GitHub Actions token) using - the instructions - [here](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token). - Personal access token must have permissions: `read:user` and `repo`. Copy - the access token when it is generated – if you lose it, you will have to - regenerate the token. - - Some users are reporting that it can take a few minutes for the personal - access token to work. For more, see - [#30](https://github.com/jstrieb/github-stats/issues/30). -2. Create a copy of this repository by clicking - [here](https://github.com/jstrieb/github-stats/generate). Note: this is - **not** the same as forking a copy because it copies everything fresh, - without the huge commit history. -3. Go to the "Secrets" page of your copy of the repository. If this is the - README of your copy, click [this link](../../settings/secrets/actions) to go - to the "Secrets" page. Otherwise, go to the "Settings" tab of the - newly-created repository and go to the "Secrets" page (bottom left). -4. Create a new secret with the name `ACCESS_TOKEN` and paste the copied - personal access token as the value. -5. It is possible to change the type of statistics reported by adding other - repository secrets. - - To ignore certain repos, add them (in owner/name format e.g., - `jstrieb/github-stats`) separated by commas to a new secret—created as - before—called `EXCLUDED`. - - To ignore certain languages, add them (separated by commas) to a new - secret called `EXCLUDED_LANGS`. For example, to exclude HTML and TeX you - could set the value to `html,tex`. - - To show statistics only for "owned" repositories and not forks with - contributions, add an environment variable (under the `env` header in the - [main - workflow](https://github.com/jstrieb/github-stats/blob/master/.github/workflows/main.yml)) - called `EXCLUDE_FORKED_REPOS` with a value of `true`. - - These other values are added as secrets by default to prevent leaking - information about private repositories. If you're not worried about that, - you can change the values directly [in the Actions workflow - itself](https://github.com/jstrieb/github-stats/blob/05de1314b870febd44d19ad2f55d5e59d83f5857/.github/workflows/main.yml#L48-L53). -6. Go to the [Actions - Page](../../actions?query=workflow%3A"Generate+Stats+Images") and press "Run - Workflow" on the right side of the screen to generate images for the first - time. - - The images will be automatically regenerated every 24 hours, but they can - be regenerated manually by running the workflow this way. -7. Take a look at the images that have been created in the - [`generated`](generated) folder. -8. To add your statistics to your GitHub Profile README, copy and paste the - following lines of code into your markdown content. Change the `username` - value to your GitHub username. - ```md - ![](https://raw.githubusercontent.com/username/github-stats/master/generated/overview.svg#gh-dark-mode-only) - ![](https://raw.githubusercontent.com/username/github-stats/master/generated/overview.svg#gh-light-mode-only) - ``` - ```md - ![](https://raw.githubusercontent.com/username/github-stats/master/generated/languages.svg#gh-dark-mode-only) - ![](https://raw.githubusercontent.com/username/github-stats/master/generated/languages.svg#gh-light-mode-only) - ``` -9. Link back to this repository so that others can generate their own - statistics images. -10. Star this repo if you like it! +TODO # Support the Project -There are a few things you can do to support the project: +If this project is useful to you, please support it! - Star the repository (and follow me on GitHub for more) - Share and upvote on sites like Twitter, Reddit, and Hacker News - Report any bugs, glitches, or errors that you find These things motivate me to keep sharing what I build, and they provide -validation that my work is appreciated! They also help me improve the -project. Thanks in advance! +validation that my work is appreciated! They also help me improve the project. +Thanks in advance! If you are insistent on spending money to show your support, I encourage you to -instead make a generous donation to one of the following organizations. By advocating -for Internet freedoms, organizations like these help me to feel comfortable -releasing work publicly on the Web. +instead make a generous donation to one of the following organizations. By +advocating for Internet freedoms, organizations like these help me to feel +comfortable releasing work publicly on the Web. - [Electronic Frontier Foundation](https://supporters.eff.org/donate/) - [Signal Foundation](https://signal.org/donate/) @@ -147,9 +85,25 @@ releasing work publicly on the Web. - [The Internet Archive](https://archive.org/donate/index.php) +## Project Status + +This project is actively maintained, but not actively developed. In other words, +I will fix bugs, but will rarely continue adding features (if at all). If there +are no recent commits, it means that everything has been running smoothly! + +If you want to contribute to the project, please open an issue to discuss first. +Pull requests that are not discussed with me ahead of time may be ignored. It's +nothing personal, I'm just busy, and reviewing others' code is not my idea of +fun. + +Even if something were to happen to me, and I could not continue to work on the +project, it will continue to work as long as the GitHub API endpoints it uses +remain active and unchanged. + + # Related Projects - Inspired by a desire to improve upon [anuraghazra/github-readme-stats](https://github.com/anuraghazra/github-readme-stats) -- Makes use of [GitHub Octicons](https://primer.style/octicons/) to precisely - match the GitHub UI +- Uses [GitHub Octicons](https://primer.style/octicons/) to precisely match the + GitHub UI From 0f05ddf39c3b4866e43e52b59a857911453711ce Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Mar 2026 21:00:11 -0400 Subject: [PATCH 202/303] Add logging messages --- src/main.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main.zig b/src/main.zig index 722e09ff4a8..124d8adfdaf 100644 --- a/src/main.zig +++ b/src/main.zig @@ -93,6 +93,7 @@ pub fn main() !void { var stats: Statistics = undefined; if (args.json_input_file) |path| { + std.log.info("Reading statistics from '{s}'", .{path}); const in = if (std.mem.eql(u8, path, "-")) std.fs.File.stdin() @@ -108,6 +109,7 @@ pub fn main() !void { defer allocator.free(data); stats = try Statistics.initFromJson(allocator, data); } else if (args.api_key) |api_key| { + std.log.info("Collecting statistics from GitHub API", .{}); var client: HttpClient = try .init(allocator, api_key); defer client.deinit(); stats = try Statistics.init(&client, allocator); @@ -115,6 +117,7 @@ pub fn main() !void { defer stats.deinit(); if (args.json_output_file) |path| { + std.log.info("Writing raw JSON data to '{s}'", .{path}); const out = if (std.mem.eql(u8, path, "-")) std.fs.File.stdout() From 0990a291bbcff9240c4983ae00c907cc993e1575 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Mar 2026 22:13:11 -0400 Subject: [PATCH 203/303] Move global allocators to arguments --- src/argparse.zig | 59 ++++++++++++++++++++++++++-------------------- src/main.zig | 41 ++++++++++++++++++-------------- src/statistics.zig | 30 +++++++++++------------ 3 files changed, 71 insertions(+), 59 deletions(-) diff --git a/src/argparse.zig b/src/argparse.zig index 8631cdb99f3..701e52c39b2 100644 --- a/src/argparse.zig +++ b/src/argparse.zig @@ -4,11 +4,9 @@ const std = @import("std"); // used globally. var stdout: *std.Io.Writer = undefined; var stderr: *std.Io.Writer = undefined; -var arena: std.heap.ArenaAllocator = undefined; -var allocator: std.mem.Allocator = undefined; pub fn parse( - gpa: std.mem.Allocator, + allocator: std.mem.Allocator, T: type, errorCheck: ?fn (args: T, stderr: *std.Io.Writer) anyerror!bool, ) !T { @@ -17,8 +15,7 @@ pub fn parse( var stderr_writer = std.fs.File.stderr().writer(&.{}); stderr = &stderr_writer.interface; - allocator = gpa; - arena = .init(allocator); + var arena: std.heap.ArenaAllocator = .init(allocator); defer arena.deinit(); const a = arena.allocator(); @@ -28,16 +25,16 @@ pub fn parse( errdefer { inline for (fields, seen) |field, seen_field| { if (seen_field) { - free_field(@field(result, field.name)); + free_field(allocator, @field(result, field.name)); } } } const args = try std.process.argsAlloc(a); defer std.process.argsFree(a, args); - try setFromCli(T, args, &seen, &result); - try setFromEnv(T, &seen, &result); - try setFromDefaults(T, &seen, &result); + try setFromCli(T, allocator, &arena, args, &seen, &result); + try setFromEnv(T, allocator, &arena, &seen, &result); + try setFromDefaults(T, allocator, &seen, &result); inline for (fields, seen) |field, seen_field| { if (!seen_field) { @@ -48,7 +45,7 @@ pub fn parse( "Missing required argument {s}\n", .{field.name}, ); - try printUsage(T, args[0]); + try printUsage(T, arena.allocator(), args[0]); std.process.exit(1); } } @@ -56,7 +53,7 @@ pub fn parse( if (errorCheck) |check| { if (!(try check(result, stderr))) { - try printUsage(T, args[0]); + try printUsage(T, arena.allocator(), args[0]); std.process.exit(1); } } @@ -66,6 +63,8 @@ pub fn parse( fn setFromCli( T: type, + allocator: std.mem.Allocator, + arena: *std.heap.ArenaAllocator, args: []const []const u8, seen: []bool, result: *T, @@ -77,14 +76,14 @@ fn setFromCli( if (std.mem.eql(u8, raw_arg, "-h") or std.mem.eql(u8, raw_arg, "--help")) { - try printUsage(T, args[0]); + try printUsage(T, arena.allocator(), args[0]); std.process.exit(0); } // TODO: Handle one-letter arguments if (!std.mem.startsWith(u8, raw_arg, "--")) { try stderr.print("Unknown argument: '{s}'\n", .{raw_arg}); - try printUsage(T, args[0]); + try printUsage(T, arena.allocator(), args[0]); std.process.exit(1); } @@ -103,7 +102,7 @@ fn setFromCli( "Missing required value for argument {s} {s}\n", .{ raw_arg, field.name }, ); - try printUsage(T, args[0]); + try printUsage(T, arena.allocator(), args[0]); std.process.exit(1); } switch (t) { @@ -125,12 +124,18 @@ fn setFromCli( } try stderr.print("Unknown argument: '{s}'\n", .{raw_arg}); - try printUsage(T, args[0]); + try printUsage(T, arena.allocator(), args[0]); std.process.exit(1); } } -fn setFromEnv(T: type, seen: []bool, result: *T) !void { +fn setFromEnv( + T: type, + allocator: std.mem.Allocator, + arena: *std.heap.ArenaAllocator, + seen: []bool, + result: *T, +) !void { const a = arena.allocator(); var env = try std.process.getEnvMap(a); defer env.deinit(); @@ -162,7 +167,12 @@ fn setFromEnv(T: type, seen: []bool, result: *T) !void { } } -fn setFromDefaults(T: type, seen: []bool, result: *T) !void { +fn setFromDefaults( + T: type, + allocator: std.mem.Allocator, + seen: []bool, + result: *T, +) !void { inline for (@typeInfo(T).@"struct".fields, seen) |field, *seen_field| { if (!seen_field.*) { if (field.defaultValue()) |default| { @@ -182,22 +192,21 @@ fn setFromDefaults(T: type, seen: []bool, result: *T) !void { } } -fn printUsage(T: type, argv0: []const u8) !void { - const a = arena.allocator(); +fn printUsage(T: type, allocator: std.mem.Allocator, argv0: []const u8) !void { try stdout.print("Usage: {s} [options]\n\n", .{argv0}); try stdout.print("Options:\n", .{}); const fields = @typeInfo(T).@"struct".fields; inline for (fields) |field| { switch (@typeInfo(strip_optional(field.type))) { .bool => { - const flag_version = try a.dupe(u8, field.name); - defer a.free(flag_version); + const flag_version = try allocator.dupe(u8, field.name); + defer allocator.free(flag_version); std.mem.replaceScalar(u8, flag_version, '_', '-'); try stdout.print("--{s}\n", .{flag_version}); }, else => { - const flag_version = try a.dupe(u8, field.name); - defer a.free(flag_version); + const flag_version = try allocator.dupe(u8, field.name); + defer allocator.free(flag_version); std.mem.replaceScalar(u8, flag_version, '_', '-'); try stdout.print("--{s} {s}\n", .{ flag_version, field.name }); }, @@ -211,10 +220,10 @@ fn strip_optional(T: type) type { return strip_optional(info.optional.child); } -fn free_field(field: anytype) void { +fn free_field(allocator: std.mem.Allocator, field: anytype) void { switch (@typeInfo(@TypeOf(field))) { .pointer => allocator.free(field), - .optional => if (field) |v| free_field(v), + .optional => if (field) |v| free_field(allocator, v), .bool, .int, .float, .@"enum" => {}, else => @compileError("Disallowed struct field type."), } diff --git a/src/main.zig b/src/main.zig index 124d8adfdaf..b48741325e0 100644 --- a/src/main.zig +++ b/src/main.zig @@ -15,7 +15,6 @@ var log_level: std.log.Level = switch (builtin.mode) { .Debug => .debug, else => .warn, }; -var allocator: std.mem.Allocator = undefined; fn logFn( comptime message_level: std.log.Level, @@ -38,7 +37,24 @@ const Args = struct { excluded_langs: ?[]const u8 = null, exclude_private: bool = false, - pub fn deinit(self: @This()) void { + const Self = @This(); + + pub fn init(allocator: std.mem.Allocator) !Self { + return try argparse.parse(allocator, Self, struct { + fn errorCheck(a: Self, stderr: *std.Io.Writer) !bool { + if (a.api_key == null and a.json_input_file == null) { + try stderr.print( + "You must pass either an input file or an API key.\n", + .{}, + ); + return false; + } + return true; + } + }.errorCheck); + } + + pub fn deinit(self: Self, allocator: std.mem.Allocator) void { if (self.api_key) |s| allocator.free(s); if (self.json_input_file) |s| allocator.free(s); if (self.json_output_file) |s| allocator.free(s); @@ -50,21 +66,10 @@ const Args = struct { pub fn main() !void { var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; defer _ = gpa.deinit(); - allocator = gpa.allocator(); - - const args = try argparse.parse(allocator, Args, struct { - fn errorCheck(a: Args, stderr: *std.Io.Writer) !bool { - if (a.api_key == null and a.json_input_file == null) { - try stderr.print( - "You must pass either an input file or an API key.\n", - .{}, - ); - return false; - } - return true; - } - }.errorCheck); - defer args.deinit(); + const allocator = gpa.allocator(); + + const args = try Args.init(allocator); + defer args.deinit(allocator); if (args.silent) { log_level = .err; } else if (args.verbose) { @@ -114,7 +119,7 @@ pub fn main() !void { defer client.deinit(); stats = try Statistics.init(&client, allocator); } else unreachable; - defer stats.deinit(); + defer stats.deinit(allocator); if (args.json_output_file) |path| { std.log.info("Writing raw JSON data to '{s}'", .{path}); diff --git a/src/statistics.zig b/src/statistics.zig index bdffb3d0110..db4f7af94f4 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -10,7 +10,6 @@ commit_contributions: u32 = 0, pr_contributions: u32 = 0, review_contributions: u32 = 0, -var allocator: std.mem.Allocator = undefined; const Statistics = @This(); const Repository = struct { @@ -22,11 +21,11 @@ const Repository = struct { views: u32, private: bool, - pub fn deinit(self: @This()) void { + pub fn deinit(self: @This(), allocator: std.mem.Allocator) void { allocator.free(self.name); if (self.languages) |languages| { for (languages) |language| { - language.deinit(); + language.deinit(allocator); } allocator.free(languages); } @@ -94,25 +93,23 @@ const Language = struct { size: u32, color: ?[]const u8 = null, - pub fn deinit(self: @This()) void { + pub fn deinit(self: @This(), allocator: std.mem.Allocator) void { allocator.free(self.name); if (self.color) |color| allocator.free(color); } }; -pub fn init(client: *HttpClient, a: std.mem.Allocator) !Statistics { - allocator = a; +pub fn init(client: *HttpClient, allocator: std.mem.Allocator) !Statistics { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); - var self: Statistics = try get_repos(&arena, client); - errdefer self.deinit(); + var self: Statistics = try get_repos(allocator, &arena, client); + errdefer self.deinit(allocator); try self.get_lines_changed(&arena, client); return self; } -pub fn initFromJson(a: std.mem.Allocator, s: []const u8) !Statistics { - allocator = a; +pub fn initFromJson(allocator: std.mem.Allocator, s: []const u8) !Statistics { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); @@ -125,9 +122,9 @@ pub fn initFromJson(a: std.mem.Allocator, s: []const u8) !Statistics { return try deepcopy(allocator, parsed); } -pub fn deinit(self: Statistics) void { +pub fn deinit(self: Statistics, allocator: std.mem.Allocator) void { for (self.repositories) |repository| { - repository.deinit(); + repository.deinit(allocator); } allocator.free(self.repositories); allocator.free(self.user); @@ -136,7 +133,7 @@ pub fn deinit(self: Statistics) void { fn get_basic_info( client: *HttpClient, - alloc: std.mem.Allocator, + allocator: std.mem.Allocator, ) !struct { []u32, []const u8, ?[]const u8 } { std.log.info("Getting contribution years...", .{}); const response, const status = try client.graphql( @@ -165,7 +162,7 @@ fn get_basic_info( contributionYears: []u32, }, } } }, - alloc, + allocator, response, .{ .ignore_unknown_fields = true }, )).data.viewer; @@ -177,6 +174,7 @@ fn get_basic_info( } fn get_repos( + allocator: std.mem.Allocator, arena: *std.heap.ArenaAllocator, client: *HttpClient, ) !Statistics { @@ -189,7 +187,7 @@ fn get_repos( try .initCapacity(allocator, 32); errdefer { for (repositories.items) |repo| { - repo.deinit(); + repo.deinit(allocator); } repositories.deinit(allocator); } @@ -347,7 +345,7 @@ fn get_repos( } } } - errdefer repository.deinit(); + errdefer repository.deinit(allocator); std.log.info( "Getting views for {s}...", From 29a3e7469e68d558958c3034d411e777f6e896e9 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 25 Mar 2026 03:51:53 +0000 Subject: [PATCH 204/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index f6e010ff52e..343d80f1b9e 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,567 +Stars7,566 -Forks1,170 +Forks1,171 All-time contributions4,555 -Lines of code changed2,779,976 +Lines of code changed2,747,981 -Repository views (past two weeks)1,498 +Repository views (past two weeks)1,476 Repositories with contributions129 From 00061d2e6835b91725a13a99915eab73c6b8e495 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 24 Mar 2026 23:49:22 -0400 Subject: [PATCH 205/303] Commit generated files on a separate branch --- .github/workflows/main.yml | 26 +-- README.md | 11 +- generated/languages.svg | 392 ------------------------------------- generated/overview.svg | 113 ----------- 4 files changed, 22 insertions(+), 520 deletions(-) delete mode 100644 generated/languages.svg delete mode 100644 generated/overview.svg diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bfa93b79f7e..1a79327ced1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,36 +11,40 @@ on: permissions: contents: write +defaults: + run: + shell: bash -euxo pipefail {0} + jobs: build: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v3 + - name: Checkout history branch + run: | + git config --global user.name "jstrieb/github-stats" + git config --global user.email "github-stats[bot]@jstrieb.github.io" + git checkout generated || git checkout -b generated + git merge master + - uses: mlugg/setup-zig@v2 with: version: 0.15.2 - # TODO: Cache build - name: Build run: | - echo TODO + zig build --release - name: Generate images run: | - echo TODO + ./zig-out/bin/github_stats env: - ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - EXCLUDED: ${{ secrets.EXCLUDED }} + API_KEY: ${{ secrets.GITHUB_TOKEN }} + EXCLUDED_REPOS: ${{ secrets.EXCLUDED_REPOS }} EXCLUDED_LANGS: ${{ secrets.EXCLUDED_LANGS }} - EXCLUDE_FORKED_REPOS: true - # Commit all changed files to the repository - name: Commit to the repo run: | - git config --global user.name "jstrieb/github-stats" - git config --global user.email "github-stats[bot]@jstrieb.github.io" git add . # Force the build to succeed, even if no files were changed git commit -m 'Update generated files' || true diff --git a/README.md b/README.md index 865513595b7..63955777653 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,15 @@ + + Generate visualizations of GitHub user and repository statistics with GitHub Actions. Visualizations can include data from private repositories, and from diff --git a/generated/languages.svg b/generated/languages.svg deleted file mode 100644 index 02d63624f6b..00000000000 --- a/generated/languages.svg +++ /dev/null @@ -1,392 +0,0 @@ - - - - - - -
    - -

    Languages Used (By File Size)

    - -
    - - - -
    - -
      - - -
    • - -Python -29.30% -
    • - - -
    • - -C -17.71% -
    • - - -
    • - -Zig -11.03% -
    • - - -
    • - -JavaScript -9.41% -
    • - - -
    • - -Svelte -7.45% -
    • - - -
    • - -Standard ML -6.67% -
    • - - -
    • - -Shell -5.34% -
    • - - -
    • - -Go -2.41% -
    • - - -
    • - -SMT -2.05% -
    • - - -
    • - -TeX -1.88% -
    • - - -
    • - -CSS -1.68% -
    • - - -
    • - -Makefile -1.41% -
    • - - -
    • - -Java -1.31% -
    • - - -
    • - -C++ -0.66% -
    • - - -
    • - -OpenSCAD -0.35% -
    • - - -
    • - -Vim Script -0.19% -
    • - - -
    • - -TypeScript -0.19% -
    • - - -
    • - -Assembly -0.17% -
    • - - -
    • - -GDB -0.16% -
    • - - -
    • - -Nix -0.16% -
    • - - -
    • - -PHP -0.13% -
    • - - -
    • - -Tree-sitter Query -0.12% -
    • - - -
    • - -Just -0.09% -
    • - - -
    • - -Dockerfile -0.08% -
    • - - -
    • - -CMake -0.03% -
    • - - -
    • - -sed -0.01% -
    • - - -
    • - -Batchfile -0.01% -
    • - - - -
    - -
    -
    -
    -
    -
    diff --git a/generated/overview.svg b/generated/overview.svg deleted file mode 100644 index e2b0b6baf34..00000000000 --- a/generated/overview.svg +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - -
    - - - - - - - - - - - - - - - - - - - - -
    Jacob Strieb's GitHub Statistics
    Stars7,515
    Forks1,160
    All-time contributions4,543
    Lines of code changed2,777,663
    Repository views (past two weeks)1,568
    Repositories with contributions128
    - -
    -
    -
    -
    -
    From 51776b8a1a448bbf5c77544d01daa28e5c3654ee Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 25 Mar 2026 16:18:31 -0400 Subject: [PATCH 206/303] Output overview SVG --- src/main.zig | 55 +++++++++++++++++----- {templates => src/templates}/languages.svg | 0 {templates => src/templates}/overview.svg | 0 3 files changed, 42 insertions(+), 13 deletions(-) rename {templates => src/templates}/languages.svg (100%) rename {templates => src/templates}/overview.svg (100%) diff --git a/src/main.zig b/src/main.zig index b48741325e0..fb0735346ba 100644 --- a/src/main.zig +++ b/src/main.zig @@ -36,6 +36,8 @@ const Args = struct { excluded_repos: ?[]const u8 = null, excluded_langs: ?[]const u8 = null, exclude_private: bool = false, + overview_output_file: ?[]const u8 = null, + languages_output_file: ?[]const u8 = null, const Self = @This(); @@ -60,6 +62,8 @@ const Args = struct { if (self.json_output_file) |s| allocator.free(s); if (self.excluded_repos) |s| allocator.free(s); if (self.excluded_langs) |s| allocator.free(s); + if (self.overview_output_file) |s| allocator.free(s); + if (self.languages_output_file) |s| allocator.free(s); } }; @@ -182,21 +186,46 @@ pub fn main() !void { }; } - inline for (@typeInfo(@TypeOf(aggregate_stats)).@"struct".fields) |field| { - if (!std.mem.eql(u8, field.name, "languages")) { - std.debug.print("{s}: {any}\n", .{ - field.name, - @field(aggregate_stats, field.name), - }); + { + const template: []const u8 = @embedFile("templates/overview.svg"); + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const a = arena.allocator(); + var out_data = template; + inline for ( + @typeInfo(@TypeOf(aggregate_stats)).@"struct".fields, + ) |field| { + switch (@typeInfo(field.type)) { + .int => { + out_data = try std.mem.replaceOwned( + u8, + a, + out_data, + "{{ " ++ field.name ++ " }}", + try std.fmt.allocPrint( + a, + "{d}", + .{@field(aggregate_stats, field.name)}, + ), + ); + }, + else => {}, + } } - } - std.debug.print("\n", .{}); - for ( - aggregate_stats.languages.keys(), - aggregate_stats.languages.values(), - ) |key, value| { - std.debug.print("{s}: {any}\n", .{ key, value }); + const path = args.overview_output_file orelse "overview.svg"; + std.log.info("Writing overview image data to '{s}'", .{path}); + const out = + if (std.mem.eql(u8, path, "-")) + std.fs.File.stdout() + else + try std.fs.cwd().createFile(path, .{}); + defer out.close(); + var write_buffer: [64 * 1024]u8 = undefined; + var writer = out.writer(&write_buffer); + + try writer.interface.writeAll(out_data); + try writer.interface.flush(); } } diff --git a/templates/languages.svg b/src/templates/languages.svg similarity index 100% rename from templates/languages.svg rename to src/templates/languages.svg diff --git a/templates/overview.svg b/src/templates/overview.svg similarity index 100% rename from templates/overview.svg rename to src/templates/overview.svg From ccd8ab4f8590d8aeb5f0e3c5d242bcb64f2cab71 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 25 Mar 2026 16:43:12 -0400 Subject: [PATCH 207/303] Print large numbers with commas --- src/main.zig | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/main.zig b/src/main.zig index fb0735346ba..6446d302f0e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -202,10 +202,9 @@ pub fn main() !void { a, out_data, "{{ " ++ field.name ++ " }}", - try std.fmt.allocPrint( + try decimalToString( a, - "{d}", - .{@field(aggregate_stats, field.name)}, + @field(aggregate_stats, field.name), ), ); }, @@ -232,3 +231,33 @@ pub fn main() !void { test { std.testing.refAllDecls(@This()); } + +fn decimalToString(allocator: std.mem.Allocator, n: anytype) ![]const u8 { + const info = @typeInfo(@TypeOf(n)); + if (info != .int or info.int.signedness != .unsigned) { + @compileError("Only implemented for unsigned integer numbers."); + } + if (n == 0) { + return try allocator.dupe(u8, "0"); + } + const s = try std.fmt.allocPrint(allocator, "{d}", .{n}); + defer allocator.free(s); + const digits = s.len; + const commas = (digits - 1) / 3; + const result = try allocator.alloc(u8, digits + commas); + var i: usize = result.len - 1; + var j: usize = s.len - 1; + while (true) { + if ((result.len - i) % 4 == 0) { + result[i] = ','; + i -= 1; + } + result[i] = s[j]; + if (i == 0 and j == 0) { + break; + } else if (i > 0 and j > 0) {} else unreachable; + i -= 1; + j -= 1; + } + return result; +} From 22ac26757de0aaed1eec86c35f169bc9ebe88523 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 26 Mar 2026 04:12:13 +0000 Subject: [PATCH 208/303] Update generated files --- generated/languages.svg | 4 ++-- generated/overview.svg | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index c996af234ee..13c747d2fc3 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.48% +7.49% diff --git a/generated/overview.svg b/generated/overview.svg index 343d80f1b9e..4a895f05114 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,566 +Stars7,569 -Forks1,171 +Forks1,172 -All-time contributions4,555 +All-time contributions4,554 -Lines of code changed2,747,981 +Lines of code changed2,749,111 -Repository views (past two weeks)1,476 +Repository views (past two weeks)1,432 Repositories with contributions129 From c3d871d6247d278e3230cdcb7840858bee450ebb Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 27 Mar 2026 03:17:02 +0000 Subject: [PATCH 209/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 4a895f05114..2af3eb7e6e1 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,569 +Stars7,585 -Forks1,172 +Forks1,186 All-time contributions4,554 -Lines of code changed2,749,111 +Lines of code changed2,780,689 -Repository views (past two weeks)1,432 +Repository views (past two weeks)1,490 Repositories with contributions129 From 1971eef114f69c7bb73d4e714bfa16d9c8225770 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 28 Mar 2026 03:03:13 +0000 Subject: [PATCH 210/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 2af3eb7e6e1..a71fe7a0a3e 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,7 +91,7 @@ tr { -Stars7,585 +Stars7,586 Forks1,186 @@ -99,7 +99,7 @@ tr { Lines of code changed2,780,689 -Repository views (past two weeks)1,490 +Repository views (past two weeks)1,473 Repositories with contributions129 From 2576f1284114477844e4d261c7b2567965274821 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 28 Mar 2026 16:28:52 -0400 Subject: [PATCH 211/303] Build languages SVG --- src/main.zig | 118 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 113 insertions(+), 5 deletions(-) diff --git a/src/main.zig b/src/main.zig index 6446d302f0e..ee8f8a979ce 100644 --- a/src/main.zig +++ b/src/main.zig @@ -108,6 +108,7 @@ pub fn main() !void { std.fs.File.stdin() else try std.fs.cwd().openFile(path, .{}); + // TODO: Don't close stdin defer in.close(); var read_buffer: [64 * 1024]u8 = undefined; var reader = in.reader(&read_buffer); @@ -132,6 +133,7 @@ pub fn main() !void { std.fs.File.stdout() else try std.fs.cwd().createFile(path, .{}); + // TODO: Don't close stdout defer out.close(); var write_buffer: [64 * 1024]u8 = undefined; var writer = out.writer(&write_buffer); @@ -150,7 +152,10 @@ pub fn main() !void { var aggregate_stats: struct { languages: std.StringArrayHashMap(u64), + language_colors: std.StringArrayHashMap([]const u8), contributions: usize, + name: []const u8, + languages_total: usize = 0, stars: usize = 0, forks: usize = 0, lines_changed: usize = 0, @@ -163,8 +168,11 @@ pub fn main() !void { stats.pr_contributions + stats.review_contributions, .languages = .init(allocator), + .language_colors = .init(allocator), + .name = stats.name, }; defer aggregate_stats.languages.deinit(); + defer aggregate_stats.language_colors.deinit(); for (stats.repositories) |repository| { if (glob.matchAny(excluded_repos orelse &.{}, repository.name) or (args.exclude_private and repository.private)) @@ -177,20 +185,33 @@ pub fn main() !void { aggregate_stats.views += repository.views; aggregate_stats.repos += 1; if (repository.languages) |langs| for (langs) |language| { + if (language.color) |color| { + try aggregate_stats.language_colors.put(language.name, color); + } if (glob.matchAny(excluded_langs orelse &.{}, language.name)) { continue; } var total = aggregate_stats.languages.get(language.name) orelse 0; total += language.size; try aggregate_stats.languages.put(language.name, total); + aggregate_stats.languages_total += language.size; }; } + aggregate_stats.languages.sort(struct { + values: @TypeOf(aggregate_stats.languages.values()), + pub fn lessThan(self: @This(), a: usize, b: usize) bool { + // Sort in reverse order + return self.values[a] >= self.values[b]; + } + }{ .values = aggregate_stats.languages.values() }); { const template: []const u8 = @embedFile("templates/overview.svg"); + var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); const a = arena.allocator(); + var out_data = template; inline for ( @typeInfo(@TypeOf(aggregate_stats)).@"struct".fields, @@ -208,7 +229,17 @@ pub fn main() !void { ), ); }, - else => {}, + .pointer => { + out_data = try std.mem.replaceOwned( + u8, + a, + out_data, + "{{ " ++ field.name ++ " }}", + @field(aggregate_stats, field.name), + ); + }, + .@"struct" => {}, + else => comptime unreachable, } } @@ -219,6 +250,83 @@ pub fn main() !void { std.fs.File.stdout() else try std.fs.cwd().createFile(path, .{}); + // TODO: Don't close stdout + defer out.close(); + var write_buffer: [64 * 1024]u8 = undefined; + var writer = out.writer(&write_buffer); + + try writer.interface.writeAll(out_data); + try writer.interface.flush(); + } + + { + const template: []const u8 = @embedFile("templates/languages.svg"); + + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const a = arena.allocator(); + + const progress = + try a.alloc([]const u8, aggregate_stats.languages.count()); + const lang_list = + try a.alloc([]const u8, aggregate_stats.languages.count()); + for ( + aggregate_stats.languages.keys(), + aggregate_stats.languages.values(), + progress, + lang_list, + 0.., + ) |language, count, *progress_s, *lang_s, i| { + const color = aggregate_stats.language_colors.get(language); + const percent = + 100 * if (aggregate_stats.languages_total == 0) + 0.0 + else + @as(f64, @floatFromInt(count)) / + @as(f64, @floatFromInt(aggregate_stats.languages_total)); + progress_s.* = try std.fmt.allocPrint(a, + \\ + , .{ color orelse "#000", percent }); + lang_s.* = try std.fmt.allocPrint(a, + \\
  • + \\ + \\ {s} + \\ {d:.2}% + \\
  • + \\ + , .{ (i + 1) * 150, color orelse "#000", language, percent }); + } + const out_data = + try std.mem.replaceOwned(u8, a, try std.mem.replaceOwned( + u8, + a, + template, + "{{ lang_list }}", + try std.mem.concat(a, u8, lang_list), + ), "{{ progress }}", try std.mem.concat(a, u8, progress)); + + const path = args.overview_output_file orelse "languages.svg"; + std.log.info("Writing languages image data to '{s}'", .{path}); + const out = + if (std.mem.eql(u8, path, "-")) + std.fs.File.stdout() + else + try std.fs.cwd().createFile(path, .{}); + // TODO: Don't close stdout defer out.close(); var write_buffer: [64 * 1024]u8 = undefined; var writer = out.writer(&write_buffer); @@ -235,16 +343,16 @@ test { fn decimalToString(allocator: std.mem.Allocator, n: anytype) ![]const u8 { const info = @typeInfo(@TypeOf(n)); if (info != .int or info.int.signedness != .unsigned) { - @compileError("Only implemented for unsigned integer numbers."); - } - if (n == 0) { - return try allocator.dupe(u8, "0"); + @compileError("Only implemented for unsigned integers."); } + const s = try std.fmt.allocPrint(allocator, "{d}", .{n}); defer allocator.free(s); const digits = s.len; const commas = (digits - 1) / 3; const result = try allocator.alloc(u8, digits + commas); + errdefer comptime unreachable; + var i: usize = result.len - 1; var j: usize = s.len - 1; while (true) { From 5a98fa75ed41b81f2fdf5df951d0151f7a5784a2 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 28 Mar 2026 16:53:13 -0400 Subject: [PATCH 212/303] Break templating into separate functions --- src/main.zig | 179 +++++++++++++++++++++++++-------------------------- 1 file changed, 89 insertions(+), 90 deletions(-) diff --git a/src/main.zig b/src/main.zig index ee8f8a979ce..6fdbb43814d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -67,6 +67,91 @@ const Args = struct { } }; +fn overview(a: std.mem.Allocator, stats: anytype) ![]const u8 { + const template: []const u8 = @embedFile("templates/overview.svg"); + var out_data = template; + inline for ( + @typeInfo(@TypeOf(stats)).@"struct".fields, + ) |field| { + switch (@typeInfo(field.type)) { + .int => { + out_data = try std.mem.replaceOwned( + u8, + a, + out_data, + "{{ " ++ field.name ++ " }}", + try decimalToString(a, @field(stats, field.name)), + ); + }, + .pointer => { + out_data = try std.mem.replaceOwned( + u8, + a, + out_data, + "{{ " ++ field.name ++ " }}", + @field(stats, field.name), + ); + }, + .@"struct" => {}, + else => comptime unreachable, + } + } + return out_data; +} + +fn languages(a: std.mem.Allocator, stats: anytype) ![]const u8 { + const template: []const u8 = @embedFile("templates/languages.svg"); + const progress = try a.alloc([]const u8, stats.languages.count()); + const lang_list = try a.alloc([]const u8, stats.languages.count()); + for ( + stats.languages.keys(), + stats.languages.values(), + progress, + lang_list, + 0.., + ) |language, count, *progress_s, *lang_s, i| { + const color = stats.language_colors.get(language); + const percent = + 100 * if (stats.languages_total == 0) + 0.0 + else + @as(f64, @floatFromInt(count)) / + @as(f64, @floatFromInt(stats.languages_total)); + progress_s.* = try std.fmt.allocPrint(a, + \\ + , .{ color orelse "#000", percent }); + lang_s.* = try std.fmt.allocPrint(a, + \\
  • + \\ + \\ {s} + \\ {d:.2}% + \\
  • + \\ + , .{ (i + 1) * 150, color orelse "#000", language, percent }); + } + return try std.mem.replaceOwned(u8, a, try std.mem.replaceOwned( + u8, + a, + template, + "{{ lang_list }}", + try std.mem.concat(a, u8, lang_list), + ), "{{ progress }}", try std.mem.concat(a, u8, progress)); +} + pub fn main() !void { var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; defer _ = gpa.deinit(); @@ -206,44 +291,13 @@ pub fn main() !void { }{ .values = aggregate_stats.languages.values() }); { - const template: []const u8 = @embedFile("templates/overview.svg"); - var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); const a = arena.allocator(); - var out_data = template; - inline for ( - @typeInfo(@TypeOf(aggregate_stats)).@"struct".fields, - ) |field| { - switch (@typeInfo(field.type)) { - .int => { - out_data = try std.mem.replaceOwned( - u8, - a, - out_data, - "{{ " ++ field.name ++ " }}", - try decimalToString( - a, - @field(aggregate_stats, field.name), - ), - ); - }, - .pointer => { - out_data = try std.mem.replaceOwned( - u8, - a, - out_data, - "{{ " ++ field.name ++ " }}", - @field(aggregate_stats, field.name), - ); - }, - .@"struct" => {}, - else => comptime unreachable, - } - } - + const out_data = try overview(a, aggregate_stats); const path = args.overview_output_file orelse "overview.svg"; + std.log.info("Writing overview image data to '{s}'", .{path}); const out = if (std.mem.eql(u8, path, "-")) @@ -254,72 +308,18 @@ pub fn main() !void { defer out.close(); var write_buffer: [64 * 1024]u8 = undefined; var writer = out.writer(&write_buffer); - try writer.interface.writeAll(out_data); try writer.interface.flush(); } { - const template: []const u8 = @embedFile("templates/languages.svg"); - var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); const a = arena.allocator(); - const progress = - try a.alloc([]const u8, aggregate_stats.languages.count()); - const lang_list = - try a.alloc([]const u8, aggregate_stats.languages.count()); - for ( - aggregate_stats.languages.keys(), - aggregate_stats.languages.values(), - progress, - lang_list, - 0.., - ) |language, count, *progress_s, *lang_s, i| { - const color = aggregate_stats.language_colors.get(language); - const percent = - 100 * if (aggregate_stats.languages_total == 0) - 0.0 - else - @as(f64, @floatFromInt(count)) / - @as(f64, @floatFromInt(aggregate_stats.languages_total)); - progress_s.* = try std.fmt.allocPrint(a, - \\ - , .{ color orelse "#000", percent }); - lang_s.* = try std.fmt.allocPrint(a, - \\
  • - \\ - \\ {s} - \\ {d:.2}% - \\
  • - \\ - , .{ (i + 1) * 150, color orelse "#000", language, percent }); - } - const out_data = - try std.mem.replaceOwned(u8, a, try std.mem.replaceOwned( - u8, - a, - template, - "{{ lang_list }}", - try std.mem.concat(a, u8, lang_list), - ), "{{ progress }}", try std.mem.concat(a, u8, progress)); - + const out_data = try languages(a, aggregate_stats); const path = args.overview_output_file orelse "languages.svg"; + std.log.info("Writing languages image data to '{s}'", .{path}); const out = if (std.mem.eql(u8, path, "-")) @@ -330,7 +330,6 @@ pub fn main() !void { defer out.close(); var write_buffer: [64 * 1024]u8 = undefined; var writer = out.writer(&write_buffer); - try writer.interface.writeAll(out_data); try writer.interface.flush(); } From 5e0c9661ec5f96bb5e93bcfb915339e881d70423 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 28 Mar 2026 17:10:32 -0400 Subject: [PATCH 213/303] Clean up main function --- src/main.zig | 104 ++++++++++++++---------------------- src/templates/languages.svg | 2 +- 2 files changed, 42 insertions(+), 64 deletions(-) diff --git a/src/main.zig b/src/main.zig index 6fdbb43814d..8f42129f086 100644 --- a/src/main.zig +++ b/src/main.zig @@ -187,20 +187,7 @@ pub fn main() !void { var stats: Statistics = undefined; if (args.json_input_file) |path| { - std.log.info("Reading statistics from '{s}'", .{path}); - const in = - if (std.mem.eql(u8, path, "-")) - std.fs.File.stdin() - else - try std.fs.cwd().openFile(path, .{}); - // TODO: Don't close stdin - defer in.close(); - var read_buffer: [64 * 1024]u8 = undefined; - var reader = in.reader(&read_buffer); - // TODO: Create a scanner from the reader instead of reading the whole - // file into memory - const data = - try (&reader.interface).allocRemaining(allocator, .unlimited); + const data = try readFile(allocator, path); defer allocator.free(data); stats = try Statistics.initFromJson(allocator, data); } else if (args.api_key) |api_key| { @@ -212,27 +199,16 @@ pub fn main() !void { defer stats.deinit(allocator); if (args.json_output_file) |path| { - std.log.info("Writing raw JSON data to '{s}'", .{path}); - const out = - if (std.mem.eql(u8, path, "-")) - std.fs.File.stdout() - else - try std.fs.cwd().createFile(path, .{}); - // TODO: Don't close stdout - defer out.close(); - var write_buffer: [64 * 1024]u8 = undefined; - var writer = out.writer(&write_buffer); - var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); - try writer.interface.writeAll( + try writeFile( + path, try std.json.Stringify.valueAlloc( arena.allocator(), stats, .{ .whitespace = .indent_2 }, ), ); - try writer.interface.flush(); } var aggregate_stats: struct { @@ -295,43 +271,15 @@ pub fn main() !void { defer arena.deinit(); const a = arena.allocator(); - const out_data = try overview(a, aggregate_stats); - const path = args.overview_output_file orelse "overview.svg"; - - std.log.info("Writing overview image data to '{s}'", .{path}); - const out = - if (std.mem.eql(u8, path, "-")) - std.fs.File.stdout() - else - try std.fs.cwd().createFile(path, .{}); - // TODO: Don't close stdout - defer out.close(); - var write_buffer: [64 * 1024]u8 = undefined; - var writer = out.writer(&write_buffer); - try writer.interface.writeAll(out_data); - try writer.interface.flush(); - } - - { - var arena = std.heap.ArenaAllocator.init(allocator); - defer arena.deinit(); - const a = arena.allocator(); - - const out_data = try languages(a, aggregate_stats); - const path = args.overview_output_file orelse "languages.svg"; + try writeFile( + args.overview_output_file orelse "overview.svg", + try overview(a, aggregate_stats), + ); - std.log.info("Writing languages image data to '{s}'", .{path}); - const out = - if (std.mem.eql(u8, path, "-")) - std.fs.File.stdout() - else - try std.fs.cwd().createFile(path, .{}); - // TODO: Don't close stdout - defer out.close(); - var write_buffer: [64 * 1024]u8 = undefined; - var writer = out.writer(&write_buffer); - try writer.interface.writeAll(out_data); - try writer.interface.flush(); + try writeFile( + args.languages_output_file orelse "languages.svg", + try languages(a, aggregate_stats), + ); } } @@ -339,6 +287,36 @@ test { std.testing.refAllDecls(@This()); } +fn readFile(allocator: std.mem.Allocator, path: []const u8) ![]const u8 { + std.log.info("Reading data from '{s}'", .{path}); + const in = + if (std.mem.eql(u8, path, "-")) + std.fs.File.stdin() + else + try std.fs.cwd().openFile(path, .{}); + defer if (!std.mem.eql(u8, path, "-")) in.close(); + var read_buffer: [64 * 1024]u8 = undefined; + var reader = in.reader(&read_buffer); + return try (&reader.interface).allocRemaining(allocator, .unlimited); +} + +fn writeFile( + path: []const u8, + data: []const u8, +) !void { + std.log.info("Writing data to '{s}'", .{path}); + const out = + if (std.mem.eql(u8, path, "-")) + std.fs.File.stdout() + else + try std.fs.cwd().createFile(path, .{}); + defer if (!std.mem.eql(u8, path, "-")) out.close(); + var write_buffer: [64 * 1024]u8 = undefined; + var writer = out.writer(&write_buffer); + try writer.interface.writeAll(data); + try writer.interface.flush(); +} + fn decimalToString(allocator: std.mem.Allocator, n: anytype) ![]const u8 { const info = @typeInfo(@TypeOf(n)); if (info != .int or info.int.signedness != .unsigned) { diff --git a/src/templates/languages.svg b/src/templates/languages.svg index a3754df18be..2d3aded586d 100644 --- a/src/templates/languages.svg +++ b/src/templates/languages.svg @@ -51,7 +51,7 @@ ul { li { display: inline-flex; font-size: 12px; - margin-right: 2ch; + margin-right: 1ch; align-items: center; flex-wrap: nowrap; transform: translateX(-500%); From 1c4665421c5dc0fe9f20ba1864df1613568d19ae Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 28 Mar 2026 17:34:14 -0400 Subject: [PATCH 214/303] Small fixes and tweaks --- .github/workflows/main.yml | 1 + build.zig.zon | 1 - src/main.zig | 23 +++++++++++------------ src/statistics.zig | 35 ++++++++++++++++++++++++++--------- 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1a79327ced1..51b9f6f578a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,6 +24,7 @@ jobs: run: | git config --global user.name "jstrieb/github-stats" git config --global user.email "github-stats[bot]@jstrieb.github.io" + # Push generated files to the generated branch git checkout generated || git checkout -b generated git merge master diff --git a/build.zig.zon b/build.zig.zon index c7bf1934a9c..4b090bd8871 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -10,6 +10,5 @@ "src", "LICENSE", "README.md", - "templates", }, } diff --git a/src/main.zig b/src/main.zig index 8f42129f086..a6bbfa6d787 100644 --- a/src/main.zig +++ b/src/main.zig @@ -67,12 +67,11 @@ const Args = struct { } }; -fn overview(a: std.mem.Allocator, stats: anytype) ![]const u8 { +fn overview(arena: *std.heap.ArenaAllocator, stats: anytype) ![]const u8 { + const a = arena.allocator(); const template: []const u8 = @embedFile("templates/overview.svg"); var out_data = template; - inline for ( - @typeInfo(@TypeOf(stats)).@"struct".fields, - ) |field| { + inline for (@typeInfo(@TypeOf(stats)).@"struct".fields) |field| { switch (@typeInfo(field.type)) { .int => { out_data = try std.mem.replaceOwned( @@ -99,7 +98,8 @@ fn overview(a: std.mem.Allocator, stats: anytype) ![]const u8 { return out_data; } -fn languages(a: std.mem.Allocator, stats: anytype) ![]const u8 { +fn languages(arena: *std.heap.ArenaAllocator, stats: anytype) ![]const u8 { + const a = arena.allocator(); const template: []const u8 = @embedFile("templates/languages.svg"); const progress = try a.alloc([]const u8, stats.languages.count()); const lang_list = try a.alloc([]const u8, stats.languages.count()); @@ -246,12 +246,12 @@ pub fn main() !void { aggregate_stats.views += repository.views; aggregate_stats.repos += 1; if (repository.languages) |langs| for (langs) |language| { - if (language.color) |color| { - try aggregate_stats.language_colors.put(language.name, color); - } if (glob.matchAny(excluded_langs orelse &.{}, language.name)) { continue; } + if (language.color) |color| { + try aggregate_stats.language_colors.put(language.name, color); + } var total = aggregate_stats.languages.get(language.name) orelse 0; total += language.size; try aggregate_stats.languages.put(language.name, total); @@ -262,23 +262,22 @@ pub fn main() !void { values: @TypeOf(aggregate_stats.languages.values()), pub fn lessThan(self: @This(), a: usize, b: usize) bool { // Sort in reverse order - return self.values[a] >= self.values[b]; + return self.values[a] > self.values[b]; } }{ .values = aggregate_stats.languages.values() }); { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); - const a = arena.allocator(); try writeFile( args.overview_output_file orelse "overview.svg", - try overview(a, aggregate_stats), + try overview(&arena, aggregate_stats), ); try writeFile( args.languages_output_file orelse "languages.svg", - try languages(a, aggregate_stats), + try languages(&arena, aggregate_stats), ); } } diff --git a/src/statistics.zig b/src/statistics.zig index db4f7af94f4..d51782c05cf 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -322,30 +322,40 @@ fn get_repos( .views = 0, .lines_changed = 0, }; - + errdefer repository.deinit(allocator); if (raw_repo.languages) |repo_languages| { - // TODO: Properly free partially initialized memory when any try - // fails in this block if (repo_languages.edges) |raw_languages| { repository.languages = try allocator.alloc( Language, raw_languages.len, ); + errdefer { + allocator.free(repository.languages.?); + repository.languages = null; + } for ( raw_languages, repository.languages.?, - ) |raw, *language| { + 0.., + ) |raw, *language, i| { + errdefer { + for (0..i, repository.languages.?) |_, l| { + allocator.free(l.name); + if (l.color) |c| allocator.free(c); + } + } language.* = .{ .name = try allocator.dupe(u8, raw.node.name), .size = raw.size, }; + errdefer allocator.free(language.name); if (raw.node.color) |color| { language.color = try allocator.dupe(u8, color); } + errdefer if (language.color) |c| allocator.free(c); } } } - errdefer repository.deinit(allocator); std.log.info( "Getting views for {s}...", @@ -378,13 +388,19 @@ fn get_repos( _ = try repository.get_lines_changed(arena, client, user); - try repositories.append(allocator, repository); try seen.put(raw_repo.nameWithOwner, true); + try repositories.append(allocator, repository); } } - const list = try repositories.toOwnedSlice(allocator); - std.sort.pdq(Repository, list, {}, struct { + result.repositories = try repositories.toOwnedSlice(allocator); + errdefer { + for (result.repositories) |repository| { + repository.deinit(allocator); + } + allocator.free(result.repositories); + } + std.sort.pdq(Repository, result.repositories, {}, struct { pub fn lessThanFn(_: void, lhs: Repository, rhs: Repository) bool { if (rhs.views == lhs.views) { return rhs.stars + rhs.forks < lhs.stars + lhs.forks; @@ -397,7 +413,6 @@ fn get_repos( errdefer allocator.free(result.user); result.name = try allocator.dupe(u8, name orelse user); errdefer allocator.free(result.name); - result.repositories = list; return result; } @@ -460,11 +475,13 @@ fn get_lines_changed( } } +// May not correctly free memory if there are errors during copying fn deepcopy(a: std.mem.Allocator, o: anytype) !@TypeOf(o) { return switch (@typeInfo(@TypeOf(o))) { .pointer => |p| switch (p.size) { .slice => v: { const result = try a.dupe(p.child, o); + errdefer a.free(result); for (o, result) |src, *dest| { dest.* = try deepcopy(a, src); } From f711476867c41cb0140e34a5d7ea3906af771533 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 28 Mar 2026 23:18:50 -0400 Subject: [PATCH 215/303] Subdivide time interval when too many repos --- src/statistics.zig | 437 ++++++++++++++++++++++++++------------------- 1 file changed, 249 insertions(+), 188 deletions(-) diff --git a/src/statistics.zig b/src/statistics.zig index d51782c05cf..0226af4c538 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -173,6 +173,244 @@ fn get_basic_info( }; } +fn get_repos_by_year( + allocator: std.mem.Allocator, + arena: *std.heap.ArenaAllocator, + client: *HttpClient, + user: []const u8, + result: *Statistics, + seen: *std.StringHashMap(bool), + repositories: *std.ArrayList(Repository), + year: usize, + start_month: usize, + months: usize, +) !void { + std.log.info( + "Getting {d} month{s} of data starting from {d}/{d}...", + .{ months, if (months != 1) "s" else "", start_month + 1, year }, + ); + var response, var status = try client.graphql( + \\query ($from: DateTime, $to: DateTime) { + \\ viewer { + \\ contributionsCollection(from: $from, to: $to) { + \\ totalRepositoryContributions + \\ totalIssueContributions + \\ totalCommitContributions + \\ totalPullRequestContributions + \\ totalPullRequestReviewContributions + \\ commitContributionsByRepository(maxRepositories: 100) { + \\ repository { + \\ nameWithOwner + \\ stargazerCount + \\ forkCount + \\ isPrivate + \\ languages( + \\ first: 100, + \\ orderBy: { direction: DESC, field: SIZE } + \\ ) { + \\ edges { + \\ size + \\ node { + \\ name + \\ color + \\ } + \\ } + \\ } + \\ } + \\ } + \\ } + \\ } + \\} + , + .{ + .from = try std.fmt.allocPrint( + arena.allocator(), + "{d}-{d:02}-01T00:00:00Z", + .{ year, start_month + 1 }, + ), + .to = try std.fmt.allocPrint( + arena.allocator(), + "{d}-{d:02}-01T00:00:00Z", + .{ + year + (start_month + months) / 12, + (start_month + months) % 12 + 1, + }, + ), + }, + ); + if (status != .ok) { + std.log.err( + "Failed to get data from {d} ({?s})", + .{ year, status.phrase() }, + ); + return error.RequestFailed; + } + const viewer = (std.json.parseFromSliceLeaky( + struct { data: struct { viewer: struct { + contributionsCollection: struct { + totalRepositoryContributions: u32, + totalIssueContributions: u32, + totalCommitContributions: u32, + totalPullRequestContributions: u32, + totalPullRequestReviewContributions: u32, + commitContributionsByRepository: []struct { + repository: struct { + nameWithOwner: []const u8, + stargazerCount: u32, + forkCount: u32, + isPrivate: bool, + languages: ?struct { + edges: ?[]struct { + size: u32, + node: struct { + name: []const u8, + color: ?[]const u8, + }, + }, + }, + }, + }, + }, + } } }, + arena.allocator(), + response, + .{ .ignore_unknown_fields = true }, + ) catch |err| { + std.debug.print("{s}\n", .{response}); + return err; + }).data.viewer; + + const stats = viewer.contributionsCollection; + std.log.info( + "Parsed {d} total repositories from {d}", + .{ stats.commitContributionsByRepository.len, year }, + ); + + const limit = 100; + if (stats.commitContributionsByRepository.len >= limit) { + for (&[_]usize{ 2, 3 }) |factor| { + if (months % factor == 0) { + for (0..factor) |i| { + try get_repos_by_year( + allocator, + arena, + client, + user, + result, + seen, + repositories, + year, + start_month + (months / factor) * i, + months / factor, + ); + } + return; + } + } else { + std.log.warn( + "More than {d} repos returned for {d}/{d}. " ++ + "Some data may be omitted due to GitHub API limitations.", + .{ limit, start_month + 1, year }, + ); + } + } + + result.repo_contributions += stats.totalRepositoryContributions; + result.issue_contributions += stats.totalIssueContributions; + result.commit_contributions += stats.totalCommitContributions; + result.pr_contributions += stats.totalPullRequestContributions; + result.review_contributions += + stats.totalPullRequestReviewContributions; + + for (stats.commitContributionsByRepository) |x| { + const raw_repo = x.repository; + if (seen.get(raw_repo.nameWithOwner) orelse false) { + std.log.debug( + "Skipping {s} (seen)", + .{raw_repo.nameWithOwner}, + ); + continue; + } + var repository = Repository{ + .name = try allocator.dupe(u8, raw_repo.nameWithOwner), + .stars = raw_repo.stargazerCount, + .forks = raw_repo.forkCount, + .private = raw_repo.isPrivate, + .languages = null, + .views = 0, + .lines_changed = 0, + }; + errdefer repository.deinit(allocator); + if (raw_repo.languages) |repo_languages| { + if (repo_languages.edges) |raw_languages| { + repository.languages = try allocator.alloc( + Language, + raw_languages.len, + ); + errdefer { + allocator.free(repository.languages.?); + repository.languages = null; + } + for ( + raw_languages, + repository.languages.?, + 0.., + ) |raw, *language, i| { + errdefer { + for (0..i, repository.languages.?) |_, l| { + allocator.free(l.name); + if (l.color) |c| allocator.free(c); + } + } + language.* = .{ + .name = try allocator.dupe(u8, raw.node.name), + .size = raw.size, + }; + errdefer allocator.free(language.name); + if (raw.node.color) |color| { + language.color = try allocator.dupe(u8, color); + } + errdefer if (language.color) |c| allocator.free(c); + } + } + } + + std.log.info( + "Getting views for {s}...", + .{raw_repo.nameWithOwner}, + ); + response, status = try client.rest( + try std.mem.concat( + arena.allocator(), + u8, + &.{ + "https://api.github.com/repos/", + raw_repo.nameWithOwner, + "/traffic/views", + }, + ), + ); + if (status == .ok) { + repository.views = (try std.json.parseFromSliceLeaky( + struct { count: u32 }, + arena.allocator(), + response, + .{ .ignore_unknown_fields = true }, + )).count; + } else { + std.log.info( + "Failed to get views for {s} ({?s})", + .{ raw_repo.nameWithOwner, status.phrase() }, + ); + } + + _ = try repository.get_lines_changed(arena, client, user); + + try seen.put(raw_repo.nameWithOwner, true); + try repositories.append(allocator, repository); + } +} + fn get_repos( allocator: std.mem.Allocator, arena: *std.heap.ArenaAllocator, @@ -202,195 +440,18 @@ fn get_repos( std.log.info("Getting data for user {s}...", .{user}); } for (years) |year| { - std.log.info("Getting data from {d}...", .{year}); - var response, var status = try client.graphql( - \\query ($from: DateTime, $to: DateTime) { - \\ viewer { - \\ contributionsCollection(from: $from, to: $to) { - \\ totalRepositoryContributions - \\ totalIssueContributions - \\ totalCommitContributions - \\ totalPullRequestContributions - \\ totalPullRequestReviewContributions - \\ commitContributionsByRepository(maxRepositories: 100) { - \\ repository { - \\ nameWithOwner - \\ stargazerCount - \\ forkCount - \\ isPrivate - \\ languages( - \\ first: 100, - \\ orderBy: { direction: DESC, field: SIZE } - \\ ) { - \\ edges { - \\ size - \\ node { - \\ name - \\ color - \\ } - \\ } - \\ } - \\ } - \\ } - \\ } - \\ } - \\} - , - .{ - .from = try std.fmt.allocPrint( - arena.allocator(), - "{d}-01-01T00:00:00Z", - .{year}, - ), - .to = try std.fmt.allocPrint( - arena.allocator(), - "{d}-01-01T00:00:00Z", - .{year + 1}, - ), - }, - ); - if (status != .ok) { - std.log.err( - "Failed to get data from {d} ({?s})", - .{ year, status.phrase() }, - ); - return error.RequestFailed; - } - const viewer = (try std.json.parseFromSliceLeaky( - struct { data: struct { viewer: struct { - contributionsCollection: struct { - totalRepositoryContributions: u32, - totalIssueContributions: u32, - totalCommitContributions: u32, - totalPullRequestContributions: u32, - totalPullRequestReviewContributions: u32, - commitContributionsByRepository: []struct { - repository: struct { - nameWithOwner: []const u8, - stargazerCount: u32, - forkCount: u32, - isPrivate: bool, - languages: ?struct { - edges: ?[]struct { - size: u32, - node: struct { - name: []const u8, - color: ?[]const u8, - }, - }, - }, - }, - }, - }, - } } }, - arena.allocator(), - response, - .{ .ignore_unknown_fields = true }, - )).data.viewer; - - const stats = viewer.contributionsCollection; - std.log.info( - "Parsed {d} total repositories from {d}", - .{ stats.commitContributionsByRepository.len, year }, + try get_repos_by_year( + allocator, + arena, + client, + user, + &result, + &seen, + &repositories, + year, + 0, + 12, ); - - result.repo_contributions += stats.totalRepositoryContributions; - result.issue_contributions += stats.totalIssueContributions; - result.commit_contributions += stats.totalCommitContributions; - result.pr_contributions += stats.totalPullRequestContributions; - result.review_contributions += - stats.totalPullRequestReviewContributions; - - // TODO: if there are 100 or more repositories, we should subdivide - // the date range in half - - for (stats.commitContributionsByRepository) |x| { - const raw_repo = x.repository; - if (seen.get(raw_repo.nameWithOwner) orelse false) { - std.log.debug( - "Skipping {s} (seen)", - .{raw_repo.nameWithOwner}, - ); - continue; - } - var repository = Repository{ - .name = try allocator.dupe(u8, raw_repo.nameWithOwner), - .stars = raw_repo.stargazerCount, - .forks = raw_repo.forkCount, - .private = raw_repo.isPrivate, - .languages = null, - .views = 0, - .lines_changed = 0, - }; - errdefer repository.deinit(allocator); - if (raw_repo.languages) |repo_languages| { - if (repo_languages.edges) |raw_languages| { - repository.languages = try allocator.alloc( - Language, - raw_languages.len, - ); - errdefer { - allocator.free(repository.languages.?); - repository.languages = null; - } - for ( - raw_languages, - repository.languages.?, - 0.., - ) |raw, *language, i| { - errdefer { - for (0..i, repository.languages.?) |_, l| { - allocator.free(l.name); - if (l.color) |c| allocator.free(c); - } - } - language.* = .{ - .name = try allocator.dupe(u8, raw.node.name), - .size = raw.size, - }; - errdefer allocator.free(language.name); - if (raw.node.color) |color| { - language.color = try allocator.dupe(u8, color); - } - errdefer if (language.color) |c| allocator.free(c); - } - } - } - - std.log.info( - "Getting views for {s}...", - .{raw_repo.nameWithOwner}, - ); - response, status = try client.rest( - try std.mem.concat( - arena.allocator(), - u8, - &.{ - "https://api.github.com/repos/", - raw_repo.nameWithOwner, - "/traffic/views", - }, - ), - ); - if (status == .ok) { - repository.views = (try std.json.parseFromSliceLeaky( - struct { count: u32 }, - arena.allocator(), - response, - .{ .ignore_unknown_fields = true }, - )).count; - } else { - std.log.info( - "Failed to get views for {s} ({?s})", - .{ raw_repo.nameWithOwner, status.phrase() }, - ); - } - - _ = try repository.get_lines_changed(arena, client, user); - - try seen.put(raw_repo.nameWithOwner, true); - try repositories.append(allocator, repository); - } } result.repositories = try repositories.toOwnedSlice(allocator); From c68c461c7dc76ad94cde96523adc924efa778823 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 29 Mar 2026 03:41:58 +0000 Subject: [PATCH 216/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index a71fe7a0a3e..781e1a6dae1 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -97,9 +97,9 @@ tr { All-time contributions4,554 -Lines of code changed2,780,689 +Lines of code changed2,774,091 -Repository views (past two weeks)1,473 +Repository views (past two weeks)1,525 Repositories with contributions129 From 7922acac8ab929feac1f7cea40be654dd5838ec2 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 28 Mar 2026 23:39:40 -0400 Subject: [PATCH 217/303] Move many arguments to a context object --- src/main.zig | 2 + src/statistics.zig | 110 +++++++++++++++++++++------------------------ 2 files changed, 53 insertions(+), 59 deletions(-) diff --git a/src/main.zig b/src/main.zig index a6bbfa6d787..52e3a5ee1dd 100644 --- a/src/main.zig +++ b/src/main.zig @@ -71,6 +71,7 @@ fn overview(arena: *std.heap.ArenaAllocator, stats: anytype) ![]const u8 { const a = arena.allocator(); const template: []const u8 = @embedFile("templates/overview.svg"); var out_data = template; + // Vulnerable to template injection. In practice, this should never happen. inline for (@typeInfo(@TypeOf(stats)).@"struct".fields) |field| { switch (@typeInfo(field.type)) { .int => { @@ -143,6 +144,7 @@ fn languages(arena: *std.heap.ArenaAllocator, stats: anytype) ![]const u8 { \\ , .{ (i + 1) * 150, color orelse "#000", language, percent }); } + // Vulnerable to template injection. In practice, this should never happen. return try std.mem.replaceOwned(u8, a, try std.mem.replaceOwned( u8, a, diff --git a/src/statistics.zig b/src/statistics.zig index 0226af4c538..92ba322d15e 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -174,13 +174,15 @@ fn get_basic_info( } fn get_repos_by_year( - allocator: std.mem.Allocator, - arena: *std.heap.ArenaAllocator, - client: *HttpClient, - user: []const u8, - result: *Statistics, - seen: *std.StringHashMap(bool), - repositories: *std.ArrayList(Repository), + context: struct { + allocator: std.mem.Allocator, + arena: *std.heap.ArenaAllocator, + client: *HttpClient, + user: []const u8, + result: *Statistics, + seen: *std.StringHashMap(bool), + repositories: *std.ArrayList(Repository), + }, year: usize, start_month: usize, months: usize, @@ -189,7 +191,7 @@ fn get_repos_by_year( "Getting {d} month{s} of data starting from {d}/{d}...", .{ months, if (months != 1) "s" else "", start_month + 1, year }, ); - var response, var status = try client.graphql( + var response, var status = try context.client.graphql( \\query ($from: DateTime, $to: DateTime) { \\ viewer { \\ contributionsCollection(from: $from, to: $to) { @@ -224,12 +226,12 @@ fn get_repos_by_year( , .{ .from = try std.fmt.allocPrint( - arena.allocator(), + context.arena.allocator(), "{d}-{d:02}-01T00:00:00Z", .{ year, start_month + 1 }, ), .to = try std.fmt.allocPrint( - arena.allocator(), + context.arena.allocator(), "{d}-{d:02}-01T00:00:00Z", .{ year + (start_month + months) / 12, @@ -245,7 +247,7 @@ fn get_repos_by_year( ); return error.RequestFailed; } - const viewer = (std.json.parseFromSliceLeaky( + const stats = (try std.json.parseFromSliceLeaky( struct { data: struct { viewer: struct { contributionsCollection: struct { totalRepositoryContributions: u32, @@ -272,15 +274,10 @@ fn get_repos_by_year( }, }, } } }, - arena.allocator(), + context.arena.allocator(), response, .{ .ignore_unknown_fields = true }, - ) catch |err| { - std.debug.print("{s}\n", .{response}); - return err; - }).data.viewer; - - const stats = viewer.contributionsCollection; + )).data.viewer.contributionsCollection; std.log.info( "Parsed {d} total repositories from {d}", .{ stats.commitContributionsByRepository.len, year }, @@ -292,13 +289,7 @@ fn get_repos_by_year( if (months % factor == 0) { for (0..factor) |i| { try get_repos_by_year( - allocator, - arena, - client, - user, - result, - seen, - repositories, + context, year, start_month + (months / factor) * i, months / factor, @@ -315,16 +306,16 @@ fn get_repos_by_year( } } - result.repo_contributions += stats.totalRepositoryContributions; - result.issue_contributions += stats.totalIssueContributions; - result.commit_contributions += stats.totalCommitContributions; - result.pr_contributions += stats.totalPullRequestContributions; - result.review_contributions += + context.result.repo_contributions += stats.totalRepositoryContributions; + context.result.issue_contributions += stats.totalIssueContributions; + context.result.commit_contributions += stats.totalCommitContributions; + context.result.pr_contributions += stats.totalPullRequestContributions; + context.result.review_contributions += stats.totalPullRequestReviewContributions; for (stats.commitContributionsByRepository) |x| { const raw_repo = x.repository; - if (seen.get(raw_repo.nameWithOwner) orelse false) { + if (context.seen.get(raw_repo.nameWithOwner) orelse false) { std.log.debug( "Skipping {s} (seen)", .{raw_repo.nameWithOwner}, @@ -332,7 +323,7 @@ fn get_repos_by_year( continue; } var repository = Repository{ - .name = try allocator.dupe(u8, raw_repo.nameWithOwner), + .name = try context.allocator.dupe(u8, raw_repo.nameWithOwner), .stars = raw_repo.stargazerCount, .forks = raw_repo.forkCount, .private = raw_repo.isPrivate, @@ -340,15 +331,15 @@ fn get_repos_by_year( .views = 0, .lines_changed = 0, }; - errdefer repository.deinit(allocator); + errdefer repository.deinit(context.allocator); if (raw_repo.languages) |repo_languages| { if (repo_languages.edges) |raw_languages| { - repository.languages = try allocator.alloc( + repository.languages = try context.allocator.alloc( Language, raw_languages.len, ); errdefer { - allocator.free(repository.languages.?); + context.allocator.free(repository.languages.?); repository.languages = null; } for ( @@ -358,19 +349,19 @@ fn get_repos_by_year( ) |raw, *language, i| { errdefer { for (0..i, repository.languages.?) |_, l| { - allocator.free(l.name); - if (l.color) |c| allocator.free(c); + context.allocator.free(l.name); + if (l.color) |c| context.allocator.free(c); } } language.* = .{ - .name = try allocator.dupe(u8, raw.node.name), + .name = try context.allocator.dupe(u8, raw.node.name), .size = raw.size, }; - errdefer allocator.free(language.name); + errdefer context.allocator.free(language.name); if (raw.node.color) |color| { - language.color = try allocator.dupe(u8, color); + language.color = try context.allocator.dupe(u8, color); } - errdefer if (language.color) |c| allocator.free(c); + errdefer if (language.color) |c| context.allocator.free(c); } } } @@ -379,9 +370,9 @@ fn get_repos_by_year( "Getting views for {s}...", .{raw_repo.nameWithOwner}, ); - response, status = try client.rest( + response, status = try context.client.rest( try std.mem.concat( - arena.allocator(), + context.arena.allocator(), u8, &.{ "https://api.github.com/repos/", @@ -393,7 +384,7 @@ fn get_repos_by_year( if (status == .ok) { repository.views = (try std.json.parseFromSliceLeaky( struct { count: u32 }, - arena.allocator(), + context.arena.allocator(), response, .{ .ignore_unknown_fields = true }, )).count; @@ -404,10 +395,14 @@ fn get_repos_by_year( ); } - _ = try repository.get_lines_changed(arena, client, user); + _ = try repository.get_lines_changed( + context.arena, + context.client, + context.user, + ); - try seen.put(raw_repo.nameWithOwner, true); - try repositories.append(allocator, repository); + try context.seen.put(raw_repo.nameWithOwner, true); + try context.repositories.append(context.allocator, repository); } } @@ -440,18 +435,15 @@ fn get_repos( std.log.info("Getting data for user {s}...", .{user}); } for (years) |year| { - try get_repos_by_year( - allocator, - arena, - client, - user, - &result, - &seen, - &repositories, - year, - 0, - 12, - ); + try get_repos_by_year(.{ + .allocator = allocator, + .arena = arena, + .client = client, + .user = user, + .result = &result, + .seen = &seen, + .repositories = &repositories, + }, year, 0, 12); } result.repositories = try repositories.toOwnedSlice(allocator); From 85d53f12ffecb2bc6c0d68ecfdad2a30cb07ecf5 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 29 Mar 2026 00:05:56 -0400 Subject: [PATCH 218/303] Bump actions/checkout pinned version --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 51b9f6f578a..4ca326f2578 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: Checkout history branch run: | git config --global user.name "jstrieb/github-stats" From ca71ce642f658ae222b139fa5d64aa10df8728e8 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 29 Mar 2026 00:26:08 -0400 Subject: [PATCH 219/303] Fix stripped debug logs in release builds --- src/main.zig | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 52e3a5ee1dd..2305948ddf3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -9,6 +9,9 @@ const Statistics = @import("statistics.zig"); pub const std_options: std.Options = .{ .logFn = logFn, + // Even though we change it later, this is necessary to ensure that debug + // logs aren't stripped in release builds. + .log_level = .debug, }; var log_level: std.log.Level = switch (builtin.mode) { @@ -32,6 +35,7 @@ const Args = struct { json_input_file: ?[]const u8 = null, json_output_file: ?[]const u8 = null, silent: bool = false, + debug: bool = false, verbose: bool = false, excluded_repos: ?[]const u8 = null, excluded_langs: ?[]const u8 = null, @@ -163,8 +167,10 @@ pub fn main() !void { defer args.deinit(allocator); if (args.silent) { log_level = .err; - } else if (args.verbose) { + } else if (args.debug) { log_level = .debug; + } else if (args.verbose) { + log_level = .info; } const excluded_repos = if (args.excluded_repos) |excluded| excluded: { var list = try std.ArrayList([]const u8).initCapacity(allocator, 16); From 471711c7eac1f8c14719fea0e8b2024863b93e96 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 29 Mar 2026 01:31:02 -0400 Subject: [PATCH 220/303] Fix possible (unlikely) double counting of lines --- src/statistics.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/statistics.zig b/src/statistics.zig index 92ba322d15e..103d77d674b 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -69,6 +69,7 @@ const Repository = struct { if (!std.mem.eql(u8, o.author.login, user)) { continue; } + self.lines_changed = 0; for (o.weeks) |week| { self.lines_changed += week.a; self.lines_changed += week.d; From 864885d5ea34e9fa0cc9d522310b4e7b67b1dc48 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 29 Mar 2026 01:39:06 -0400 Subject: [PATCH 221/303] Support using custom runtime templates --- src/main.zig | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/main.zig b/src/main.zig index 2305948ddf3..a6b0b2e4937 100644 --- a/src/main.zig +++ b/src/main.zig @@ -42,6 +42,8 @@ const Args = struct { exclude_private: bool = false, overview_output_file: ?[]const u8 = null, languages_output_file: ?[]const u8 = null, + overview_template: ?[]const u8 = null, + languages_template: ?[]const u8 = null, const Self = @This(); @@ -68,12 +70,17 @@ const Args = struct { if (self.excluded_langs) |s| allocator.free(s); if (self.overview_output_file) |s| allocator.free(s); if (self.languages_output_file) |s| allocator.free(s); + if (self.overview_template) |s| allocator.free(s); + if (self.languages_template) |s| allocator.free(s); } }; -fn overview(arena: *std.heap.ArenaAllocator, stats: anytype) ![]const u8 { +fn overview( + arena: *std.heap.ArenaAllocator, + stats: anytype, + template: []const u8, +) ![]const u8 { const a = arena.allocator(); - const template: []const u8 = @embedFile("templates/overview.svg"); var out_data = template; // Vulnerable to template injection. In practice, this should never happen. inline for (@typeInfo(@TypeOf(stats)).@"struct".fields) |field| { @@ -103,9 +110,12 @@ fn overview(arena: *std.heap.ArenaAllocator, stats: anytype) ![]const u8 { return out_data; } -fn languages(arena: *std.heap.ArenaAllocator, stats: anytype) ![]const u8 { +fn languages( + arena: *std.heap.ArenaAllocator, + stats: anytype, + template: []const u8, +) ![]const u8 { const a = arena.allocator(); - const template: []const u8 = @embedFile("templates/languages.svg"); const progress = try a.alloc([]const u8, stats.languages.count()); const lang_list = try a.alloc([]const u8, stats.languages.count()); for ( @@ -280,12 +290,26 @@ pub fn main() !void { try writeFile( args.overview_output_file orelse "overview.svg", - try overview(&arena, aggregate_stats), + try overview( + &arena, + aggregate_stats, + if (args.overview_template) |template| + try readFile(arena.allocator(), template) + else + @embedFile("templates/overview.svg"), + ), ); try writeFile( args.languages_output_file orelse "languages.svg", - try languages(&arena, aggregate_stats), + try languages( + &arena, + aggregate_stats, + if (args.languages_template) |template| + try readFile(arena.allocator(), template) + else + @embedFile("templates/languages.svg"), + ), ); } } From e53a8573329fb5fd89951e301bb1ac6abdf07b2b Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 29 Mar 2026 11:22:28 -0400 Subject: [PATCH 222/303] Tweak timeout --- src/statistics.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/statistics.zig b/src/statistics.zig index 103d77d674b..c48639fd029 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -515,7 +515,7 @@ fn get_lines_changed( // Exponential backoff (in expectation) with jitter item.delay += std.crypto.random.intRangeAtMost(i64, 2, item.delay); - item.delay = @min(item.delay, 240); + item.delay = @min(item.delay, 600); try q.add(item); }, else => |status| { From abde76572f099000b2ed195f86e3c5df6b3999de Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 29 Mar 2026 11:22:37 -0400 Subject: [PATCH 223/303] Cross-compile for many architectures --- build.zig | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/build.zig b/build.zig index f69ae1fce35..74d4843c466 100644 --- a/build.zig +++ b/build.zig @@ -1,16 +1,16 @@ const std = @import("std"); -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); +pub fn build(b: *std.Build) !void { + const default_target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseSafe, }); const exe = b.addExecutable(.{ - .name = "github_stats", + .name = "github-stats", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), - .target = target, + .target = default_target, .optimize = optimize, }), }); @@ -28,4 +28,55 @@ pub fn build(b: *std.Build) void { const run_tests = b.addRunArtifact(tests); const test_step = b.step("test", "Run the tests"); test_step.dependOn(&run_tests.step); + + const release_step = b.step("release", "Cross-compile release binaries"); + const release_targets: []const std.Target.Query = &.{ + // Zig tier 1 supported compiler targets (manually tested) + .{ .cpu_arch = .x86_64, .os_tag = .linux }, + .{ .cpu_arch = .x86_64, .os_tag = .macos }, + // Zig tier 2 supported compiler targets (manually tested) + .{ .cpu_arch = .aarch64, .os_tag = .macos }, + .{ .cpu_arch = .x86_64, .os_tag = .windows }, + // Zig tier 2 supported compiler targets (untested) + .{ .cpu_arch = .aarch64, .os_tag = .freebsd }, + .{ .cpu_arch = .aarch64, .os_tag = .linux }, + .{ .cpu_arch = .aarch64, .os_tag = .netbsd }, + .{ .cpu_arch = .aarch64, .os_tag = .windows }, + .{ .cpu_arch = .arm, .os_tag = .freebsd }, + .{ .cpu_arch = .arm, .os_tag = .linux }, + .{ .cpu_arch = .arm, .os_tag = .netbsd }, + .{ .cpu_arch = .loongarch64, .os_tag = .linux }, + .{ .cpu_arch = .powerpc, .os_tag = .linux }, + .{ .cpu_arch = .powerpc, .os_tag = .netbsd }, + .{ .cpu_arch = .powerpc64, .os_tag = .freebsd }, + .{ .cpu_arch = .powerpc64, .os_tag = .linux }, + .{ .cpu_arch = .powerpc64le, .os_tag = .freebsd }, + .{ .cpu_arch = .powerpc64le, .os_tag = .linux }, + .{ .cpu_arch = .riscv32, .os_tag = .linux }, + .{ .cpu_arch = .riscv64, .os_tag = .freebsd }, + .{ .cpu_arch = .riscv64, .os_tag = .linux }, + .{ .cpu_arch = .thumb, .os_tag = .windows }, + .{ .cpu_arch = .thumb, .os_tag = .linux }, + // Fails with error due to networking + // .{ .cpu_arch = .wasm32, .os_tag = .wasi }, + .{ .cpu_arch = .x86, .os_tag = .linux }, + .{ .cpu_arch = .x86, .os_tag = .windows }, + .{ .cpu_arch = .x86_64, .os_tag = .freebsd }, + .{ .cpu_arch = .x86_64, .os_tag = .netbsd }, + }; + for (release_targets) |t| { + const cross_exe = b.addExecutable(.{ + .name = try std.fmt.allocPrint( + b.allocator, + "github-stats_{s}", + .{try t.zigTriple(b.allocator)}, + ), + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = b.resolveTargetQuery(t), + .optimize = .ReleaseFast, + }), + }); + release_step.dependOn(&b.addInstallArtifact(cross_exe, .{}).step); + } } From c5314afedbcf829ebcbc12be81e58e564a0e5b63 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 29 Mar 2026 11:44:53 -0400 Subject: [PATCH 224/303] Add Actions workflow to build and upload releases --- .github/workflows/release.yml | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000000..8374f327e5b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: Build Release Binaries + +on: + push: + tags: + - '*' + workflow_dispatch: + +defaults: + run: + shell: bash -euxo pipefail {0} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: mlugg/setup-zig@v2 + with: + version: 0.15.2 + + - name: Build + run: | + zig build release + + - name: Upload Release Artifacts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + run: | + ( + cd zig-out/bin/ + gh release create \ + "${TAG}" \ + --title "${TAG} Release" \ + * + ) From a25c8f5bb898b8b4021b07c7861b0db2142ddf5e Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 29 Mar 2026 11:56:10 -0400 Subject: [PATCH 225/303] Tiny, non-functional tweak --- src/statistics.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/statistics.zig b/src/statistics.zig index c48639fd029..eeca8906d62 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -65,11 +65,11 @@ const Repository = struct { response, .{ .ignore_unknown_fields = true }, )); + self.lines_changed = 0; for (authors) |o| { if (!std.mem.eql(u8, o.author.login, user)) { continue; } - self.lines_changed = 0; for (o.weeks) |week| { self.lines_changed += week.a; self.lines_changed += week.d; From f912e56937fd648a6bb56cedf5c6ec003926d61c Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 30 Mar 2026 03:36:53 +0000 Subject: [PATCH 226/303] Update generated files --- generated/languages.svg | 18 +++++++++--------- generated/overview.svg | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 13c747d2fc3..29c3f2a99d5 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -28.90% +28.88% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.46% +17.45% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -11.98% +11.97% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.29% +9.28% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.49% +7.48% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.58% +6.57% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.27% +5.34% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.86% +1.85% diff --git a/generated/overview.svg b/generated/overview.svg index 781e1a6dae1..547d5587a95 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -95,13 +95,13 @@ tr { Forks1,186 -All-time contributions4,554 +All-time contributions4,558 -Lines of code changed2,774,091 +Lines of code changed2,780,861 -Repository views (past two weeks)1,525 +Repository views (past two weeks)1,490 -Repositories with contributions129 +Repositories with contributions130 From eb8b5130d258d1cc46050ee6e6372ded32900cb7 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 30 Mar 2026 21:24:27 -0400 Subject: [PATCH 227/303] Pin mlugg/setup-zig external action SHA --- .github/workflows/main.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4ca326f2578..7a981d89de7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,7 @@ jobs: git checkout generated || git checkout -b generated git merge master - - uses: mlugg/setup-zig@v2 + - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2 tag with: version: 0.15.2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8374f327e5b..11c3baa5465 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: mlugg/setup-zig@v2 + - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2 tag with: version: 0.15.2 From 434b02ccb096da31aca3d1d3272a94be5722fd0e Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 30 Mar 2026 21:56:38 -0400 Subject: [PATCH 228/303] Fix erroneous snake case function names --- src/argparse.zig | 20 ++++++++++---------- src/statistics.zig | 24 ++++++++++++------------ 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/argparse.zig b/src/argparse.zig index 701e52c39b2..e3b6e982df3 100644 --- a/src/argparse.zig +++ b/src/argparse.zig @@ -25,7 +25,7 @@ pub fn parse( errdefer { inline for (fields, seen) |field, seen_field| { if (seen_field) { - free_field(allocator, @field(result, field.name)); + freeField(allocator, @field(result, field.name)); } } } @@ -38,7 +38,7 @@ pub fn parse( inline for (fields, seen) |field, seen_field| { if (!seen_field) { - if (@typeInfo(strip_optional(field.type)) == .bool) { + if (@typeInfo(stripOptional(field.type)) == .bool) { @field(result, field.name) = false; } else { try stderr.print( @@ -92,7 +92,7 @@ fn setFromCli( std.mem.replaceScalar(u8, arg, '-', '_'); inline for (@typeInfo(T).@"struct".fields, seen) |field, *seen_field| { if (!seen_field.* and std.ascii.eqlIgnoreCase(arg, field.name)) { - const t = @typeInfo(strip_optional(field.type)); + const t = @typeInfo(stripOptional(field.type)); if (t == .bool) { @field(result, field.name) = true; } else { @@ -146,7 +146,7 @@ fn setFromEnv( std.mem.replaceScalar(u8, key, '-', '_'); inline for (@typeInfo(T).@"struct".fields, seen) |field, *seen_field| { if (!seen_field.* and std.ascii.eqlIgnoreCase(key, field.name)) { - switch (@typeInfo(strip_optional(field.type))) { + switch (@typeInfo(stripOptional(field.type))) { .bool => { const value = try a.dupe(u8, entry.value_ptr.*); defer a.free(value); @@ -176,7 +176,7 @@ fn setFromDefaults( inline for (@typeInfo(T).@"struct".fields, seen) |field, *seen_field| { if (!seen_field.*) { if (field.defaultValue()) |default| { - switch (@typeInfo(strip_optional(field.type))) { + switch (@typeInfo(stripOptional(field.type))) { .bool, .int, .float, .@"enum" => { @field(result, field.name) = default; }, @@ -197,7 +197,7 @@ fn printUsage(T: type, allocator: std.mem.Allocator, argv0: []const u8) !void { try stdout.print("Options:\n", .{}); const fields = @typeInfo(T).@"struct".fields; inline for (fields) |field| { - switch (@typeInfo(strip_optional(field.type))) { + switch (@typeInfo(stripOptional(field.type))) { .bool => { const flag_version = try allocator.dupe(u8, field.name); defer allocator.free(flag_version); @@ -214,16 +214,16 @@ fn printUsage(T: type, allocator: std.mem.Allocator, argv0: []const u8) !void { } } -fn strip_optional(T: type) type { +fn stripOptional(T: type) type { const info = @typeInfo(T); if (info != .optional) return T; - return strip_optional(info.optional.child); + return stripOptional(info.optional.child); } -fn free_field(allocator: std.mem.Allocator, field: anytype) void { +fn freeField(allocator: std.mem.Allocator, field: anytype) void { switch (@typeInfo(@TypeOf(field))) { .pointer => allocator.free(field), - .optional => if (field) |v| free_field(allocator, v), + .optional => if (field) |v| freeField(allocator, v), .bool, .int, .float, .@"enum" => {}, else => @compileError("Disallowed struct field type."), } diff --git a/src/statistics.zig b/src/statistics.zig index eeca8906d62..9abcd6ede5f 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -31,7 +31,7 @@ const Repository = struct { } } - pub fn get_lines_changed( + pub fn getLinesChanged( self: *@This(), arena: *std.heap.ArenaAllocator, client: *HttpClient, @@ -104,9 +104,9 @@ pub fn init(client: *HttpClient, allocator: std.mem.Allocator) !Statistics { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); - var self: Statistics = try get_repos(allocator, &arena, client); + var self: Statistics = try getRepos(allocator, &arena, client); errdefer self.deinit(allocator); - try self.get_lines_changed(&arena, client); + try self.getLinesChanged(&arena, client); return self; } @@ -132,7 +132,7 @@ pub fn deinit(self: Statistics, allocator: std.mem.Allocator) void { allocator.free(self.name); } -fn get_basic_info( +fn getBasicInfo( client: *HttpClient, allocator: std.mem.Allocator, ) !struct { []u32, []const u8, ?[]const u8 } { @@ -174,7 +174,7 @@ fn get_basic_info( }; } -fn get_repos_by_year( +fn getReposByYear( context: struct { allocator: std.mem.Allocator, arena: *std.heap.ArenaAllocator, @@ -289,7 +289,7 @@ fn get_repos_by_year( for (&[_]usize{ 2, 3 }) |factor| { if (months % factor == 0) { for (0..factor) |i| { - try get_repos_by_year( + try getReposByYear( context, year, start_month + (months / factor) * i, @@ -396,7 +396,7 @@ fn get_repos_by_year( ); } - _ = try repository.get_lines_changed( + _ = try repository.getLinesChanged( context.arena, context.client, context.user, @@ -407,7 +407,7 @@ fn get_repos_by_year( } } -fn get_repos( +fn getRepos( allocator: std.mem.Allocator, arena: *std.heap.ArenaAllocator, client: *HttpClient, @@ -429,14 +429,14 @@ fn get_repos( defer seen.deinit(); const years, const user, const name = - try get_basic_info(client, arena.allocator()); + try getBasicInfo(client, arena.allocator()); if (name) |n| { std.log.info("Getting data for {s} ({s})...", .{ n, user }); } else { std.log.info("Getting data for user {s}...", .{user}); } for (years) |year| { - try get_repos_by_year(.{ + try getReposByYear(.{ .allocator = allocator, .arena = arena, .client = client, @@ -470,7 +470,7 @@ fn get_repos( return result; } -fn get_lines_changed( +fn getLinesChanged( self: *Statistics, arena: *std.heap.ArenaAllocator, client: *HttpClient, @@ -508,7 +508,7 @@ fn get_lines_changed( }); std.Thread.sleep(delay * std.time.ns_per_s); } - switch (try item.repo.get_lines_changed(arena, client, self.user)) { + switch (try item.repo.getLinesChanged(arena, client, self.user)) { .ok => {}, .accepted => { item.timestamp = std.time.timestamp() + item.delay; From eaa124fb4bb29045abee0cee5934a9ef8ca70c3b Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 31 Mar 2026 03:23:54 +0000 Subject: [PATCH 229/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 547d5587a95..817ca677588 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,586 -Forks1,186 +Forks1,184 All-time contributions4,558 -Lines of code changed2,780,861 +Lines of code changed2,780,374 -Repository views (past two weeks)1,490 +Repository views (past two weeks)1,491 Repositories with contributions130 From f002a4c641f4e1d71167c6643cfe82c81b6e8640 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 31 Mar 2026 00:11:52 -0400 Subject: [PATCH 230/303] Make max backoff configurable --- src/argparse.zig | 14 ++++++++------ src/main.zig | 3 ++- src/statistics.zig | 11 ++++++++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/argparse.zig b/src/argparse.zig index e3b6e982df3..81d7d35cde0 100644 --- a/src/argparse.zig +++ b/src/argparse.zig @@ -106,15 +106,15 @@ fn setFromCli( std.process.exit(1); } switch (t) { - // TODO - .int, .float, .@"enum" => comptime unreachable, + .int => @field(result, field.name) = + try std.fmt.parseInt(field.type, args[i], 0), .pointer => @field( result, field.name, ) = try allocator.dupe(u8, args[i]), .bool => comptime unreachable, else => @compileError( - "Disallowed struct field type.", + "Disallowed or unimplemented struct field type.", ), } } @@ -153,13 +153,15 @@ fn setFromEnv( @field(result, field.name) = value.len > 0 and !std.ascii.eqlIgnoreCase(value, "false"); }, - // TODO - .int, .float, .@"enum" => comptime unreachable, + .int => @field(result, field.name) = + try std.fmt.parseInt(field.type, entry.value_ptr.*, 0), .pointer => @field( result, field.name, ) = try allocator.dupe(u8, entry.value_ptr.*), - else => @compileError("Disallowed struct field type."), + else => @compileError( + "Disallowed or unimplemented struct field type.", + ), } seen_field.* = true; } diff --git a/src/main.zig b/src/main.zig index a6b0b2e4937..01570d2a176 100644 --- a/src/main.zig +++ b/src/main.zig @@ -44,6 +44,7 @@ const Args = struct { languages_output_file: ?[]const u8 = null, overview_template: ?[]const u8 = null, languages_template: ?[]const u8 = null, + max_backoff: usize = 600, const Self = @This(); @@ -212,7 +213,7 @@ pub fn main() !void { std.log.info("Collecting statistics from GitHub API", .{}); var client: HttpClient = try .init(allocator, api_key); defer client.deinit(); - stats = try Statistics.init(&client, allocator); + stats = try Statistics.init(&client, allocator, args.max_backoff); } else unreachable; defer stats.deinit(allocator); diff --git a/src/statistics.zig b/src/statistics.zig index 9abcd6ede5f..f1f7bb59cef 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -100,13 +100,17 @@ const Language = struct { } }; -pub fn init(client: *HttpClient, allocator: std.mem.Allocator) !Statistics { +pub fn init( + client: *HttpClient, + allocator: std.mem.Allocator, + max_backoff: usize, +) !Statistics { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); var self: Statistics = try getRepos(allocator, &arena, client); errdefer self.deinit(allocator); - try self.getLinesChanged(&arena, client); + try self.getLinesChanged(&arena, client, max_backoff); return self; } @@ -474,6 +478,7 @@ fn getLinesChanged( self: *Statistics, arena: *std.heap.ArenaAllocator, client: *HttpClient, + max_backoff: usize, ) !void { const T = struct { repo: *Repository, @@ -515,7 +520,7 @@ fn getLinesChanged( // Exponential backoff (in expectation) with jitter item.delay += std.crypto.random.intRangeAtMost(i64, 2, item.delay); - item.delay = @min(item.delay, 600); + item.delay = @min(item.delay, max_backoff); try q.add(item); }, else => |status| { From 6e11445b9b461caa623c8fe5726f61c92e36a2b3 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 31 Mar 2026 00:23:11 -0400 Subject: [PATCH 231/303] Make args deinit more flexible --- src/main.zig | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/main.zig b/src/main.zig index 01570d2a176..0cd178b653c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -64,15 +64,27 @@ const Args = struct { } pub fn deinit(self: Self, allocator: std.mem.Allocator) void { - if (self.api_key) |s| allocator.free(s); - if (self.json_input_file) |s| allocator.free(s); - if (self.json_output_file) |s| allocator.free(s); - if (self.excluded_repos) |s| allocator.free(s); - if (self.excluded_langs) |s| allocator.free(s); - if (self.overview_output_file) |s| allocator.free(s); - if (self.languages_output_file) |s| allocator.free(s); - if (self.overview_template) |s| allocator.free(s); - if (self.languages_template) |s| allocator.free(s); + inline for (@typeInfo(Self).@"struct".fields) |field| { + switch (@typeInfo(field.type)) { + .optional => |optional| { + switch (@typeInfo(optional.child)) { + .pointer => |pointer| switch (pointer.size) { + .slice => if (@field(self, field.name)) |p| + allocator.free(p), + else => comptime unreachable, + }, + .bool, .int => {}, + else => comptime unreachable, + } + }, + .pointer => |p| switch (p.size) { + .slice => allocator.free(@field(self, field.name)), + else => comptime unreachable, + }, + .bool, .int => {}, + else => comptime unreachable, + } + } } }; From aa529a1807dc90e1ec7d5950694d2b762b66057a Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 1 Apr 2026 03:38:56 +0000 Subject: [PATCH 232/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 817ca677588..8e834e06a1f 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,586 +Stars7,587 Forks1,184 All-time contributions4,558 -Lines of code changed2,780,374 +Lines of code changed2,780,861 -Repository views (past two weeks)1,491 +Repository views (past two weeks)1,455 Repositories with contributions130 From 5f63c09ed7d3a4db1b39308231e4d9fbee6c95c4 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 1 Apr 2026 09:56:38 -0400 Subject: [PATCH 233/303] Switch a tuple to named fields --- src/statistics.zig | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/statistics.zig b/src/statistics.zig index f1f7bb59cef..d0138fde579 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -136,10 +136,11 @@ pub fn deinit(self: Statistics, allocator: std.mem.Allocator) void { allocator.free(self.name); } -fn getBasicInfo( - client: *HttpClient, - allocator: std.mem.Allocator, -) !struct { []u32, []const u8, ?[]const u8 } { +fn getBasicInfo(client: *HttpClient, allocator: std.mem.Allocator) !struct { + years: []u32, + user: []const u8, + name: ?[]const u8, +} { std.log.info("Getting contribution years...", .{}); const response, const status = try client.graphql( \\query { @@ -172,9 +173,9 @@ fn getBasicInfo( .{ .ignore_unknown_fields = true }, )).data.viewer; return .{ - parsed.contributionsCollection.contributionYears, - parsed.login, - parsed.name, + .years = parsed.contributionsCollection.contributionYears, + .user = parsed.login, + .name = parsed.name, }; } @@ -432,19 +433,18 @@ fn getRepos( var seen: std.StringHashMap(bool) = .init(arena.allocator()); defer seen.deinit(); - const years, const user, const name = - try getBasicInfo(client, arena.allocator()); - if (name) |n| { - std.log.info("Getting data for {s} ({s})...", .{ n, user }); + const info = try getBasicInfo(client, arena.allocator()); + if (info.name) |n| { + std.log.info("Getting data for {s} ({s})...", .{ n, info.user }); } else { - std.log.info("Getting data for user {s}...", .{user}); + std.log.info("Getting data for user {s}...", .{info.user}); } - for (years) |year| { + for (info.years) |year| { try getReposByYear(.{ .allocator = allocator, .arena = arena, .client = client, - .user = user, + .user = info.user, .result = &result, .seen = &seen, .repositories = &repositories, @@ -467,9 +467,9 @@ fn getRepos( } }.lessThanFn); - result.user = try allocator.dupe(u8, user); + result.user = try allocator.dupe(u8, info.user); errdefer allocator.free(result.user); - result.name = try allocator.dupe(u8, name orelse user); + result.name = try allocator.dupe(u8, info.name orelse info.user); errdefer allocator.free(result.name); return result; } From 33ca851f85a840cce963eb27dea87bacc7f7ed39 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 1 Apr 2026 09:56:56 -0400 Subject: [PATCH 234/303] Bump the GitHub REST API version --- src/http_client.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/http_client.zig b/src/http_client.zig index f735593b1eb..973769a7a55 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -145,7 +145,7 @@ pub fn rest( .authorization = .{ .override = self.bearer }, .content_type = .{ .override = "application/json" }, }, - &.{.{ .name = "X-GitHub-Api-Version", .value = "2022-11-28" }}, + &.{.{ .name = "X-GitHub-Api-Version", .value = "2026-03-10" }}, 8, ); } From 09537972e87dcb752744a4917841fa27f2ac91c8 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 1 Apr 2026 11:31:37 -0400 Subject: [PATCH 235/303] Add max retries for buggy API endpoint --- src/argparse.zig | 13 ++++++++++--- src/main.zig | 15 ++++++++++----- src/statistics.zig | 15 +++++++++++++-- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/argparse.zig b/src/argparse.zig index 81d7d35cde0..3b8bfa97a37 100644 --- a/src/argparse.zig +++ b/src/argparse.zig @@ -107,7 +107,11 @@ fn setFromCli( } switch (t) { .int => @field(result, field.name) = - try std.fmt.parseInt(field.type, args[i], 0), + try std.fmt.parseInt( + stripOptional(field.type), + args[i], + 0, + ), .pointer => @field( result, field.name, @@ -153,8 +157,11 @@ fn setFromEnv( @field(result, field.name) = value.len > 0 and !std.ascii.eqlIgnoreCase(value, "false"); }, - .int => @field(result, field.name) = - try std.fmt.parseInt(field.type, entry.value_ptr.*, 0), + .int => @field(result, field.name) = try std.fmt.parseInt( + stripOptional(field.type), + entry.value_ptr.*, + 0, + ), .pointer => @field( result, field.name, diff --git a/src/main.zig b/src/main.zig index 0cd178b653c..015d0c4690d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -45,6 +45,7 @@ const Args = struct { overview_template: ?[]const u8 = null, languages_template: ?[]const u8 = null, max_backoff: usize = 600, + max_retries: ?usize = null, const Self = @This(); @@ -216,16 +217,20 @@ pub fn main() !void { } else null; defer if (excluded_langs) |excluded| allocator.free(excluded); - var stats: Statistics = undefined; - if (args.json_input_file) |path| { + var stats: Statistics = if (args.json_input_file) |path| stats: { const data = try readFile(allocator, path); defer allocator.free(data); - stats = try Statistics.initFromJson(allocator, data); - } else if (args.api_key) |api_key| { + break :stats try Statistics.initFromJson(allocator, data); + } else if (args.api_key) |api_key| stats: { std.log.info("Collecting statistics from GitHub API", .{}); var client: HttpClient = try .init(allocator, api_key); defer client.deinit(); - stats = try Statistics.init(&client, allocator, args.max_backoff); + break :stats try Statistics.init( + &client, + allocator, + args.max_backoff, + args.max_retries, + ); } else unreachable; defer stats.deinit(allocator); diff --git a/src/statistics.zig b/src/statistics.zig index d0138fde579..94bea2ac4f8 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -104,13 +104,14 @@ pub fn init( client: *HttpClient, allocator: std.mem.Allocator, max_backoff: usize, + max_retries: ?usize, ) !Statistics { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); var self: Statistics = try getRepos(allocator, &arena, client); errdefer self.deinit(allocator); - try self.getLinesChanged(&arena, client, max_backoff); + try self.getLinesChanged(&arena, client, max_backoff, max_retries); return self; } @@ -479,11 +480,13 @@ fn getLinesChanged( arena: *std.heap.ArenaAllocator, client: *HttpClient, max_backoff: usize, + max_retries: ?usize, ) !void { const T = struct { repo: *Repository, delay: i64, timestamp: i64, + retries: usize, }; var q: std.PriorityQueue(T, void, struct { pub fn compareFn(_: void, lhs: T, rhs: T) std.math.Order { @@ -499,6 +502,7 @@ fn getLinesChanged( .repo = repo, .delay = 8, .timestamp = std.time.timestamp(), + .retries = 0, }); } while (q.count() > 0) { @@ -521,7 +525,14 @@ fn getLinesChanged( item.delay += std.crypto.random.intRangeAtMost(i64, 2, item.delay); item.delay = @min(item.delay, max_backoff); - try q.add(item); + item.retries += 1; + if (max_retries) |max| { + if (item.retries <= max) { + try q.add(item); + } + } else { + try q.add(item); + } }, else => |status| { std.log.err( From af4281cab6f8c5f918e6c61a21cdc964dc915541 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 1 Apr 2026 11:45:59 -0400 Subject: [PATCH 236/303] Add explanatory comment for convoluted logic --- src/statistics.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/statistics.zig b/src/statistics.zig index 94bea2ac4f8..5844b3a0c5a 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -291,6 +291,11 @@ fn getReposByYear( ); const limit = 100; + // This slightly convoluted logic subdivides the months range for the + // current call. It assumes the initial months range is 12, and subdivides + // by increasingly large prime factors of 12. If it cannot divide by any + // prime factors of 12, the size of the range is 1. In that case, it emits a + // warning and proceeds with processing the data. if (stats.commitContributionsByRepository.len >= limit) { for (&[_]usize{ 2, 3 }) |factor| { if (months % factor == 0) { From 31c88c8317750e01614c7749302e004e3d66c9f0 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 1 Apr 2026 12:09:26 -0400 Subject: [PATCH 237/303] Add additional accuracy caveats to README --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 63955777653..5cb04f8c29a 100644 --- a/README.md +++ b/README.md @@ -52,13 +52,23 @@ The GitHub statistics API returns inaccurate results in some situations: - If you lack permissions to access the view count for a repository, it will be tallied as zero views – this is common for external repositories where your only contribution is making a pull request -- Total lines of code modified may be inflated – it counts changes to files like - `package.json` that may impact the line count in surprising ways +- Total lines of code modified may be inflated – GitHub counts changes to files like + `package-lock.json` that may impact the line count in surprising ways + - On the other hand, GitHub refuses to count lines of code for repositories + with more than 10,000 commits, so contributions to those will not be + reflected in the data at all + - GitHub no longer supports computing contributor stats for private repos on + free accounts, so we compute them ourselves by cloning the repo locally and + tallying the stats with the git CLI – our computed totals may differ from + GitHub's - Only repositories with commit contributions are counted, so if you only open an issue on a repo, it will not show up in the statistics - Repos you created and own may not be counted if you never commit to them, or if the committer email is not connected to your GitHub account +If the calculated numbers seem strange, run the CLI locally and dump JSON output +to determine which repositories are affecting the statistics in unexpected ways. + # Installation From 8db45d48844cd86e994c294e11fb3786f0999068 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 1 Apr 2026 20:48:55 -0400 Subject: [PATCH 238/303] Clean up HTTP client --- src/http_client.zig | 93 +++++++++++++-------------------------------- 1 file changed, 26 insertions(+), 67 deletions(-) diff --git a/src/http_client.zig b/src/http_client.zig index 973769a7a55..8e440e9701d 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -1,6 +1,6 @@ -//! Naive, unoptimized HTTP client with .get and .post methods. Simple, and not -//! particularly efficient. Response bodies stay allocated for the lifetime of -//! the client. +//! Naive, unoptimized HTTP client with a .request method that wraps Zig's HTTP +//! client fetch. Simple, and not particularly efficient. Response bodies stay +//! allocated for the lifetime of the client. const std = @import("std"); @@ -11,6 +11,12 @@ bearer: []const u8, const Self = @This(); const Response = struct { []const u8, std.http.Status }; +const Request = struct { + url: []const u8, + body: ?[]const u8 = null, + headers: std.http.Client.Request.Headers = .{}, + extra_headers: []const std.http.Header = &.{}, +}; pub fn init(allocator: std.mem.Allocator, token: []const u8) !Self { const arena = try allocator.create(std.heap.ArenaAllocator); @@ -30,13 +36,7 @@ pub fn deinit(self: *Self) void { self.gpa.destroy(self.arena); } -pub fn get( - self: *Self, - url: []const u8, - headers: std.http.Client.Request.Headers, - extra_headers: []const std.http.Header, - retries: isize, -) !Response { +pub fn fetch(self: *Self, request: Request, retries: isize) !Response { if (retries <= -1) { return error.TooManyRetries; } @@ -47,10 +47,10 @@ pub fn get( ); errdefer writer.deinit(); const status = (try (self.client.fetch(.{ - .location = .{ .url = url }, + .location = .{ .url = request.url }, .response_writer = &writer.writer, - .headers = headers, - .extra_headers = extra_headers, + .payload = request.body, + .headers = request.headers, }) catch |err| switch (err) { error.HttpConnectionClosing => { // Handle a Zig HTTP bug where keep-alive connections are closed by @@ -64,48 +64,7 @@ pub fn get( ); self.client.deinit(); self.client = .{ .allocator = self.arena.allocator() }; - return self.get(url, headers, extra_headers, retries - 1); - }, - else => err, - })).status; - return .{ try writer.toOwnedSlice(), status }; -} - -pub fn post( - self: *Self, - url: []const u8, - body: []const u8, - headers: std.http.Client.Request.Headers, - retries: isize, -) !Response { - if (retries <= -1) { - return error.TooManyRetries; - } - - var writer = try std.Io.Writer.Allocating.initCapacity( - self.arena.allocator(), - 1024, - ); - errdefer writer.deinit(); - const status = (try (self.client.fetch(.{ - .location = .{ .url = url }, - .response_writer = &writer.writer, - .payload = body, - .headers = headers, - }) catch |err| switch (err) { - error.HttpConnectionClosing => { - // Handle a Zig HTTP bug where keep-alive connections are closed by - // the server after a timeout, but the client doesn't handle it - // properly. For now we nuke the whole client (and associated - // connection pool) and make a new one, but there might be a better - // way to handle this. - std.log.debug( - "Keep alive connection closed. Initializing a new client.", - .{}, - ); - self.client.deinit(); - self.client = .{ .allocator = self.arena.allocator() }; - return self.post(url, body, headers, retries - 1); + return self.fetch(request, retries - 1); }, else => err, })).status; @@ -121,31 +80,31 @@ pub fn graphql( defer arena.deinit(); const allocator = arena.allocator(); - return try self.post( - "https://api.github.com/graphql", - try std.json.Stringify.valueAlloc(allocator, .{ + return try self.fetch(.{ + .url = "https://api.github.com/graphql", + .body = try std.json.Stringify.valueAlloc(allocator, .{ .query = body, .variables = variables, }, .{}), - .{ + .headers = .{ .authorization = .{ .override = self.bearer }, .content_type = .{ .override = "application/json" }, }, - 8, - ); + }, 8); } pub fn rest( self: *Self, url: []const u8, ) !Response { - return try self.get( - url, - .{ + return try self.fetch(.{ + .url = url, + .headers = .{ .authorization = .{ .override = self.bearer }, .content_type = .{ .override = "application/json" }, }, - &.{.{ .name = "X-GitHub-Api-Version", .value = "2026-03-10" }}, - 8, - ); + .extra_headers = &.{ + .{ .name = "X-GitHub-Api-Version", .value = "2026-03-10" }, + }, + }, 8); } From 19d9e8b47a1e755c591215ca2806ee401d5ae957 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 2 Apr 2026 03:04:06 +0000 Subject: [PATCH 239/303] Update generated files --- generated/languages.svg | 18 +++++++++--------- generated/overview.svg | 12 ++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 29c3f2a99d5..dc819a98d0a 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -28.88% +28.85% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.45% +17.43% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -11.97% +12.05% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.48% +7.47% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.57% +6.56% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.34% +5.33% @@ -216,7 +216,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> SMT -2.02% +2.01% @@ -234,7 +234,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> CSS -1.70% +1.69% diff --git a/generated/overview.svg b/generated/overview.svg index 8e834e06a1f..eddb7c3333b 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,17 +91,17 @@ tr { -Stars7,587 +Stars7,589 -Forks1,184 +Forks1,185 -All-time contributions4,558 +All-time contributions4,562 -Lines of code changed2,780,861 +Lines of code changed2,957,809 -Repository views (past two weeks)1,455 +Repository views (past two weeks)1,462 -Repositories with contributions130 +Repositories with contributions131 From 159b29fac202a1333578d7cdb0b72a4b78784948 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Thu, 2 Apr 2026 01:21:45 -0400 Subject: [PATCH 240/303] Refactor HTTP client response type --- src/http_client.zig | 10 ++++++++-- src/statistics.zig | 32 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/http_client.zig b/src/http_client.zig index 8e440e9701d..f305e658bc2 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -10,7 +10,10 @@ client: std.http.Client, bearer: []const u8, const Self = @This(); -const Response = struct { []const u8, std.http.Status }; +const Response = struct { + body: []const u8, + status: std.http.Status, +}; const Request = struct { url: []const u8, body: ?[]const u8 = null, @@ -68,7 +71,10 @@ pub fn fetch(self: *Self, request: Request, retries: isize) !Response { }, else => err, })).status; - return .{ try writer.toOwnedSlice(), status }; + return .{ + .body = try writer.toOwnedSlice(), + .status = status, + }; } pub fn graphql( diff --git a/src/statistics.zig b/src/statistics.zig index 5844b3a0c5a..8cf9225c7e7 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -41,7 +41,7 @@ const Repository = struct { "Trying to get lines of code changed for {s}...", .{self.name}, ); - const response, const status = try client.rest( + const response = try client.rest( try std.mem.concat( arena.allocator(), u8, @@ -52,7 +52,7 @@ const Repository = struct { }, ), ); - if (status == .ok) { + if (response.status == .ok) { const authors = (try std.json.parseFromSliceLeaky( []struct { author: struct { login: []const u8 }, @@ -62,7 +62,7 @@ const Repository = struct { }, }, arena.allocator(), - response, + response.body, .{ .ignore_unknown_fields = true }, )); self.lines_changed = 0; @@ -85,7 +85,7 @@ const Repository = struct { }, ); } - return status; + return response.status; } }; @@ -143,7 +143,7 @@ fn getBasicInfo(client: *HttpClient, allocator: std.mem.Allocator) !struct { name: ?[]const u8, } { std.log.info("Getting contribution years...", .{}); - const response, const status = try client.graphql( + const response = try client.graphql( \\query { \\ viewer { \\ login @@ -154,10 +154,10 @@ fn getBasicInfo(client: *HttpClient, allocator: std.mem.Allocator) !struct { \\ } \\} , null); - if (status != .ok) { + if (response.status != .ok) { std.log.err( "Failed to get contribution years ({?s})", - .{status.phrase()}, + .{response.status.phrase()}, ); return error.RequestFailed; } @@ -170,7 +170,7 @@ fn getBasicInfo(client: *HttpClient, allocator: std.mem.Allocator) !struct { }, } } }, allocator, - response, + response.body, .{ .ignore_unknown_fields = true }, )).data.viewer; return .{ @@ -198,7 +198,7 @@ fn getReposByYear( "Getting {d} month{s} of data starting from {d}/{d}...", .{ months, if (months != 1) "s" else "", start_month + 1, year }, ); - var response, var status = try context.client.graphql( + var response = try context.client.graphql( \\query ($from: DateTime, $to: DateTime) { \\ viewer { \\ contributionsCollection(from: $from, to: $to) { @@ -247,10 +247,10 @@ fn getReposByYear( ), }, ); - if (status != .ok) { + if (response.status != .ok) { std.log.err( "Failed to get data from {d} ({?s})", - .{ year, status.phrase() }, + .{ year, response.status.phrase() }, ); return error.RequestFailed; } @@ -282,7 +282,7 @@ fn getReposByYear( }, } } }, context.arena.allocator(), - response, + response.body, .{ .ignore_unknown_fields = true }, )).data.viewer.contributionsCollection; std.log.info( @@ -382,7 +382,7 @@ fn getReposByYear( "Getting views for {s}...", .{raw_repo.nameWithOwner}, ); - response, status = try context.client.rest( + response = try context.client.rest( try std.mem.concat( context.arena.allocator(), u8, @@ -393,17 +393,17 @@ fn getReposByYear( }, ), ); - if (status == .ok) { + if (response.status == .ok) { repository.views = (try std.json.parseFromSliceLeaky( struct { count: u32 }, context.arena.allocator(), - response, + response.body, .{ .ignore_unknown_fields = true }, )).count; } else { std.log.info( "Failed to get views for {s} ({?s})", - .{ raw_repo.nameWithOwner, status.phrase() }, + .{ raw_repo.nameWithOwner, response.status.phrase() }, ); } From d0da20a4c7feb9d7dd212fb774f8bc5495eacf0e Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 3 Apr 2026 03:41:06 +0000 Subject: [PATCH 241/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index eddb7c3333b..def75d7a27b 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,589 +Stars7,590 Forks1,185 All-time contributions4,562 -Lines of code changed2,957,809 +Lines of code changed2,937,882 -Repository views (past two weeks)1,462 +Repository views (past two weeks)1,485 Repositories with contributions131 From 495d22e6ceb86fb6b85c2af0b1bb3d115a7f0ada Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 4 Apr 2026 03:00:30 +0000 Subject: [PATCH 242/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index def75d7a27b..0cfe92a180d 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,590 +Stars7,591 Forks1,185 All-time contributions4,562 -Lines of code changed2,937,882 +Lines of code changed2,957,809 -Repository views (past two weeks)1,485 +Repository views (past two weeks)1,500 Repositories with contributions131 From ae2ab0690573650d99e46b9e972fe2a26963d531 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 4 Apr 2026 09:45:41 -0400 Subject: [PATCH 243/303] Make max backoff optional --- src/main.zig | 2 +- src/statistics.zig | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main.zig b/src/main.zig index 015d0c4690d..93c9aab2009 100644 --- a/src/main.zig +++ b/src/main.zig @@ -44,7 +44,7 @@ const Args = struct { languages_output_file: ?[]const u8 = null, overview_template: ?[]const u8 = null, languages_template: ?[]const u8 = null, - max_backoff: usize = 600, + max_backoff: ?usize = null, max_retries: ?usize = null, const Self = @This(); diff --git a/src/statistics.zig b/src/statistics.zig index 8cf9225c7e7..3aea3069fe6 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -103,7 +103,7 @@ const Language = struct { pub fn init( client: *HttpClient, allocator: std.mem.Allocator, - max_backoff: usize, + max_backoff: ?usize, max_retries: ?usize, ) !Statistics { var arena = std.heap.ArenaAllocator.init(allocator); @@ -484,7 +484,7 @@ fn getLinesChanged( self: *Statistics, arena: *std.heap.ArenaAllocator, client: *HttpClient, - max_backoff: usize, + max_backoff: ?usize, max_retries: ?usize, ) !void { const T = struct { @@ -529,7 +529,9 @@ fn getLinesChanged( // Exponential backoff (in expectation) with jitter item.delay += std.crypto.random.intRangeAtMost(i64, 2, item.delay); - item.delay = @min(item.delay, max_backoff); + if (max_backoff) |backoff| { + item.delay = @min(item.delay, backoff); + } item.retries += 1; if (max_retries) |max| { if (item.retries <= max) { From 76d9b26ecd6a42d0b2d0e70b0c0e73eec32655e4 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 4 Apr 2026 13:05:15 -0400 Subject: [PATCH 244/303] Prevent error from delay that is too small --- src/statistics.zig | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/statistics.zig b/src/statistics.zig index 3aea3069fe6..4f5768945f2 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -527,8 +527,11 @@ fn getLinesChanged( .accepted => { item.timestamp = std.time.timestamp() + item.delay; // Exponential backoff (in expectation) with jitter - item.delay += - std.crypto.random.intRangeAtMost(i64, 2, item.delay); + item.delay += std.crypto.random.intRangeAtMost( + i64, + 2, + @max(item.delay, 2), + ); if (max_backoff) |backoff| { item.delay = @min(item.delay, backoff); } From c736d1bb0b72fcfa6e2e8990aaacf3c767962133 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 4 Apr 2026 13:11:20 -0400 Subject: [PATCH 245/303] Better template parsing and processing --- src/main.zig | 71 +++++---------------------------------- src/template.zig | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 63 deletions(-) create mode 100644 src/template.zig diff --git a/src/main.zig b/src/main.zig index 93c9aab2009..fe28c1cc9d3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3,6 +3,7 @@ const std = @import("std"); const argparse = @import("argparse.zig"); const glob = @import("glob.zig"); +const templateFill = @import("template.zig").fill; const HttpClient = @import("http_client.zig"); const Statistics = @import("statistics.zig"); @@ -95,33 +96,7 @@ fn overview( template: []const u8, ) ![]const u8 { const a = arena.allocator(); - var out_data = template; - // Vulnerable to template injection. In practice, this should never happen. - inline for (@typeInfo(@TypeOf(stats)).@"struct".fields) |field| { - switch (@typeInfo(field.type)) { - .int => { - out_data = try std.mem.replaceOwned( - u8, - a, - out_data, - "{{ " ++ field.name ++ " }}", - try decimalToString(a, @field(stats, field.name)), - ); - }, - .pointer => { - out_data = try std.mem.replaceOwned( - u8, - a, - out_data, - "{{ " ++ field.name ++ " }}", - @field(stats, field.name), - ); - }, - .@"struct" => {}, - else => comptime unreachable, - } - } - return out_data; + return templateFill(a, template, stats); } fn languages( @@ -172,14 +147,14 @@ fn languages( \\ , .{ (i + 1) * 150, color orelse "#000", language, percent }); } - // Vulnerable to template injection. In practice, this should never happen. - return try std.mem.replaceOwned(u8, a, try std.mem.replaceOwned( - u8, + return templateFill( a, template, - "{{ lang_list }}", - try std.mem.concat(a, u8, lang_list), - ), "{{ progress }}", try std.mem.concat(a, u8, progress)); + struct { lang_list: []const u8, progress: []const u8 }{ + .lang_list = try std.mem.concat(a, u8, lang_list), + .progress = try std.mem.concat(a, u8, progress), + }, + ); } pub fn main() !void { @@ -365,33 +340,3 @@ fn writeFile( try writer.interface.writeAll(data); try writer.interface.flush(); } - -fn decimalToString(allocator: std.mem.Allocator, n: anytype) ![]const u8 { - const info = @typeInfo(@TypeOf(n)); - if (info != .int or info.int.signedness != .unsigned) { - @compileError("Only implemented for unsigned integers."); - } - - const s = try std.fmt.allocPrint(allocator, "{d}", .{n}); - defer allocator.free(s); - const digits = s.len; - const commas = (digits - 1) / 3; - const result = try allocator.alloc(u8, digits + commas); - errdefer comptime unreachable; - - var i: usize = result.len - 1; - var j: usize = s.len - 1; - while (true) { - if ((result.len - i) % 4 == 0) { - result[i] = ','; - i -= 1; - } - result[i] = s[j]; - if (i == 0 and j == 0) { - break; - } else if (i > 0 and j > 0) {} else unreachable; - i -= 1; - j -= 1; - } - return result; -} diff --git a/src/template.zig b/src/template.zig new file mode 100644 index 00000000000..a71ef8620fd --- /dev/null +++ b/src/template.zig @@ -0,0 +1,86 @@ +const std = @import("std"); + +fn decimalToString(allocator: std.mem.Allocator, n: anytype) ![]const u8 { + const info = @typeInfo(@TypeOf(n)); + if (info != .int or info.int.signedness != .unsigned) { + @compileError("Only implemented for unsigned integers."); + } + + const s = try std.fmt.allocPrint(allocator, "{d}", .{n}); + defer allocator.free(s); + const digits = s.len; + const commas = (digits - 1) / 3; + const result = try allocator.alloc(u8, digits + commas); + errdefer comptime unreachable; + + var i: usize = result.len - 1; + var j: usize = s.len - 1; + while (true) { + if ((result.len - i) % 4 == 0) { + result[i] = ','; + i -= 1; + } + result[i] = s[j]; + if (i == 0 and j == 0) { + break; + } else if (i > 0 and j > 0) {} else unreachable; + i -= 1; + j -= 1; + } + return result; +} + +pub fn fill( + a: std.mem.Allocator, + template: []const u8, + o: anytype, +) ![]const u8 { + var w = try std.Io.Writer.Allocating.initCapacity(a, template.len * 2); + errdefer w.deinit(); + const writer = &(w.writer); + + var i: usize = 0; + while (i < template.len) { + if (std.mem.indexOfPos(u8, template, i, "{{")) |start| { + if (std.mem.indexOfPos(u8, template, start + 2, "}}")) |end| { + try writer.writeAll(template[i..start]); + defer i = end + 2; + const name = std.mem.trim(u8, template[start + 2 .. end], " "); + inline for ( + @typeInfo(@TypeOf(o)).@"struct".fields, + ) |f| { + if (std.mem.eql(u8, f.name, name)) { + const field = @field(o, f.name); + switch (@typeInfo(@TypeOf(field))) { + .int => { + const s = try decimalToString(a, field); + defer a.free(s); + try writer.writeAll(s); + }, + .pointer => |p| { + if (p.size != .slice or p.child != u8) { + comptime unreachable; + } + try writer.writeAll(field); + }, + .@"struct" => return error.InvalidField, + else => comptime unreachable, + } + break; + } + } else { + return error.InvalidField; + } + } else { + // If there is no closing }} treat the initial {{ as a literal + try writer.writeAll(template[i..]); + break; + } + } else { + try writer.writeAll(template[i..]); + break; + } + } + + return try w.toOwnedSlice(); +} From 5317b03a4b6bb94593c821c833b5903fc306138f Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 5 Apr 2026 03:40:58 +0000 Subject: [PATCH 246/303] Update generated files --- generated/overview.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generated/overview.svg b/generated/overview.svg index 0cfe92a180d..6fa2eb6457d 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -99,7 +99,7 @@ tr { Lines of code changed2,957,809 -Repository views (past two weeks)1,500 +Repository views (past two weeks)1,506 Repositories with contributions131 From 56688535d38ffde2b370e913265703106372a0c2 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 5 Apr 2026 11:22:15 -0400 Subject: [PATCH 247/303] Don't use one arena for all HTTP client responses --- src/http_client.zig | 39 +++++++++++++++------------------------ src/statistics.zig | 15 ++++++++++----- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/src/http_client.zig b/src/http_client.zig index f305e658bc2..65966abb7a1 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -4,8 +4,7 @@ const std = @import("std"); -gpa: std.mem.Allocator, -arena: *std.heap.ArenaAllocator, +allocator: std.mem.Allocator, client: std.http.Client, bearer: []const u8, @@ -22,21 +21,16 @@ const Request = struct { }; pub fn init(allocator: std.mem.Allocator, token: []const u8) !Self { - const arena = try allocator.create(std.heap.ArenaAllocator); - arena.* = std.heap.ArenaAllocator.init(allocator); - const a = arena.allocator(); return .{ - .gpa = allocator, - .arena = arena, - .client = .{ .allocator = a }, - .bearer = try std.fmt.allocPrint(a, "Bearer {s}", .{token}), + .allocator = allocator, + .client = .{ .allocator = allocator }, + .bearer = try std.fmt.allocPrint(allocator, "Bearer {s}", .{token}), }; } pub fn deinit(self: *Self) void { self.client.deinit(); - self.arena.deinit(); - self.gpa.destroy(self.arena); + self.allocator.free(self.bearer); } pub fn fetch(self: *Self, request: Request, retries: isize) !Response { @@ -44,10 +38,8 @@ pub fn fetch(self: *Self, request: Request, retries: isize) !Response { return error.TooManyRetries; } - var writer = try std.Io.Writer.Allocating.initCapacity( - self.arena.allocator(), - 1024, - ); + var writer = + try std.Io.Writer.Allocating.initCapacity(self.allocator, 1024); errdefer writer.deinit(); const status = (try (self.client.fetch(.{ .location = .{ .url = request.url }, @@ -66,7 +58,8 @@ pub fn fetch(self: *Self, request: Request, retries: isize) !Response { .{}, ); self.client.deinit(); - self.client = .{ .allocator = self.arena.allocator() }; + self.client = .{ .allocator = self.allocator }; + writer.deinit(); return self.fetch(request, retries - 1); }, else => err, @@ -82,16 +75,14 @@ pub fn graphql( body: []const u8, variables: anytype, ) !Response { - var arena = std.heap.ArenaAllocator.init(self.arena.allocator()); - defer arena.deinit(); - const allocator = arena.allocator(); - + const serialized = try std.json.Stringify.valueAlloc(self.allocator, .{ + .query = body, + .variables = variables, + }, .{}); + defer self.allocator.free(serialized); return try self.fetch(.{ .url = "https://api.github.com/graphql", - .body = try std.json.Stringify.valueAlloc(allocator, .{ - .query = body, - .variables = variables, - }, .{}), + .body = serialized, .headers = .{ .authorization = .{ .override = self.bearer }, .content_type = .{ .override = "application/json" }, diff --git a/src/statistics.zig b/src/statistics.zig index 4f5768945f2..6e38bd6090c 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -52,6 +52,7 @@ const Repository = struct { }, ), ); + defer client.allocator.free(response.body); if (response.status == .ok) { const authors = (try std.json.parseFromSliceLeaky( []struct { @@ -137,7 +138,7 @@ pub fn deinit(self: Statistics, allocator: std.mem.Allocator) void { allocator.free(self.name); } -fn getBasicInfo(client: *HttpClient, allocator: std.mem.Allocator) !struct { +fn getBasicInfo(client: *HttpClient, arena: *std.heap.ArenaAllocator) !struct { years: []u32, user: []const u8, name: ?[]const u8, @@ -154,6 +155,7 @@ fn getBasicInfo(client: *HttpClient, allocator: std.mem.Allocator) !struct { \\ } \\} , null); + defer client.allocator.free(response.body); if (response.status != .ok) { std.log.err( "Failed to get contribution years ({?s})", @@ -169,9 +171,9 @@ fn getBasicInfo(client: *HttpClient, allocator: std.mem.Allocator) !struct { contributionYears: []u32, }, } } }, - allocator, + arena.allocator(), response.body, - .{ .ignore_unknown_fields = true }, + .{ .ignore_unknown_fields = true, .allocate = .alloc_always }, )).data.viewer; return .{ .years = parsed.contributionsCollection.contributionYears, @@ -247,6 +249,7 @@ fn getReposByYear( ), }, ); + errdefer context.client.allocator.free(response.body); if (response.status != .ok) { std.log.err( "Failed to get data from {d} ({?s})", @@ -283,8 +286,9 @@ fn getReposByYear( } } }, context.arena.allocator(), response.body, - .{ .ignore_unknown_fields = true }, + .{ .ignore_unknown_fields = true, .allocate = .alloc_always }, )).data.viewer.contributionsCollection; + context.client.allocator.free(response.body); std.log.info( "Parsed {d} total repositories from {d}", .{ stats.commitContributionsByRepository.len, year }, @@ -393,6 +397,7 @@ fn getReposByYear( }, ), ); + defer context.client.allocator.free(response.body); if (response.status == .ok) { repository.views = (try std.json.parseFromSliceLeaky( struct { count: u32 }, @@ -439,7 +444,7 @@ fn getRepos( var seen: std.StringHashMap(bool) = .init(arena.allocator()); defer seen.deinit(); - const info = try getBasicInfo(client, arena.allocator()); + const info = try getBasicInfo(client, arena); if (info.name) |n| { std.log.info("Getting data for {s} ({s})...", .{ n, info.user }); } else { From 1c82ad1d51d9feb3eb8d04d3a258a05915543f52 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 6 Apr 2026 03:46:13 +0000 Subject: [PATCH 248/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 6fa2eb6457d..ea9f5331e07 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,7 +91,7 @@ tr { -Stars7,591 +Stars7,592 Forks1,185 @@ -99,7 +99,7 @@ tr { Lines of code changed2,957,809 -Repository views (past two weeks)1,506 +Repository views (past two weeks)1,480 Repositories with contributions131 From a106a7233612f6c090b1fc1e1b01aad953958b30 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 7 Apr 2026 03:49:17 +0000 Subject: [PATCH 249/303] Update generated files --- generated/overview.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generated/overview.svg b/generated/overview.svg index ea9f5331e07..9e4d8b14cc4 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -99,7 +99,7 @@ tr { Lines of code changed2,957,809 -Repository views (past two weeks)1,480 +Repository views (past two weeks)1,489 Repositories with contributions131 From d9b3fd51f00cd559741e9116ad88e4551a87ad99 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Tue, 7 Apr 2026 23:02:39 -0400 Subject: [PATCH 250/303] Rename API_KEY to GITHUB_TOKEN for compatibility --- src/main.zig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main.zig b/src/main.zig index fe28c1cc9d3..73d1fd03f9e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -32,7 +32,7 @@ fn logFn( } const Args = struct { - api_key: ?[]const u8 = null, + github_token: ?[]const u8 = null, json_input_file: ?[]const u8 = null, json_output_file: ?[]const u8 = null, silent: bool = false, @@ -53,9 +53,9 @@ const Args = struct { pub fn init(allocator: std.mem.Allocator) !Self { return try argparse.parse(allocator, Self, struct { fn errorCheck(a: Self, stderr: *std.Io.Writer) !bool { - if (a.api_key == null and a.json_input_file == null) { + if (a.github_token == null and a.json_input_file == null) { try stderr.print( - "You must pass either an input file or an API key.\n", + "You must pass either an input file or an GitHub token.\n", .{}, ); return false; @@ -196,9 +196,9 @@ pub fn main() !void { const data = try readFile(allocator, path); defer allocator.free(data); break :stats try Statistics.initFromJson(allocator, data); - } else if (args.api_key) |api_key| stats: { + } else if (args.github_token) |github_token| stats: { std.log.info("Collecting statistics from GitHub API", .{}); - var client: HttpClient = try .init(allocator, api_key); + var client: HttpClient = try .init(allocator, github_token); defer client.deinit(); break :stats try Statistics.init( &client, From ec0aee97ae185392caaaa27eedb14b41d4644c80 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 8 Apr 2026 03:58:30 +0000 Subject: [PATCH 251/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 9e4d8b14cc4..85378c4bb70 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,592 +Stars7,593 -Forks1,185 +Forks1,187 All-time contributions4,562 -Lines of code changed2,957,809 +Lines of code changed2,941,425 -Repository views (past two weeks)1,489 +Repository views (past two weeks)1,532 Repositories with contributions131 From bed589e3955a18c8f630c1e27d8d3510cb367e84 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 8 Apr 2026 01:05:29 -0400 Subject: [PATCH 252/303] Acutally use passed-in extra headers --- src/http_client.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/http_client.zig b/src/http_client.zig index 65966abb7a1..7b4c32959c5 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -46,6 +46,7 @@ pub fn fetch(self: *Self, request: Request, retries: isize) !Response { .response_writer = &writer.writer, .payload = request.body, .headers = request.headers, + .extra_headers = request.extra_headers, }) catch |err| switch (err) { error.HttpConnectionClosing => { // Handle a Zig HTTP bug where keep-alive connections are closed by From 8e72ddc8b9a77213701dfd7d20d0b1b4b29557b9 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 8 Apr 2026 11:59:50 -0400 Subject: [PATCH 253/303] Fix double free in HTTP client --- src/http_client.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/http_client.zig b/src/http_client.zig index 7b4c32959c5..0d9d8110362 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -40,7 +40,8 @@ pub fn fetch(self: *Self, request: Request, retries: isize) !Response { var writer = try std.Io.Writer.Allocating.initCapacity(self.allocator, 1024); - errdefer writer.deinit(); + var writer_initialized = true; + errdefer if (writer_initialized) writer.deinit(); const status = (try (self.client.fetch(.{ .location = .{ .url = request.url }, .response_writer = &writer.writer, @@ -61,6 +62,7 @@ pub fn fetch(self: *Self, request: Request, retries: isize) !Response { self.client.deinit(); self.client = .{ .allocator = self.allocator }; writer.deinit(); + writer_initialized = false; return self.fetch(request, retries - 1); }, else => err, From 3895edbd357951fe6515d345cf73d207801bb29e Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 8 Apr 2026 12:00:29 -0400 Subject: [PATCH 254/303] Add helper function for splitting arg lists --- src/main.zig | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/main.zig b/src/main.zig index 73d1fd03f9e..c1cfd0c78ed 100644 --- a/src/main.zig +++ b/src/main.zig @@ -171,25 +171,17 @@ pub fn main() !void { } else if (args.verbose) { log_level = .info; } - const excluded_repos = if (args.excluded_repos) |excluded| excluded: { - var list = try std.ArrayList([]const u8).initCapacity(allocator, 16); - errdefer list.deinit(allocator); - var iterator = std.mem.tokenizeAny(u8, excluded, ", \t\r\n|\"'\x00"); - while (iterator.next()) |pattern| { - try list.append(allocator, pattern); - } - break :excluded try list.toOwnedSlice(allocator); - } else null; + const excluded_repos = + if (args.excluded_repos) |excluded| + try splitList(allocator, excluded, " ,\t\r\n|\"'\x00") + else + null; defer if (excluded_repos) |excluded| allocator.free(excluded); - const excluded_langs = if (args.excluded_langs) |excluded| excluded: { - var list = try std.ArrayList([]const u8).initCapacity(allocator, 16); - errdefer list.deinit(allocator); - var iterator = std.mem.tokenizeAny(u8, excluded, ",\t\r\n|\"'\x00"); - while (iterator.next()) |pattern| { - try list.append(allocator, std.mem.trim(u8, pattern, " ")); - } - break :excluded try list.toOwnedSlice(allocator); - } else null; + const excluded_langs = + if (args.excluded_langs) |excluded| + try splitList(allocator, excluded, ",\t\r\n|\"'\x00") + else + null; defer if (excluded_langs) |excluded| allocator.free(excluded); var stats: Statistics = if (args.json_input_file) |path| stats: { @@ -340,3 +332,17 @@ fn writeFile( try writer.interface.writeAll(data); try writer.interface.flush(); } + +fn splitList( + allocator: std.mem.Allocator, + original: []const u8, + separators: []const u8, +) ![][]const u8 { + var list = try std.ArrayList([]const u8).initCapacity(allocator, 16); + errdefer list.deinit(allocator); + var iterator = std.mem.tokenizeAny(u8, original, separators); + while (iterator.next()) |pattern| { + try list.append(allocator, std.mem.trim(u8, pattern, " ")); + } + return try list.toOwnedSlice(allocator); +} From 821bb0a20f3fc3c32781b2cb9fbae2dc235fa15d Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Wed, 8 Apr 2026 15:23:07 -0400 Subject: [PATCH 255/303] Pull version from build.zig.zon and print --- build.zig | 5 +++++ build.zig.zon | 2 +- src/main.zig | 18 +++++++++++++++++- src/statistics.zig | 2 +- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/build.zig b/build.zig index 74d4843c466..dc0ecc44b76 100644 --- a/build.zig +++ b/build.zig @@ -5,6 +5,9 @@ pub fn build(b: *std.Build) !void { const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseSafe, }); + const version = @import("build.zig.zon").version; + const options = b.addOptions(); + options.addOption([]const u8, "version", version); const exe = b.addExecutable(.{ .name = "github-stats", @@ -14,6 +17,7 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }), }); + exe.root_module.addImport("options", options.createModule()); b.installArtifact(exe); const run_step = b.step("run", "Run the app"); @@ -77,6 +81,7 @@ pub fn build(b: *std.Build) !void { .optimize = .ReleaseFast, }), }); + cross_exe.root_module.addImport("options", options.createModule()); release_step.dependOn(&b.addInstallArtifact(cross_exe, .{}).step); } } diff --git a/build.zig.zon b/build.zig.zon index 4b090bd8871..def29b95582 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .github_stats, - .version = "0.0.0", + .version = "0.0.1", .fingerprint = 0x80bb05a632422e37, // Changing this has security and trust implications. .minimum_zig_version = "0.15.2", .dependencies = .{}, diff --git a/src/main.zig b/src/main.zig index c1cfd0c78ed..b52a3de5f99 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,5 +1,6 @@ const builtin = @import("builtin"); const std = @import("std"); +const version = @import("options").version; const argparse = @import("argparse.zig"); const glob = @import("glob.zig"); @@ -47,13 +48,16 @@ const Args = struct { languages_template: ?[]const u8 = null, max_backoff: ?usize = null, max_retries: ?usize = null, + version: bool = false, const Self = @This(); pub fn init(allocator: std.mem.Allocator) !Self { return try argparse.parse(allocator, Self, struct { fn errorCheck(a: Self, stderr: *std.Io.Writer) !bool { - if (a.github_token == null and a.json_input_file == null) { + if (a.github_token == null and a.json_input_file == null and + !a.version) + { try stderr.print( "You must pass either an input file or an GitHub token.\n", .{}, @@ -164,6 +168,18 @@ pub fn main() !void { const args = try Args.init(allocator); defer args.deinit(allocator); + if (args.version) { + const stdout = std.fs.File.stdout(); + var writer = stdout.writer(&.{}); + try writer.interface.print( + \\GitHub Stats version {s} + \\https://github.com/jstrieb/github-stats + \\ + \\Created by Jacob Strieb + \\ + , .{version}); + return; + } if (args.silent) { log_level = .err; } else if (args.debug) { diff --git a/src/statistics.zig b/src/statistics.zig index 6e38bd6090c..c8a439c8bb3 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -523,7 +523,7 @@ fn getLinesChanged( std.log.debug("Sleeping for {d}s. Waiting for {d} repo{s}.", .{ delay, q.count() + 1, - if (q.count() != 0) "s" else "", + if (q.count() + 1 != 0) "s" else "", }); std.Thread.sleep(delay * std.time.ns_per_s); } From 31089400646d3aab1cb8bef5a943cbd175c01325 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 9 Apr 2026 03:34:54 +0000 Subject: [PATCH 256/303] Update generated files --- generated/languages.svg | 14 +++++++------- generated/overview.svg | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index dc819a98d0a..2f06331f1ff 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -28.85% +28.81% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.43% +17.41% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -12.05% +12.17% @@ -171,7 +171,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> JavaScript -9.28% +9.26% @@ -180,7 +180,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Svelte -7.47% +7.46% @@ -198,7 +198,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Shell -5.33% +5.32% diff --git a/generated/overview.svg b/generated/overview.svg index 85378c4bb70..a5a09353477 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,593 +Stars7,594 Forks1,187 -All-time contributions4,562 +All-time contributions4,568 -Lines of code changed2,941,425 +Lines of code changed2,941,749 -Repository views (past two weeks)1,532 +Repository views (past two weeks)1,629 Repositories with contributions131 From 704a7cd5f9a6300cb35c81c4cb9db1db8eebc25d Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 10 Apr 2026 04:01:52 +0000 Subject: [PATCH 257/303] Update generated files --- generated/languages.svg | 10 +++++----- generated/overview.svg | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 2f06331f1ff..3ec6bf18710 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -28.81% +28.80% @@ -153,7 +153,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> C -17.41% +17.40% @@ -162,7 +162,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Zig -12.17% +12.20% @@ -189,7 +189,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Standard ML -6.56% +6.55% diff --git a/generated/overview.svg b/generated/overview.svg index a5a09353477..df66f12173f 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,594 +Stars7,593 -Forks1,187 +Forks1,188 -All-time contributions4,568 +All-time contributions4,570 -Lines of code changed2,941,749 +Lines of code changed2,958,044 -Repository views (past two weeks)1,629 +Repository views (past two weeks)1,635 Repositories with contributions131 From a60904f9dcd35051397233e14fceac82fdaced2c Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Fri, 10 Apr 2026 18:19:14 -0400 Subject: [PATCH 258/303] Put the current commit in the version --- build.zig | 10 +++++++++- src/git.zig | 24 ++++++++++++++++++++++++ src/main.zig | 1 - 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 src/git.zig diff --git a/build.zig b/build.zig index dc0ecc44b76..1b4c3a4dbcc 100644 --- a/build.zig +++ b/build.zig @@ -1,11 +1,19 @@ const std = @import("std"); +const git = @import("src/git.zig"); pub fn build(b: *std.Build) !void { const default_target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseSafe, }); - const version = @import("build.zig.zon").version; + var version: []const u8 = @import("build.zig.zon").version; + if (git.isInstalled(b.allocator)) { + version = try std.fmt.allocPrint( + b.allocator, + "{s} ({s})", + .{ version, try git.currentCommit(b.allocator) }, + ); + } const options = b.addOptions(); options.addOption([]const u8, "version", version); diff --git a/src/git.zig b/src/git.zig new file mode 100644 index 00000000000..6e6015f2cc1 --- /dev/null +++ b/src/git.zig @@ -0,0 +1,24 @@ +const std = @import("std"); + +pub fn isInstalled(gpa: std.mem.Allocator) bool { + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const run = std.process.Child.run(.{ + .allocator = arena.allocator(), + .argv = &.{ "git", "--version" }, + }) catch return false; + return switch (run.term) { + .Exited => |v| v == 0, + else => false, + }; +} + +pub fn currentCommit(gpa: std.mem.Allocator) ![]const u8 { + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const run = try std.process.Child.run(.{ + .allocator = arena.allocator(), + .argv = &.{ "git", "rev-parse", "HEAD" }, + }); + return try gpa.dupe(u8, run.stdout[0..8]); +} diff --git a/src/main.zig b/src/main.zig index b52a3de5f99..0df165bb03a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -174,7 +174,6 @@ pub fn main() !void { try writer.interface.print( \\GitHub Stats version {s} \\https://github.com/jstrieb/github-stats - \\ \\Created by Jacob Strieb \\ , .{version}); From 48e840029f56b20a9f7328487b74d2947b239fd4 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Fri, 10 Apr 2026 19:21:33 -0400 Subject: [PATCH 259/303] Remove variable backoff --- src/main.zig | 2 -- src/statistics.zig | 25 +++++++++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/main.zig b/src/main.zig index 0df165bb03a..cf265699408 100644 --- a/src/main.zig +++ b/src/main.zig @@ -46,7 +46,6 @@ const Args = struct { languages_output_file: ?[]const u8 = null, overview_template: ?[]const u8 = null, languages_template: ?[]const u8 = null, - max_backoff: ?usize = null, max_retries: ?usize = null, version: bool = false, @@ -210,7 +209,6 @@ pub fn main() !void { break :stats try Statistics.init( &client, allocator, - args.max_backoff, args.max_retries, ); } else unreachable; diff --git a/src/statistics.zig b/src/statistics.zig index c8a439c8bb3..ca68f45a881 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -104,7 +104,6 @@ const Language = struct { pub fn init( client: *HttpClient, allocator: std.mem.Allocator, - max_backoff: ?usize, max_retries: ?usize, ) !Statistics { var arena = std.heap.ArenaAllocator.init(allocator); @@ -112,7 +111,7 @@ pub fn init( var self: Statistics = try getRepos(allocator, &arena, client); errdefer self.deinit(allocator); - try self.getLinesChanged(&arena, client, max_backoff, max_retries); + try self.getLinesChanged(&arena, client, max_retries); return self; } @@ -489,7 +488,6 @@ fn getLinesChanged( self: *Statistics, arena: *std.heap.ArenaAllocator, client: *HttpClient, - max_backoff: ?usize, max_retries: ?usize, ) !void { const T = struct { @@ -510,7 +508,7 @@ fn getLinesChanged( } try q.add(.{ .repo = repo, - .delay = 8, + .delay = 0, .timestamp = std.time.timestamp(), .retries = 0, }); @@ -531,15 +529,18 @@ fn getLinesChanged( .ok => {}, .accepted => { item.timestamp = std.time.timestamp() + item.delay; + // Note: this actually works way better with a very short delay, + // hence no exponential backoff + item.delay = std.crypto.random.intRangeAtMost(i64, 0, 2); // Exponential backoff (in expectation) with jitter - item.delay += std.crypto.random.intRangeAtMost( - i64, - 2, - @max(item.delay, 2), - ); - if (max_backoff) |backoff| { - item.delay = @min(item.delay, backoff); - } + // item.delay += std.crypto.random.intRangeAtMost( + // i64, + // 2, + // @max(item.delay, 2), + // ); + // if (max_backoff) |backoff| { + // item.delay = @min(item.delay, backoff); + // } item.retries += 1; if (max_retries) |max| { if (item.retries <= max) { From 1b5688e7cea6ab3ecc2df9e0debb5f627dd10efd Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Fri, 10 Apr 2026 20:45:24 -0400 Subject: [PATCH 260/303] Don't leak repo name on error --- src/statistics.zig | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/statistics.zig b/src/statistics.zig index ca68f45a881..ea794aa6a34 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -531,16 +531,7 @@ fn getLinesChanged( item.timestamp = std.time.timestamp() + item.delay; // Note: this actually works way better with a very short delay, // hence no exponential backoff - item.delay = std.crypto.random.intRangeAtMost(i64, 0, 2); - // Exponential backoff (in expectation) with jitter - // item.delay += std.crypto.random.intRangeAtMost( - // i64, - // 2, - // @max(item.delay, 2), - // ); - // if (max_backoff) |backoff| { - // item.delay = @min(item.delay, backoff); - // } + item.delay = std.crypto.random.intRangeAtMost(i64, 0, 4); item.retries += 1; if (max_retries) |max| { if (item.retries <= max) { @@ -551,10 +542,14 @@ fn getLinesChanged( } }, else => |status| { - std.log.err( + std.log.info( "Failed to get contribution data for {s} ({?s})", .{ item.repo.name, status.phrase() }, ); + std.log.err( + "Request failed with response {?s}", + .{status.phrase()}, + ); return error.RequestFailed; }, } From a305104fd86b0792ce4e221f0779e8da0fb8fb34 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 11 Apr 2026 07:41:36 +0000 Subject: [PATCH 261/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index df66f12173f..2eedfd084da 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,593 +Stars7,594 Forks1,188 -All-time contributions4,570 +All-time contributions4,571 -Lines of code changed2,958,044 +Lines of code changed0 -Repository views (past two weeks)1,635 +Repository views (past two weeks)1,704 Repositories with contributions131 From 360a61c4775601d3f73f339d017c94f35acca808 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sun, 12 Apr 2026 03:45:39 +0000 Subject: [PATCH 262/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 2eedfd084da..c64ecc1c765 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,13 +91,13 @@ tr { -Stars7,594 +Stars7,598 -Forks1,188 +Forks1,189 All-time contributions4,571 -Lines of code changed0 +Lines of code changed2,958,553 Repository views (past two weeks)1,704 From b08f3bd7f18548873da46dfd01fc08012aaf9bbb Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Mon, 13 Apr 2026 03:54:13 +0000 Subject: [PATCH 263/303] Update generated files --- generated/languages.svg | 2 +- generated/overview.svg | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 3ec6bf18710..31df5e6f1f4 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    diff --git a/generated/overview.svg b/generated/overview.svg index c64ecc1c765..d77b278de9c 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,598 +Stars7,600 -Forks1,189 +Forks1,192 -All-time contributions4,571 +All-time contributions4,572 -Lines of code changed2,958,553 +Lines of code changed2,942,194 -Repository views (past two weeks)1,704 +Repository views (past two weeks)1,730 Repositories with contributions131 From 6afcf93d8549f55f3715e97ddeb13384029ec3d6 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 13 Apr 2026 02:24:06 -0400 Subject: [PATCH 264/303] Rename CLI arguments --- README.md | 7 ++----- src/main.zig | 31 +++++++++++++++++-------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 5cb04f8c29a..4a6caf51638 100644 --- a/README.md +++ b/README.md @@ -57,17 +57,13 @@ The GitHub statistics API returns inaccurate results in some situations: - On the other hand, GitHub refuses to count lines of code for repositories with more than 10,000 commits, so contributions to those will not be reflected in the data at all - - GitHub no longer supports computing contributor stats for private repos on - free accounts, so we compute them ourselves by cloning the repo locally and - tallying the stats with the git CLI – our computed totals may differ from - GitHub's - Only repositories with commit contributions are counted, so if you only open an issue on a repo, it will not show up in the statistics - Repos you created and own may not be counted if you never commit to them, or if the committer email is not connected to your GitHub account If the calculated numbers seem strange, run the CLI locally and dump JSON output -to determine which repositories are affecting the statistics in unexpected ways. +to determine which repositories are skewing the statistics in unexpected ways. # Installation @@ -82,6 +78,7 @@ If this project is useful to you, please support it! - Star the repository (and follow me on GitHub for more) - Share and upvote on sites like Twitter, Reddit, and Hacker News - Report any bugs, glitches, or errors that you find +- [Check out my other projects](https://jstrieb.github.io/projects/) These things motivate me to keep sharing what I build, and they provide validation that my work is appreciated! They also help me improve the project. diff --git a/src/main.zig b/src/main.zig index cf265699408..9201e21ed75 100644 --- a/src/main.zig +++ b/src/main.zig @@ -32,6 +32,9 @@ fn logFn( } } +const embedded_overview_template = @embedFile("templates/overview.svg"); +const embedded_languages_template = @embedFile("templates/languages.svg"); + const Args = struct { github_token: ?[]const u8 = null, json_input_file: ?[]const u8 = null, @@ -39,8 +42,8 @@ const Args = struct { silent: bool = false, debug: bool = false, verbose: bool = false, - excluded_repos: ?[]const u8 = null, - excluded_langs: ?[]const u8 = null, + exclude_repos: ?[]const u8 = null, + exclude_langs: ?[]const u8 = null, exclude_private: bool = false, overview_output_file: ?[]const u8 = null, languages_output_file: ?[]const u8 = null, @@ -185,18 +188,18 @@ pub fn main() !void { } else if (args.verbose) { log_level = .info; } - const excluded_repos = - if (args.excluded_repos) |excluded| - try splitList(allocator, excluded, " ,\t\r\n|\"'\x00") + const exclude_repos = + if (args.exclude_repos) |exclude| + try splitList(allocator, exclude, " ,\t\r\n|\"'\x00") else null; - defer if (excluded_repos) |excluded| allocator.free(excluded); - const excluded_langs = - if (args.excluded_langs) |excluded| - try splitList(allocator, excluded, ",\t\r\n|\"'\x00") + defer if (exclude_repos) |exclude| allocator.free(exclude); + const exclude_langs = + if (args.exclude_langs) |exclude| + try splitList(allocator, exclude, ",\t\r\n|\"'\x00") else null; - defer if (excluded_langs) |excluded| allocator.free(excluded); + defer if (exclude_langs) |exclude| allocator.free(exclude); var stats: Statistics = if (args.json_input_file) |path| stats: { const data = try readFile(allocator, path); @@ -251,7 +254,7 @@ pub fn main() !void { defer aggregate_stats.languages.deinit(); defer aggregate_stats.language_colors.deinit(); for (stats.repositories) |repository| { - if (glob.matchAny(excluded_repos orelse &.{}, repository.name) or + if (glob.matchAny(exclude_repos orelse &.{}, repository.name) or (args.exclude_private and repository.private)) { continue; @@ -262,7 +265,7 @@ pub fn main() !void { aggregate_stats.views += repository.views; aggregate_stats.repos += 1; if (repository.languages) |langs| for (langs) |language| { - if (glob.matchAny(excluded_langs orelse &.{}, language.name)) { + if (glob.matchAny(exclude_langs orelse &.{}, language.name)) { continue; } if (language.color) |color| { @@ -294,7 +297,7 @@ pub fn main() !void { if (args.overview_template) |template| try readFile(arena.allocator(), template) else - @embedFile("templates/overview.svg"), + embedded_overview_template, ), ); @@ -306,7 +309,7 @@ pub fn main() !void { if (args.languages_template) |template| try readFile(arena.allocator(), template) else - @embedFile("templates/languages.svg"), + embedded_languages_template, ), ); } From 2786fe13462ad3126c82cb4911237f224dc8927f Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 13 Apr 2026 02:27:36 -0400 Subject: [PATCH 265/303] Add CLI args to dump the templates --- src/main.zig | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main.zig b/src/main.zig index 9201e21ed75..3b7ecaa39ef 100644 --- a/src/main.zig +++ b/src/main.zig @@ -51,6 +51,8 @@ const Args = struct { languages_template: ?[]const u8 = null, max_retries: ?usize = null, version: bool = false, + dump_overview_template: ?[]const u8 = null, + dump_languages_template: ?[]const u8 = null, const Self = @This(); @@ -170,6 +172,14 @@ pub fn main() !void { const args = try Args.init(allocator); defer args.deinit(allocator); + if (args.silent) { + log_level = .err; + } else if (args.debug) { + log_level = .debug; + } else if (args.verbose) { + log_level = .info; + } + if (args.version) { const stdout = std.fs.File.stdout(); var writer = stdout.writer(&.{}); @@ -181,13 +191,17 @@ pub fn main() !void { , .{version}); return; } - if (args.silent) { - log_level = .err; - } else if (args.debug) { - log_level = .debug; - } else if (args.verbose) { - log_level = .info; + + if (args.dump_overview_template) |path| { + try writeFile(path, embedded_overview_template); + return; } + + if (args.dump_languages_template) |path| { + try writeFile(path, embedded_languages_template); + return; + } + const exclude_repos = if (args.exclude_repos) |exclude| try splitList(allocator, exclude, " ,\t\r\n|\"'\x00") From 91ea133e8f71ffe8850562165da2b321d68f08b6 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Tue, 14 Apr 2026 04:40:08 +0000 Subject: [PATCH 266/303] Update generated files --- generated/languages.svg | 8 ++++---- generated/overview.svg | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/generated/languages.svg b/generated/languages.svg index 31df5e6f1f4..f4a5567310f 100644 --- a/generated/languages.svg +++ b/generated/languages.svg @@ -132,7 +132,7 @@ div.ellipsis {
    - +
    @@ -144,7 +144,7 @@ div.ellipsis { viewBox="0 0 16 16" version="1.1" width="16" height="16"> Python -28.80% +28.79% @@ -207,7 +207,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> Go -2.37% +2.36% @@ -225,7 +225,7 @@ fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"> viewBox="0 0 16 16" version="1.1" width="16" height="16"> TeX -1.85% +1.89% diff --git a/generated/overview.svg b/generated/overview.svg index d77b278de9c..a153d2d35b9 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,600 +Stars7,601 -Forks1,192 +Forks1,193 -All-time contributions4,572 +All-time contributions4,579 -Lines of code changed2,942,194 +Lines of code changed2,642,486 -Repository views (past two weeks)1,730 +Repository views (past two weeks)1,731 Repositories with contributions131 From df20b2fe3d1bed733fede2a7aca72415fa9bc6c3 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Wed, 15 Apr 2026 08:06:48 +0000 Subject: [PATCH 267/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index a153d2d35b9..a45dbb0a8e3 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,601 +Stars7,602 Forks1,193 All-time contributions4,579 -Lines of code changed2,642,486 +Lines of code changed0 -Repository views (past two weeks)1,731 +Repository views (past two weeks)1,793 Repositories with contributions131 From b2aab96f716408cdf35db5480161c9076eac90fb Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Thu, 16 Apr 2026 08:19:42 +0000 Subject: [PATCH 268/303] Update generated files --- generated/overview.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index a45dbb0a8e3..0bf5e6b19a4 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,602 +Stars7,605 -Forks1,193 +Forks1,195 All-time contributions4,579 Lines of code changed0 -Repository views (past two weeks)1,793 +Repository views (past two weeks)1,839 Repositories with contributions131 From a18cccebd7cc0770c5b62309c0bccf8cdc35875c Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Fri, 17 Apr 2026 03:23:09 -0400 Subject: [PATCH 269/303] Set default retries and fail on empty token --- src/main.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.zig b/src/main.zig index 3b7ecaa39ef..3e1904ef7bf 100644 --- a/src/main.zig +++ b/src/main.zig @@ -49,7 +49,7 @@ const Args = struct { languages_output_file: ?[]const u8 = null, overview_template: ?[]const u8 = null, languages_template: ?[]const u8 = null, - max_retries: ?usize = null, + max_retries: ?usize = 60, version: bool = false, dump_overview_template: ?[]const u8 = null, dump_languages_template: ?[]const u8 = null, @@ -59,8 +59,8 @@ const Args = struct { pub fn init(allocator: std.mem.Allocator) !Self { return try argparse.parse(allocator, Self, struct { fn errorCheck(a: Self, stderr: *std.Io.Writer) !bool { - if (a.github_token == null and a.json_input_file == null and - !a.version) + if ((a.github_token == null or a.github_token.?.len == 0) and + a.json_input_file == null and !a.version) { try stderr.print( "You must pass either an input file or an GitHub token.\n", From ed59e37b128e2492a9e7c1ee8cf67147f33761fe Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Fri, 17 Apr 2026 03:23:26 -0400 Subject: [PATCH 270/303] Add updated install documentation --- .github/workflows/main.yml | 8 +- README.md | 147 ++++++++++++++++++++++++++++++++----- 2 files changed, 133 insertions(+), 22 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7a981d89de7..72d4f591618 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,9 +40,11 @@ jobs: run: | ./zig-out/bin/github_stats env: - API_KEY: ${{ secrets.GITHUB_TOKEN }} - EXCLUDED_REPOS: ${{ secrets.EXCLUDED_REPOS }} - EXCLUDED_LANGS: ${{ secrets.EXCLUDED_LANGS }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + EXCLUDE_REPOS: ${{ secrets.EXCLUDE_REPOS }} + EXCLUDE_LANGS: ${{ secrets.EXCLUDE_LANGS }} + EXCLUDE_PRIVATE: "false" + DEBUG: "false" - name: Commit to the repo run: | diff --git a/README.md b/README.md index 4a6caf51638..0182d055934 100644 --- a/README.md +++ b/README.md @@ -64,14 +64,120 @@ The GitHub statistics API returns inaccurate results in some situations: If the calculated numbers seem strange, run the CLI locally and dump JSON output to determine which repositories are skewing the statistics in unexpected ways. - - -# Installation - -TODO - - -# Support the Project +See [below](#analyzing-the-data) for tips. + + +## Installation + +To make your own statistics images: make a copy of this repository, make a +GitHub API token, add the token to the repository, run the Actions workflow, +and retrieve the images. + +1. [Make a "**classic**" personal access token with `read:user` and `repo` + permissions.](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) + 1. [Navigate to the personal access tokens (classic) + page.](https://github.com/settings/tokens) Open that link in a new tab, + or proceed with the steps below. + 1. Click your avatar in the top right corner, then "Settings" on the menu + that drops down. + 1. Click "Developer settings" from the menu on the left. + 1. Click "Personal access tokens", then "Tokens (classic)" from the menu + on the left. + 1. Click "Generate new token" in the top right, then "Generate new token + (classic)" in the menu that drops down. + 1. Set the expiration date to "none" (unless you want to periodically + regenerate this key). + 1. Check `repo` and `read:user` permissions. + 1. Click the green "Generate token" button at the bottom. + 1. **Copy the token and save it somewhere.** If you lose it, you will not be + able to access it again, and will have to regenerate a new one. I keep + mine saved along with the GitHub entry in my password manager. + 1. Some users report that it can take some time for the personal access + token to take effect. For more information, see + [#30](https://github.com/jstrieb/github-stats/issues/30). +1. Create a copy of this repository by clicking + [here](https://github.com/jstrieb/github-stats/generate). + - Equivalently, click the big, green "Use this template" button at the top + left of the page, then click "Create a new repository." + - Note: this is **not** the same as forking a copy because it copies + everything fresh, without the huge commit history. +1. Create a new repository secret named `GITHUB_TOKEN` with your personal + access token from the first step. + 1. [Go to the "New secret" page for your copy of this repository by clicking + this link.](../../settings/secrets/actions/new) + - If the link doesn't work, try clicking it from your copy of this + repository. + - Alternatively, go to the page manually. + 1. Click "Settings" for your copy of this repository. + 1. Click "Secrets and variables" on the left, then "Actions" from the + menu that drops down. + 1. Click the green "New repository secret" button on the "Actions + secrets and variables" page. + 1. Name your secret `GITHUB_TOKEN`. + 1. Paste your personal access token from step 1 into the large "Secret" text + box. +1. (Optional) Make other secrets for more configuration. + - To exclude some repositories from the aggregate statistics, add them + (separated by commas) to a secret called `EXCLUDE_REPOS`. + - To exclude some languages from the aggregate statistics, add them + (separated by commas) to a secret called `EXCLUDE_LANGS`. + - These can also be set directly in [the Actions + workflow](.github/workflows/main.yml), but you should set them as secrets + if you want to keep the repository names or languages private. + - Other configuration options can be set as environment variables or command + line arguments by directly editing [the Actions + workflow](.github/workflows/main.yml). +1. Go to the [Actions + page](../../actions?query=workflow%3A"Generate+Stats+Images") and click "Run + Workflow" on the right side of the screen to generate images for the first + time. + - They automatically regenerate every 24 hours, but they can be manually + regenerated by running the workflow this way. +1. Take a look at the images that have been created on the [`generated` + branch](tree/generated/). + - The [`overview.svg`](tree/generated/overview.svg) file. + - The [`languages.svg`](tree/generated/languages.svg) file. +1. To add the statistics to your GitHub profile README, copy and paste the + following lines of code into your markdown content. + - Replace `[USERNAME]` in the links below with your own username. + ``` markdown + ![](https://github.com/[USERNAME]/github-stats/blob/generated/overview.svg#gh-dark-mode-only) + ![](https://github.com/[USERNAME]/github-stats/blob/generated/overview.svg#gh-light-mode-only) + ![](https://github.com/[USERNAME]/github-stats/blob/generated/languages.svg#gh-dark-mode-only) + ![](https://github.com/[USERNAME]/github-stats/blob/generated/languages.svg#gh-light-mode-only) + [Created by `jstrieb/github-stats`.](https://github.com/jstrieb/github-stats) + ``` +1. Star this repo if you like it! + + +## Analyzing the Data + +Using the `github-stats` CLI (available on the +[releases](https://github.com/jstrieb/github-stats/releases/latest) page) to +run locally, you can dump raw statistics data to a JSON file using the +`--json-output-file` command line argument. Then, you can import the JSON file +into your programming language of choice and start analyzing. + +My preference is to use [`jq`](https://github.com/jqlang/jq) from the command +line. The command line examples below assume the JSON file is stored in +`stats.json`. + + +### List all + +List all repositories, sorted with most-viewed at the bottom. + +``` bash +jq '.repositories | sort_by(.views) | del(.[].languages)' stats.json +``` + +In that command, replace `.views` with any other field name (such as +`.lines_changed` or `.stars`) to sort by that field instead. The command +removes the languages field (using `del()`) because it can clutter the output, +making it hard to read. + + +## Support the Project If this project is useful to you, please support it! @@ -85,9 +191,7 @@ validation that my work is appreciated! They also help me improve the project. Thanks in advance! If you are insistent on spending money to show your support, I encourage you to -instead make a generous donation to one of the following organizations. By -advocating for Internet freedoms, organizations like these help me to feel -comfortable releasing work publicly on the Web. +instead make a generous donation to one of the following organizations. - [Electronic Frontier Foundation](https://supporters.eff.org/donate/) - [Signal Foundation](https://signal.org/donate/) @@ -97,21 +201,26 @@ comfortable releasing work publicly on the Web. ## Project Status -This project is actively maintained, but not actively developed. In other words, -I will fix bugs, but will rarely continue adding features (if at all). If there -are no recent commits, it means that everything has been running smoothly! +This project is actively maintained, but not actively developed. In other +words, I will fix bugs, but will rarely add features (if at all). If there are +no recent commits, it means that everything has been running smoothly! + +GitHub's APIs often have unexpected errors, downtime, and strange, +intermittent, undocumented behavior. Issues generating statistics images often +resolve themselves within a day or two, without any changes to this code or +repository. -If you want to contribute to the project, please open an issue to discuss first. -Pull requests that are not discussed with me ahead of time may be ignored. It's -nothing personal, I'm just busy, and reviewing others' code is not my idea of -fun. +If you want to contribute to the project, please open an issue and discuss +first. Pull requests that are not discussed with me ahead of time may be +ignored. It's nothing personal, I'm just busy, and reviewing others' code is +nowhere near as fun as working on other projects. Even if something were to happen to me, and I could not continue to work on the project, it will continue to work as long as the GitHub API endpoints it uses remain active and unchanged. -# Related Projects +## Related Projects - Inspired by a desire to improve upon [anuraghazra/github-readme-stats](https://github.com/anuraghazra/github-readme-stats) From 4d3f417a9f73689b0e51494134f3bbdd27cfd942 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Fri, 17 Apr 2026 08:11:55 +0000 Subject: [PATCH 271/303] Update generated files --- generated/overview.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index 0bf5e6b19a4..e59bb3a7a69 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -93,13 +93,13 @@ tr { Stars7,605 -Forks1,195 +Forks1,197 All-time contributions4,579 Lines of code changed0 -Repository views (past two weeks)1,839 +Repository views (past two weeks)1,900 Repositories with contributions131 From 5f87d80288d7be83e66c94551c7722a9c57bbdd7 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 18 Apr 2026 01:48:22 -0400 Subject: [PATCH 272/303] Get lines changed from cloned repo --- README.md | 7 ++++++ src/git.zig | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/README.md b/README.md index 0182d055934..d317744d47a 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,13 @@ The GitHub statistics API returns inaccurate results in some situations: an issue on a repo, it will not show up in the statistics - Repos you created and own may not be counted if you never commit to them, or if the committer email is not connected to your GitHub account +- [The GitHub API endpoint for computing contributor statistics no longer works + reliably](https://github.com/orgs/community/discussions/192970), so we compute + the statistics ourselves by cloning each repository locally and tallying lines + changed with the `git` CLI + - Our computed totals likely under-count relative to GitHub's, since theirs + correctly attribute authorship for contributions to pull requests with + several authors that end up squashed and merged by just one author If the calculated numbers seem strange, run the CLI locally and dump JSON output to determine which repositories are skewing the statistics in unexpected ways. diff --git a/src/git.zig b/src/git.zig index 6e6015f2cc1..46cb43ba822 100644 --- a/src/git.zig +++ b/src/git.zig @@ -22,3 +22,75 @@ pub fn currentCommit(gpa: std.mem.Allocator) ![]const u8 { }); return try gpa.dupe(u8, run.stdout[0..8]); } + +pub fn getLinesChanged( + gpa: std.mem.Allocator, + repo: []const u8, + emails: []const []const u8, +) !u32 { + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const allocator = arena.allocator(); + + const repo_path = try std.mem.replaceOwned(u8, allocator, repo, "/", "_"); + const repo_url = try std.mem.concat(allocator, u8, &.{ + "https://github.com/", repo, ".git", + }); + const clone = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &.{ + "git", + "clone", + "--bare", + "--filter=blob:limit=1m", + "--no-tags", + "--single-branch", + repo_url, + repo_path, + }, + }); + switch (clone.term) { + .Exited => |v| if (v != 0) return error.CloneFailed, + else => return error.CloneFailed, + } + defer std.fs.cwd().deleteTree(repo_path) catch {}; + + const email_args = try allocator.alloc([]const u8, emails.len * 2); + for (emails, 0..) |email, i| { + email_args[i * 2] = "--author"; + email_args[i * 2 + 1] = email; + } + const log_args = try std.mem.concat(allocator, []const u8, &.{ + &.{ + "git", + "-C", + repo_path, + "log", + "--numstat", + "--pretty=tformat:", + }, + email_args, + }); + const log = try std.process.Child.run(.{ + .allocator = allocator, + .argv = log_args, + .max_output_bytes = 64 * 1024 * 1024 * 1024, + }); + switch (log.term) { + .Exited => |v| if (v != 0) return error.LogFailed, + else => return error.LogFailed, + } + + var lines_changed: u32 = 0; + var lines = std.mem.tokenizeScalar(u8, log.stdout, '\n'); + while (lines.next()) |line| { + if (line.len == 0) continue; + var parts = std.mem.tokenizeAny(u8, line, " \t"); + const additions = + std.fmt.parseUnsigned(u32, parts.next().?, 10) catch 0; + const deletions = + std.fmt.parseUnsigned(u32, parts.next().?, 10) catch 0; + lines_changed += additions + deletions; + } + return lines_changed; +} From 631c67ad42b8f6f6469a6c705cd512ee146fba15 Mon Sep 17 00:00:00 2001 From: jstrieb/github-stats Date: Sat, 18 Apr 2026 08:02:56 +0000 Subject: [PATCH 273/303] Update generated files --- generated/overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generated/overview.svg b/generated/overview.svg index e59bb3a7a69..17e40742ad2 100644 --- a/generated/overview.svg +++ b/generated/overview.svg @@ -91,15 +91,15 @@ tr { -Stars7,605 +Stars7,606 -Forks1,197 +Forks1,198 -All-time contributions4,579 +All-time contributions4,581 Lines of code changed0 -Repository views (past two weeks)1,900 +Repository views (past two weeks)1,901 Repositories with contributions131 From 85053114b4e081e1bc769641d587b917367d085b Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 18 Apr 2026 22:15:48 -0400 Subject: [PATCH 274/303] Use user emails and cloned repos for lines_changed --- README.md | 10 ++++-- src/git.zig | 25 +++++++++++--- src/http_client.zig | 9 ++++- src/statistics.zig | 82 ++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 114 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d317744d47a..4a4465e5a0b 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,8 @@ To make your own statistics images: make a copy of this repository, make a GitHub API token, add the token to the repository, run the Actions workflow, and retrieve the images. -1. [Make a "**classic**" personal access token with `read:user` and `repo` +1. [Make a "**classic**" personal access token with `read:user`, `user:email`, + and `repo` permissions.](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) 1. [Navigate to the personal access tokens (classic) page.](https://github.com/settings/tokens) Open that link in a new tab, @@ -94,7 +95,12 @@ and retrieve the images. (classic)" in the menu that drops down. 1. Set the expiration date to "none" (unless you want to periodically regenerate this key). - 1. Check `repo` and `read:user` permissions. + 1. Check `read:user` and `user:email` and `repo` permissions. + - `read:user` and `repo` permissions are necessary for reading user and + repository metadata to calculate statistics + - `user:email` permission is necessary for correctly attributing commits + to the user when cloning repositories locally to compute lines of code + changed 1. Click the green "Generate token" button at the bottom. 1. **Copy the token and save it somewhere.** If you lose it, you will not be able to access it again, and will have to regenerate a new one. I keep diff --git a/src/git.zig b/src/git.zig index 46cb43ba822..c4a743fd455 100644 --- a/src/git.zig +++ b/src/git.zig @@ -1,19 +1,29 @@ const std = @import("std"); +var is_installed: ?bool = null; + pub fn isInstalled(gpa: std.mem.Allocator) bool { + if (is_installed) |v| { + return v; + } var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const run = std.process.Child.run(.{ .allocator = arena.allocator(), .argv = &.{ "git", "--version" }, - }) catch return false; - return switch (run.term) { + }) catch { + is_installed = false; + return is_installed.?; + }; + is_installed = switch (run.term) { .Exited => |v| v == 0, else => false, }; + return is_installed.?; } pub fn currentCommit(gpa: std.mem.Allocator) ![]const u8 { + if (!isInstalled(gpa)) return error.GitNotInstalled; var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const run = try std.process.Child.run(.{ @@ -25,17 +35,22 @@ pub fn currentCommit(gpa: std.mem.Allocator) ![]const u8 { pub fn getLinesChanged( gpa: std.mem.Allocator, + login: []const u8, + token: []const u8, repo: []const u8, emails: []const []const u8, ) !u32 { + if (!isInstalled(gpa)) return error.GitNotInstalled; var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const allocator = arena.allocator(); const repo_path = try std.mem.replaceOwned(u8, allocator, repo, "/", "_"); - const repo_url = try std.mem.concat(allocator, u8, &.{ - "https://github.com/", repo, ".git", - }); + const repo_url = try std.fmt.allocPrint( + allocator, + "https://{s}:{s}@github.com/{s}.git", + .{ login, token, repo }, + ); const clone = try std.process.Child.run(.{ .allocator = allocator, .argv = &.{ diff --git a/src/http_client.zig b/src/http_client.zig index 0d9d8110362..1bc5ca4f708 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -7,6 +7,7 @@ const std = @import("std"); allocator: std.mem.Allocator, client: std.http.Client, bearer: []const u8, +token: []const u8, const Self = @This(); const Response = struct { @@ -21,16 +22,22 @@ const Request = struct { }; pub fn init(allocator: std.mem.Allocator, token: []const u8) !Self { + const bearer = try std.fmt.allocPrint(allocator, "Bearer {s}", .{token}); + errdefer allocator.free(bearer); + const cloned_token = try allocator.dupe(u8, token); + errdefer allocator.free(cloned_token); return .{ .allocator = allocator, .client = .{ .allocator = allocator }, - .bearer = try std.fmt.allocPrint(allocator, "Bearer {s}", .{token}), + .bearer = bearer, + .token = cloned_token, }; } pub fn deinit(self: *Self) void { self.client.deinit(); self.allocator.free(self.bearer); + self.allocator.free(self.token); } pub fn fetch(self: *Self, request: Request, retries: isize) !Response { diff --git a/src/statistics.zig b/src/statistics.zig index ea794aa6a34..bea380a6daf 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -1,9 +1,11 @@ const std = @import("std"); +const git = @import("git.zig"); const HttpClient = @import("http_client.zig"); repositories: []Repository, user: []const u8, name: []const u8, +emails: [][]const u8, repo_contributions: u32 = 0, issue_contributions: u32 = 0, commit_contributions: u32 = 0, @@ -135,12 +137,17 @@ pub fn deinit(self: Statistics, allocator: std.mem.Allocator) void { allocator.free(self.repositories); allocator.free(self.user); allocator.free(self.name); + for (self.emails) |email| { + allocator.free(email); + } + allocator.free(self.emails); } fn getBasicInfo(client: *HttpClient, arena: *std.heap.ArenaAllocator) !struct { years: []u32, user: []const u8, name: ?[]const u8, + emails: [][]const u8, } { std.log.info("Getting contribution years...", .{}); const response = try client.graphql( @@ -174,10 +181,43 @@ fn getBasicInfo(client: *HttpClient, arena: *std.heap.ArenaAllocator) !struct { response.body, .{ .ignore_unknown_fields = true, .allocate = .alloc_always }, )).data.viewer; + + std.log.info("Getting contributor emails...", .{}); + const email_response = + try client.rest("https://api.github.com/user/emails"); + defer client.allocator.free(email_response.body); + var emails: [][]const u8 = &.{}; + if (email_response.status == .ok) { + const parsed_emails = (try std.json.parseFromSliceLeaky( + []struct { email: []const u8 }, + arena.allocator(), + email_response.body, + .{ .ignore_unknown_fields = true, .allocate = .alloc_always }, + )); + if (parsed_emails.len > 0) { + emails = try arena.allocator().alloc([]const u8, parsed_emails.len); + for (parsed_emails, emails) |src, *dest| { + dest.* = src.email; + } + } + } else { + std.log.err("Failed to get user emails. " ++ + "Token may be missing `user:email` permission.", .{}); + } + if (emails.len == 0) { + emails = try arena.allocator().alloc([]const u8, 1); + emails[0] = try std.fmt.allocPrint( + arena.allocator(), + "{s}@users.noreply.github.com", + .{parsed.login}, + ); + } + return .{ .years = parsed.contributionsCollection.contributionYears, .user = parsed.login, .name = parsed.name, + .emails = emails, }; } @@ -430,6 +470,7 @@ fn getRepos( var result: Statistics = .{ .user = undefined, .name = undefined, + .emails = undefined, .repositories = undefined, }; var repositories: std.ArrayList(Repository) = @@ -449,6 +490,28 @@ fn getRepos( } else { std.log.info("Getting data for user {s}...", .{info.user}); } + + result.user = try allocator.dupe(u8, info.user); + errdefer allocator.free(result.user); + result.name = try allocator.dupe(u8, info.name orelse info.user); + errdefer allocator.free(result.name); + + result.emails = try allocator.alloc([]const u8, info.emails.len); + errdefer allocator.free(result.emails); + for (result.emails, info.emails, 0..) |*dest, src, i| { + errdefer { + for (result.emails[0..i]) |email| { + allocator.free(email); + } + } + dest.* = try allocator.dupe(u8, src); + } + errdefer { + for (result.emails) |email| { + allocator.free(email); + } + } + for (info.years) |year| { try getReposByYear(.{ .allocator = allocator, @@ -477,10 +540,6 @@ fn getRepos( } }.lessThanFn); - result.user = try allocator.dupe(u8, info.user); - errdefer allocator.free(result.user); - result.name = try allocator.dupe(u8, info.name orelse info.user); - errdefer allocator.free(result.name); return result; } @@ -536,6 +595,21 @@ fn getLinesChanged( if (max_retries) |max| { if (item.retries <= max) { try q.add(item); + } else { + std.log.info( + "Cloning {s} to get lines changed...", + .{item.repo.name}, + ); + item.repo.lines_changed = git.getLinesChanged( + arena.allocator(), + self.user, + client.token, + item.repo.name, + self.emails, + ) catch |e| switch (e) { + error.GitNotInstalled => 0, + else => return e, + }; } } else { try q.add(item); From 6250a968eaed5939afde93efaced29aecc502849 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 18 Apr 2026 23:02:19 -0400 Subject: [PATCH 275/303] Mention globbing patterns in docs --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 4a4465e5a0b..8c1e6bd614a 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,9 @@ and retrieve the images. (separated by commas) to a secret called `EXCLUDE_REPOS`. - To exclude some languages from the aggregate statistics, add them (separated by commas) to a secret called `EXCLUDE_LANGS`. + - Lists for `EXCLUDE_REPOS` and `EXCLUDE_LANGS` can use globbing patterns. + For example, to exclude all repos by "@jstrieb", add `jstrieb/*` to + `EXCLUDE_REPOS`. - These can also be set directly in [the Actions workflow](.github/workflows/main.yml), but you should set them as secrets if you want to keep the repository names or languages private. From bdda1de05e185fda559b1c6065ad3c32e01c8a9d Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 18 Apr 2026 23:36:26 -0400 Subject: [PATCH 276/303] Clean up docs slightly --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8c1e6bd614a..55017736703 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ dark theme. ## Background When someone views a GitHub profile, it is often because they are curious about -the user's open source contributions. Unfortunately, that user's stars, forks, +the user's open-source contributions. Unfortunately, that user's stars, forks, and pinned repositories do not necessarily reflect the contributions they make to private repositories. The data likewise does not present a complete picture of the user's total contributions beyond the current year. @@ -94,13 +94,13 @@ and retrieve the images. 1. Click "Generate new token" in the top right, then "Generate new token (classic)" in the menu that drops down. 1. Set the expiration date to "none" (unless you want to periodically - regenerate this key). - 1. Check `read:user` and `user:email` and `repo` permissions. + regenerate this token). + 1. Check `read:user`, `user:email`, and `repo` permissions. - `read:user` and `repo` permissions are necessary for reading user and - repository metadata to calculate statistics + repository metadata to calculate statistics. - `user:email` permission is necessary for correctly attributing commits to the user when cloning repositories locally to compute lines of code - changed + changed. 1. Click the green "Generate token" button at the bottom. 1. **Copy the token and save it somewhere.** If you lose it, you will not be able to access it again, and will have to regenerate a new one. I keep @@ -154,7 +154,7 @@ and retrieve the images. - The [`overview.svg`](tree/generated/overview.svg) file. - The [`languages.svg`](tree/generated/languages.svg) file. 1. To add the statistics to your GitHub profile README, copy and paste the - following lines of code into your markdown content. + following lines of code into your markdown content. - Replace `[USERNAME]` in the links below with your own username. ``` markdown ![](https://github.com/[USERNAME]/github-stats/blob/generated/overview.svg#gh-dark-mode-only) @@ -171,17 +171,17 @@ and retrieve the images. Using the `github-stats` CLI (available on the [releases](https://github.com/jstrieb/github-stats/releases/latest) page) to run locally, you can dump raw statistics data to a JSON file using the -`--json-output-file` command line argument. Then, you can import the JSON file -into your programming language of choice and start analyzing. +`--json-output-file` command-line argument. Then, you can import the JSON file +into your programming language of choice and start analyzing. My preference is to use [`jq`](https://github.com/jqlang/jq) from the command -line. The command line examples below assume the JSON file is stored in +line. The command-line examples below assume the JSON file is stored in `stats.json`. ### List all -List all repositories, sorted with most-viewed at the bottom. +List all repositories, sorted with most-viewed at the bottom. ``` bash jq '.repositories | sort_by(.views) | del(.[].languages)' stats.json From 2cf5b9efb76b3a9094ec229c865d6b4627c6224f Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 18 Apr 2026 23:39:51 -0400 Subject: [PATCH 277/303] Fix workflow --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 72d4f591618..033f9b2bf9c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,7 +26,7 @@ jobs: git config --global user.email "github-stats[bot]@jstrieb.github.io" # Push generated files to the generated branch git checkout generated || git checkout -b generated - git merge master + git merge master || true - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2 tag with: @@ -38,7 +38,7 @@ jobs: - name: Generate images run: | - ./zig-out/bin/github_stats + ./zig-out/bin/github-stats env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} EXCLUDE_REPOS: ${{ secrets.EXCLUDE_REPOS }} From 2baa868a9f3ae0a8212f4141d496bb3d9b820f39 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 18 Apr 2026 23:56:59 -0400 Subject: [PATCH 278/303] Rename GITHUB_TOKEN to ACCESS_TOKEN --- .github/workflows/main.yml | 2 +- .github/workflows/release.yml | 2 +- README.md | 4 ++-- src/main.zig | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 033f9b2bf9c..4d5dbf6fa6c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,7 +40,7 @@ jobs: run: | ./zig-out/bin/github-stats env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} EXCLUDE_REPOS: ${{ secrets.EXCLUDE_REPOS }} EXCLUDE_LANGS: ${{ secrets.EXCLUDE_LANGS }} EXCLUDE_PRIVATE: "false" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 11c3baa5465..1cbfe0ebe24 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: - name: Upload Release Artifacts env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} TAG: ${{ github.ref_name }} run: | ( diff --git a/README.md b/README.md index 55017736703..8f3d9252f52 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ and retrieve the images. left of the page, then click "Create a new repository." - Note: this is **not** the same as forking a copy because it copies everything fresh, without the huge commit history. -1. Create a new repository secret named `GITHUB_TOKEN` with your personal +1. Create a new repository secret named `ACCESS_TOKEN` with your personal access token from the first step. 1. [Go to the "New secret" page for your copy of this repository by clicking this link.](../../settings/secrets/actions/new) @@ -126,7 +126,7 @@ and retrieve the images. menu that drops down. 1. Click the green "New repository secret" button on the "Actions secrets and variables" page. - 1. Name your secret `GITHUB_TOKEN`. + 1. Name your secret `ACCESS_TOKEN`. 1. Paste your personal access token from step 1 into the large "Secret" text box. 1. (Optional) Make other secrets for more configuration. diff --git a/src/main.zig b/src/main.zig index 3e1904ef7bf..9ba33bb0784 100644 --- a/src/main.zig +++ b/src/main.zig @@ -36,7 +36,7 @@ const embedded_overview_template = @embedFile("templates/overview.svg"); const embedded_languages_template = @embedFile("templates/languages.svg"); const Args = struct { - github_token: ?[]const u8 = null, + access_token: ?[]const u8 = null, json_input_file: ?[]const u8 = null, json_output_file: ?[]const u8 = null, silent: bool = false, @@ -59,7 +59,7 @@ const Args = struct { pub fn init(allocator: std.mem.Allocator) !Self { return try argparse.parse(allocator, Self, struct { fn errorCheck(a: Self, stderr: *std.Io.Writer) !bool { - if ((a.github_token == null or a.github_token.?.len == 0) and + if ((a.access_token == null or a.access_token.?.len == 0) and a.json_input_file == null and !a.version) { try stderr.print( @@ -219,9 +219,9 @@ pub fn main() !void { const data = try readFile(allocator, path); defer allocator.free(data); break :stats try Statistics.initFromJson(allocator, data); - } else if (args.github_token) |github_token| stats: { + } else if (args.access_token) |access_token| stats: { std.log.info("Collecting statistics from GitHub API", .{}); - var client: HttpClient = try .init(allocator, github_token); + var client: HttpClient = try .init(allocator, access_token); defer client.deinit(); break :stats try Statistics.init( &client, From 47c33ff132c62a8b72ab03a5132b8420f7aeb660 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 19 Apr 2026 00:02:01 -0400 Subject: [PATCH 279/303] Use fewer retries by default --- src/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 9ba33bb0784..84403c5cce5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -49,7 +49,7 @@ const Args = struct { languages_output_file: ?[]const u8 = null, overview_template: ?[]const u8 = null, languages_template: ?[]const u8 = null, - max_retries: ?usize = 60, + max_retries: ?usize = 25, version: bool = false, dump_overview_template: ?[]const u8 = null, dump_languages_template: ?[]const u8 = null, From 9d9af5f6a60656428aea3a24aea010292fb9fca1 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 19 Apr 2026 00:14:17 -0400 Subject: [PATCH 280/303] Skip collecting lines_changed when forbidden --- src/statistics.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/statistics.zig b/src/statistics.zig index bea380a6daf..ca2f7088631 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -585,7 +585,7 @@ fn getLinesChanged( std.Thread.sleep(delay * std.time.ns_per_s); } switch (try item.repo.getLinesChanged(arena, client, self.user)) { - .ok => {}, + .ok, .forbidden => {}, .accepted => { item.timestamp = std.time.timestamp() + item.delay; // Note: this actually works way better with a very short delay, From 2355fd28880dc5609e05730c775b2b0537265e03 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 19 Apr 2026 00:20:34 -0400 Subject: [PATCH 281/303] Fix push in workflow --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4d5dbf6fa6c..cbeed0cd41d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -51,4 +51,4 @@ jobs: git add . # Force the build to succeed, even if no files were changed git commit -m 'Update generated files' || true - git push + git push --set-upstream origin generated From 02a2646b272f1b09b5be8332c4044a9a40be53eb Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 19 Apr 2026 00:23:14 -0400 Subject: [PATCH 282/303] Clone locally on forbidden --- src/statistics.zig | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/statistics.zig b/src/statistics.zig index ca2f7088631..7bdb9a447b2 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -585,8 +585,11 @@ fn getLinesChanged( std.Thread.sleep(delay * std.time.ns_per_s); } switch (try item.repo.getLinesChanged(arena, client, self.user)) { - .ok, .forbidden => {}, - .accepted => { + .ok => {}, + // If we're hitting rate limits on this API, just clone the repo + // locally to compute lines changed + // https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api?apiVersion=2026-03-10#rate-limit-errors + .accepted, .forbidden, .too_many_requests => { item.timestamp = std.time.timestamp() + item.delay; // Note: this actually works way better with a very short delay, // hence no exponential backoff From 31acb8819d15191618ce91373b3fb585521fac7a Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 19 Apr 2026 00:39:46 -0400 Subject: [PATCH 283/303] Actaully use the existing generated branch --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cbeed0cd41d..dee2b937992 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,6 +25,7 @@ jobs: git config --global user.name "jstrieb/github-stats" git config --global user.email "github-stats[bot]@jstrieb.github.io" # Push generated files to the generated branch + git pull git checkout generated || git checkout -b generated git merge master || true From 68bed589f0a00b669d3943993e9b47766a1a5a5a Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 19 Apr 2026 01:30:17 -0400 Subject: [PATCH 284/303] Set version number to 2.0.0 release candidate 1 --- build.zig.zon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig.zon b/build.zig.zon index def29b95582..b17317db94e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .github_stats, - .version = "0.0.1", + .version = "2.0.0-rc.1", .fingerprint = 0x80bb05a632422e37, // Changing this has security and trust implications. .minimum_zig_version = "0.15.2", .dependencies = .{}, From 9fa7050ee5cc1e0fb317fe001f05bb3e49e45608 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 19 Apr 2026 01:41:17 -0400 Subject: [PATCH 285/303] Fix release builds --- build.zig | 3 ++- build.zig.zon | 2 +- src/git.zig | 5 ++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/build.zig b/build.zig index 1b4c3a4dbcc..5dd59a81699 100644 --- a/build.zig +++ b/build.zig @@ -64,7 +64,8 @@ pub fn build(b: *std.Build) !void { .{ .cpu_arch = .powerpc64, .os_tag = .linux }, .{ .cpu_arch = .powerpc64le, .os_tag = .freebsd }, .{ .cpu_arch = .powerpc64le, .os_tag = .linux }, - .{ .cpu_arch = .riscv32, .os_tag = .linux }, + // Fails with errors (haven't investigated) + // .{ .cpu_arch = .riscv32, .os_tag = .linux }, .{ .cpu_arch = .riscv64, .os_tag = .freebsd }, .{ .cpu_arch = .riscv64, .os_tag = .linux }, .{ .cpu_arch = .thumb, .os_tag = .windows }, diff --git a/build.zig.zon b/build.zig.zon index b17317db94e..125bc69d125 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .github_stats, - .version = "2.0.0-rc.1", + .version = "2.0.0-rc.2", .fingerprint = 0x80bb05a632422e37, // Changing this has security and trust implications. .minimum_zig_version = "0.15.2", .dependencies = .{}, diff --git a/src/git.zig b/src/git.zig index c4a743fd455..dcbda53adec 100644 --- a/src/git.zig +++ b/src/git.zig @@ -89,7 +89,10 @@ pub fn getLinesChanged( const log = try std.process.Child.run(.{ .allocator = allocator, .argv = log_args, - .max_output_bytes = 64 * 1024 * 1024 * 1024, + .max_output_bytes = @min( + 64 * 1024 * 1024 * 1024, + std.math.maxInt(usize), + ), }); switch (log.term) { .Exited => |v| if (v != 0) return error.LogFailed, From 2ad31b24578333e48b1effca529067610f17d6b5 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 19 Apr 2026 01:45:56 -0400 Subject: [PATCH 286/303] Fix max memory bug: we want 64MB max, not 64GB max --- build.zig.zon | 2 +- src/git.zig | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 125bc69d125..57d3f3514cc 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .github_stats, - .version = "2.0.0-rc.2", + .version = "2.0.0-rc.3", .fingerprint = 0x80bb05a632422e37, // Changing this has security and trust implications. .minimum_zig_version = "0.15.2", .dependencies = .{}, diff --git a/src/git.zig b/src/git.zig index dcbda53adec..28680610633 100644 --- a/src/git.zig +++ b/src/git.zig @@ -89,10 +89,7 @@ pub fn getLinesChanged( const log = try std.process.Child.run(.{ .allocator = allocator, .argv = log_args, - .max_output_bytes = @min( - 64 * 1024 * 1024 * 1024, - std.math.maxInt(usize), - ), + .max_output_bytes = 64 * 1024 * 1024, }); switch (log.term) { .Exited => |v| if (v != 0) return error.LogFailed, From 1584f67b1e4a2f2d341a078dca0cb0c5cd90b3bc Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 19 Apr 2026 01:56:01 -0400 Subject: [PATCH 287/303] Drop the word "Release" from release titles --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1cbfe0ebe24..43f481a0424 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,6 +33,6 @@ jobs: cd zig-out/bin/ gh release create \ "${TAG}" \ - --title "${TAG} Release" \ + --title "${TAG}" \ * ) From 4d09a1ed50b38a4f2f6bb9dd30b99487bd2f922c Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 19 Apr 2026 02:17:05 -0400 Subject: [PATCH 288/303] Clarify "lines changed" disclaimer --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8f3d9252f52..4ccd356f981 100644 --- a/README.md +++ b/README.md @@ -47,27 +47,27 @@ would be unable to access. The GitHub statistics API returns inaccurate results in some situations: +- Total lines of code modified may be inflated – GitHub counts changes to files + like `package-lock.json` that may impact the line count in surprising ways + - On the other hand, GitHub refuses to count lines of code for repositories + with more than 10,000 commits, so contributions to those will not be + reflected in the data at all + - [The GitHub API endpoint for computing contributor statistics no longer + works reliably](https://github.com/orgs/community/discussions/192970), so we + compute the statistics ourselves by cloning each repository locally and + tallying lines changed with the `git` CLI + - Our computed totals likely under-count relative to GitHub's, since theirs + correctly attribute authorship for contributions to pull requests with + several authors that end up squashed and merged by just one author - Repository view count statistics often seem too low, and many referring sites are not captured - If you lack permissions to access the view count for a repository, it will be tallied as zero views – this is common for external repositories where your only contribution is making a pull request -- Total lines of code modified may be inflated – GitHub counts changes to files like - `package-lock.json` that may impact the line count in surprising ways - - On the other hand, GitHub refuses to count lines of code for repositories - with more than 10,000 commits, so contributions to those will not be - reflected in the data at all - Only repositories with commit contributions are counted, so if you only open an issue on a repo, it will not show up in the statistics - Repos you created and own may not be counted if you never commit to them, or if the committer email is not connected to your GitHub account -- [The GitHub API endpoint for computing contributor statistics no longer works - reliably](https://github.com/orgs/community/discussions/192970), so we compute - the statistics ourselves by cloning each repository locally and tallying lines - changed with the `git` CLI - - Our computed totals likely under-count relative to GitHub's, since theirs - correctly attribute authorship for contributions to pull requests with - several authors that end up squashed and merged by just one author If the calculated numbers seem strange, run the CLI locally and dump JSON output to determine which repositories are skewing the statistics in unexpected ways. From 10c0b54d5e25000a369bf3b044d4a86b4308f5a1 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 19 Apr 2026 10:06:36 -0400 Subject: [PATCH 289/303] Add logging for successful local clone --- src/statistics.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/statistics.zig b/src/statistics.zig index 7bdb9a447b2..83440da8771 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -613,6 +613,12 @@ fn getLinesChanged( error.GitNotInstalled => 0, else => return e, }; + std.log.info("Got {d} line{s} changed by {s} in {s}", .{ + item.repo.lines_changed, + if (item.repo.lines_changed != 1) "s" else "", + self.user, + item.repo.name, + }); } } else { try q.add(item); From 30352abc402530f8eb4978f1401ae05771ed9ea7 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 19 Apr 2026 10:45:17 -0400 Subject: [PATCH 290/303] Fix typo --- src/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 84403c5cce5..bd6442e60d6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -63,7 +63,7 @@ const Args = struct { a.json_input_file == null and !a.version) { try stderr.print( - "You must pass either an input file or an GitHub token.\n", + "You must pass an input file or a GitHub token.\n", .{}, ); return false; From bdfb0057c9063361468f3744beeddb58ec0bd224 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 19 Apr 2026 10:45:28 -0400 Subject: [PATCH 291/303] Fewer retries until their API is fixed --- .github/workflows/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dee2b937992..2db9ad8934f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -46,6 +46,9 @@ jobs: EXCLUDE_LANGS: ${{ secrets.EXCLUDE_LANGS }} EXCLUDE_PRIVATE: "false" DEBUG: "false" + # TODO: Remove this when they get their API working again + # https://github.com/orgs/community/discussions/192970 + MAX_RETRIES: 5 - name: Commit to the repo run: | From a2231a244b2380f549c1740af35142a211445dea Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 19 Apr 2026 11:07:24 -0400 Subject: [PATCH 292/303] Disable Zig cache for release builds --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 43f481a0424..fb16bf63bf9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,6 +19,9 @@ jobs: - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2 tag with: version: 0.15.2 + # I only want to disable the build cache, but don't see a good way to do + # that besides disabling all of the caching + use-cache: false - name: Build run: | From b39d7e4ed5ca147d4ebc99957a45ec0e21c04be0 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sun, 19 Apr 2026 12:23:01 -0400 Subject: [PATCH 293/303] Add local install instructions and analysis example --- README.md | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4ccd356f981..4012bb52bd2 100644 --- a/README.md +++ b/README.md @@ -171,15 +171,23 @@ and retrieve the images. Using the `github-stats` CLI (available on the [releases](https://github.com/jstrieb/github-stats/releases/latest) page) to run locally, you can dump raw statistics data to a JSON file using the -`--json-output-file` command-line argument. Then, you can import the JSON file -into your programming language of choice and start analyzing. +`--json-output-file` command-line argument. -My preference is to use [`jq`](https://github.com/jqlang/jq) from the command -line. The command-line examples below assume the JSON file is stored in +``` bash +sudo curl --location --output '/usr/local/bin/github-stats' 'https://github.com/jstrieb/github-stats/releases/latest/download/github-stats_x86_64-linux' +sudo chmod +x /usr/local/bin/github-stats +github-stats --version + +github-stats --access-token [YOUR API KEY] --json-output-file stats.json --debug +``` + +Then, you can import the JSON file into your programming language of choice and +start analyzing. My preference is to use [`jq`](https://github.com/jqlang/jq) +from the command line. The examples below assume the JSON file is stored in `stats.json`. -### List all +### List All List all repositories, sorted with most-viewed at the bottom. @@ -193,6 +201,21 @@ removes the languages field (using `del()`) because it can clutter the output, making it hard to read. +### List Languages + +List all languages, sorted with most-used at the bottom. + +``` bash +jq --raw-output ' + [.repositories[].languages[]] + | group_by(.name) + | sort_by([.[].size] | add) + | .[] + | "\(.[0].name): \([.[].size] | add)" +' stats.json +``` + + ## Support the Project If this project is useful to you, please support it! From 68ddcc6a288d7c3af450a1abd753b3be38778d4c Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 20 Apr 2026 11:03:37 -0400 Subject: [PATCH 294/303] Tweak and elaborate on language names Closes #94. Closes #137. --- README.md | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4012bb52bd2..c964270b172 100644 --- a/README.md +++ b/README.md @@ -47,18 +47,21 @@ would be unable to access. The GitHub statistics API returns inaccurate results in some situations: -- Total lines of code modified may be inflated – GitHub counts changes to files - like `package-lock.json` that may impact the line count in surprising ways +- Total lines of code modified may be too high or too low + - GitHub counts changes to files like `package-lock.json` that may inflate the + line count in surprising ways - On the other hand, GitHub refuses to count lines of code for repositories with more than 10,000 commits, so contributions to those will not be reflected in the data at all - [The GitHub API endpoint for computing contributor statistics no longer works reliably](https://github.com/orgs/community/discussions/192970), so we - compute the statistics ourselves by cloning each repository locally and - tallying lines changed with the `git` CLI + fall back on computing the statistics ourselves by cloning each repository + locally and tallying lines changed with the `git` CLI - Our computed totals likely under-count relative to GitHub's, since theirs correctly attribute authorship for contributions to pull requests with several authors that end up squashed and merged by just one author + - They also correctly attribute commits we may miss if they are made with + old email addresses no longer connected to the account - Repository view count statistics often seem too low, and many referring sites are not captured - If you lack permissions to access the view count for a repository, it will @@ -84,8 +87,8 @@ and retrieve the images. and `repo` permissions.](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) 1. [Navigate to the personal access tokens (classic) - page.](https://github.com/settings/tokens) Open that link in a new tab, - or proceed with the steps below. + page.](https://github.com/settings/tokens) Open that link in a new tab, or + proceed with the steps below. 1. Click your avatar in the top right corner, then "Settings" on the menu that drops down. 1. Click "Developer settings" from the menu on the left. @@ -105,8 +108,8 @@ and retrieve the images. 1. **Copy the token and save it somewhere.** If you lose it, you will not be able to access it again, and will have to regenerate a new one. I keep mine saved along with the GitHub entry in my password manager. - 1. Some users report that it can take some time for the personal access - token to take effect. For more information, see + 1. Some users report that it can take some time for the personal access token + to take effect. For more information, see [#30](https://github.com/jstrieb/github-stats/issues/30). 1. Create a copy of this repository by clicking [here](https://github.com/jstrieb/github-stats/generate). @@ -114,8 +117,8 @@ and retrieve the images. left of the page, then click "Create a new repository." - Note: this is **not** the same as forking a copy because it copies everything fresh, without the huge commit history. -1. Create a new repository secret named `ACCESS_TOKEN` with your personal - access token from the first step. +1. Create a new repository secret named `ACCESS_TOKEN` with your personal access + token from the first step. 1. [Go to the "New secret" page for your copy of this repository by clicking this link.](../../settings/secrets/actions/new) - If the link doesn't work, try clicking it from your copy of this @@ -132,10 +135,17 @@ and retrieve the images. 1. (Optional) Make other secrets for more configuration. - To exclude some repositories from the aggregate statistics, add them (separated by commas) to a secret called `EXCLUDE_REPOS`. + - To prevent your copy of this repository from showing up in your + statistics, add the name of your copy of the repo to this list. - To exclude some languages from the aggregate statistics, add them (separated by commas) to a secret called `EXCLUDE_LANGS`. + - The languages are case insensitive, and can include spaces. + - Language names can be found either in a [local stats file generated by + the CLI](#list-languages), or in the [list used by GitHub + linguist](https://github.com/github-linguist/linguist/blob/537297cdae3ab05f8d5dd1c03627a5bd73707b19/lib/linguist/languages.yml) + (which powers their language analysis on the back end). - Lists for `EXCLUDE_REPOS` and `EXCLUDE_LANGS` can use globbing patterns. - For example, to exclude all repos by "@jstrieb", add `jstrieb/*` to + For example, to exclude all repos by user "jstrieb", add `jstrieb/*` to `EXCLUDE_REPOS`. - These can also be set directly in [the Actions workflow](.github/workflows/main.yml), but you should set them as secrets From 05c3e71cbe2c450ecbe37296c8980897f67d19bd Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 20 Apr 2026 18:46:28 -0400 Subject: [PATCH 295/303] Specify that the install instructions are for Linux --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c964270b172..854acd24e57 100644 --- a/README.md +++ b/README.md @@ -184,8 +184,13 @@ run locally, you can dump raw statistics data to a JSON file using the `--json-output-file` command-line argument. ``` bash -sudo curl --location --output '/usr/local/bin/github-stats' 'https://github.com/jstrieb/github-stats/releases/latest/download/github-stats_x86_64-linux' +# Instructions for Linux. Change the filename at the end of the URL for macOS. +sudo curl \ + --location \ + --output '/usr/local/bin/github-stats' \ + 'https://github.com/jstrieb/github-stats/releases/latest/download/github-stats_x86_64-linux' sudo chmod +x /usr/local/bin/github-stats + github-stats --version github-stats --access-token [YOUR API KEY] --json-output-file stats.json --debug From b755b9df269857efb9a1a504c8ee1831ffb576de Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 20 Apr 2026 18:46:41 -0400 Subject: [PATCH 296/303] Set version to 2.0.0 --- build.zig.zon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig.zon b/build.zig.zon index 57d3f3514cc..c2221037752 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .github_stats, - .version = "2.0.0-rc.3", + .version = "2.0.0", .fingerprint = 0x80bb05a632422e37, // Changing this has security and trust implications. .minimum_zig_version = "0.15.2", .dependencies = .{}, From 6d30b028939ab479fe53296d58c4e224edaa7c6d Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 20 Apr 2026 19:43:54 -0400 Subject: [PATCH 297/303] Build binary before checking out history branch --- .github/workflows/main.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2db9ad8934f..61db51bcc16 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,14 +20,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - name: Checkout history branch - run: | - git config --global user.name "jstrieb/github-stats" - git config --global user.email "github-stats[bot]@jstrieb.github.io" - # Push generated files to the generated branch - git pull - git checkout generated || git checkout -b generated - git merge master || true - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2 tag with: @@ -37,6 +29,14 @@ jobs: run: | zig build --release + - name: Checkout history branch + run: | + git config --global user.name "jstrieb/github-stats" + git config --global user.email "github-stats[bot]@jstrieb.github.io" + # Push generated files to the generated branch + git pull + git checkout generated || git checkout -b generated + - name: Generate images run: | ./zig-out/bin/github-stats From a015d4f88a699a8d3b3973948d3e58b1ab794340 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 25 Apr 2026 12:43:44 -0400 Subject: [PATCH 298/303] Port to Zig 0.16.0 --- .github/workflows/main.yml | 2 +- .github/workflows/release.yml | 2 +- build.zig | 4 +- build.zig.zon | 2 +- src/argparse.zig | 17 ++++---- src/git.zig | 32 ++++++--------- src/http_client.zig | 14 ++++--- src/main.zig | 76 ++++++++++++++++++++--------------- src/statistics.zig | 34 +++++++++------- 9 files changed, 99 insertions(+), 84 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 61db51bcc16..525f981a772 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2 tag with: - version: 0.15.2 + version: 0.16.0 - name: Build run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fb16bf63bf9..71f37e63791 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2 tag with: - version: 0.15.2 + version: 0.16.0 # I only want to disable the build cache, but don't see a good way to do # that besides disabling all of the caching use-cache: false diff --git a/build.zig b/build.zig index 5dd59a81699..cdcc952c4fc 100644 --- a/build.zig +++ b/build.zig @@ -7,11 +7,11 @@ pub fn build(b: *std.Build) !void { .preferred_optimize_mode = .ReleaseSafe, }); var version: []const u8 = @import("build.zig.zon").version; - if (git.isInstalled(b.allocator)) { + if (git.isInstalled(b.allocator, b.graph.io)) { version = try std.fmt.allocPrint( b.allocator, "{s} ({s})", - .{ version, try git.currentCommit(b.allocator) }, + .{ version, try git.currentCommit(b.allocator, b.graph.io) }, ); } const options = b.addOptions(); diff --git a/build.zig.zon b/build.zig.zon index c2221037752..2e2ab971681 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,7 +2,7 @@ .name = .github_stats, .version = "2.0.0", .fingerprint = 0x80bb05a632422e37, // Changing this has security and trust implications. - .minimum_zig_version = "0.15.2", + .minimum_zig_version = "0.16.0", .dependencies = .{}, .paths = .{ "build.zig", diff --git a/src/argparse.zig b/src/argparse.zig index 3b8bfa97a37..aadc7a81f02 100644 --- a/src/argparse.zig +++ b/src/argparse.zig @@ -6,18 +6,22 @@ var stdout: *std.Io.Writer = undefined; var stderr: *std.Io.Writer = undefined; pub fn parse( - allocator: std.mem.Allocator, + init: std.process.Init, T: type, errorCheck: ?fn (args: T, stderr: *std.Io.Writer) anyerror!bool, ) !T { - var stdout_writer = std.fs.File.stdout().writer(&.{}); + const allocator = init.gpa; + const io = init.io; + + var stdout_writer = std.Io.File.stdout().writer(io, &.{}); stdout = &stdout_writer.interface; - var stderr_writer = std.fs.File.stderr().writer(&.{}); + var stderr_writer = std.Io.File.stderr().writer(io, &.{}); stderr = &stderr_writer.interface; var arena: std.heap.ArenaAllocator = .init(allocator); defer arena.deinit(); const a = arena.allocator(); + const args = try init.minimal.args.toSlice(a); const fields = @typeInfo(T).@"struct".fields; var seen = [_]bool{false} ** fields.len; @@ -30,10 +34,8 @@ pub fn parse( } } - const args = try std.process.argsAlloc(a); - defer std.process.argsFree(a, args); try setFromCli(T, allocator, &arena, args, &seen, &result); - try setFromEnv(T, allocator, &arena, &seen, &result); + try setFromEnv(T, allocator, &arena, &seen, &result, init.environ_map); try setFromDefaults(T, allocator, &seen, &result); inline for (fields, seen) |field, seen_field| { @@ -139,10 +141,9 @@ fn setFromEnv( arena: *std.heap.ArenaAllocator, seen: []bool, result: *T, + env: *std.process.Environ.Map, ) !void { const a = arena.allocator(); - var env = try std.process.getEnvMap(a); - defer env.deinit(); var iterator = env.iterator(); while (iterator.next()) |entry| { const key = try a.dupe(u8, entry.key_ptr.*); diff --git a/src/git.zig b/src/git.zig index 28680610633..84a6d5b022b 100644 --- a/src/git.zig +++ b/src/git.zig @@ -2,32 +2,30 @@ const std = @import("std"); var is_installed: ?bool = null; -pub fn isInstalled(gpa: std.mem.Allocator) bool { +pub fn isInstalled(gpa: std.mem.Allocator, io: std.Io) bool { if (is_installed) |v| { return v; } var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); - const run = std.process.Child.run(.{ - .allocator = arena.allocator(), + const run = std.process.run(arena.allocator(), io, .{ .argv = &.{ "git", "--version" }, }) catch { is_installed = false; return is_installed.?; }; is_installed = switch (run.term) { - .Exited => |v| v == 0, + .exited => |v| v == 0, else => false, }; return is_installed.?; } -pub fn currentCommit(gpa: std.mem.Allocator) ![]const u8 { - if (!isInstalled(gpa)) return error.GitNotInstalled; +pub fn currentCommit(gpa: std.mem.Allocator, io: std.Io) ![]const u8 { + if (!isInstalled(gpa, io)) return error.GitNotInstalled; var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); - const run = try std.process.Child.run(.{ - .allocator = arena.allocator(), + const run = try std.process.run(arena.allocator(), io, .{ .argv = &.{ "git", "rev-parse", "HEAD" }, }); return try gpa.dupe(u8, run.stdout[0..8]); @@ -35,12 +33,13 @@ pub fn currentCommit(gpa: std.mem.Allocator) ![]const u8 { pub fn getLinesChanged( gpa: std.mem.Allocator, + io: std.Io, login: []const u8, token: []const u8, repo: []const u8, emails: []const []const u8, ) !u32 { - if (!isInstalled(gpa)) return error.GitNotInstalled; + if (!isInstalled(gpa, io)) return error.GitNotInstalled; var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const allocator = arena.allocator(); @@ -51,8 +50,7 @@ pub fn getLinesChanged( "https://{s}:{s}@github.com/{s}.git", .{ login, token, repo }, ); - const clone = try std.process.Child.run(.{ - .allocator = allocator, + const clone = try std.process.run(allocator, io, .{ .argv = &.{ "git", "clone", @@ -65,10 +63,10 @@ pub fn getLinesChanged( }, }); switch (clone.term) { - .Exited => |v| if (v != 0) return error.CloneFailed, + .exited => |v| if (v != 0) return error.CloneFailed, else => return error.CloneFailed, } - defer std.fs.cwd().deleteTree(repo_path) catch {}; + defer std.Io.Dir.cwd().deleteTree(io, repo_path) catch {}; const email_args = try allocator.alloc([]const u8, emails.len * 2); for (emails, 0..) |email, i| { @@ -86,13 +84,9 @@ pub fn getLinesChanged( }, email_args, }); - const log = try std.process.Child.run(.{ - .allocator = allocator, - .argv = log_args, - .max_output_bytes = 64 * 1024 * 1024, - }); + const log = try std.process.run(allocator, io, .{ .argv = log_args }); switch (log.term) { - .Exited => |v| if (v != 0) return error.LogFailed, + .exited => |v| if (v != 0) return error.LogFailed, else => return error.LogFailed, } diff --git a/src/http_client.zig b/src/http_client.zig index 1bc5ca4f708..35a2d8c9bdc 100644 --- a/src/http_client.zig +++ b/src/http_client.zig @@ -5,6 +5,7 @@ const std = @import("std"); allocator: std.mem.Allocator, +io: std.Io, client: std.http.Client, bearer: []const u8, token: []const u8, @@ -21,14 +22,15 @@ const Request = struct { extra_headers: []const std.http.Header = &.{}, }; -pub fn init(allocator: std.mem.Allocator, token: []const u8) !Self { +pub fn init(allocator: std.mem.Allocator, io: std.Io, token: []const u8) !Self { const bearer = try std.fmt.allocPrint(allocator, "Bearer {s}", .{token}); errdefer allocator.free(bearer); const cloned_token = try allocator.dupe(u8, token); errdefer allocator.free(cloned_token); return .{ .allocator = allocator, - .client = .{ .allocator = allocator }, + .io = io, + .client = .{ .allocator = allocator, .io = io }, .bearer = bearer, .token = cloned_token, }; @@ -49,7 +51,7 @@ pub fn fetch(self: *Self, request: Request, retries: isize) !Response { try std.Io.Writer.Allocating.initCapacity(self.allocator, 1024); var writer_initialized = true; errdefer if (writer_initialized) writer.deinit(); - const status = (try (self.client.fetch(.{ + const status = (self.client.fetch(.{ .location = .{ .url = request.url }, .response_writer = &writer.writer, .payload = request.body, @@ -67,13 +69,13 @@ pub fn fetch(self: *Self, request: Request, retries: isize) !Response { .{}, ); self.client.deinit(); - self.client = .{ .allocator = self.allocator }; + self.client = .{ .allocator = self.allocator, .io = self.io }; writer.deinit(); writer_initialized = false; return self.fetch(request, retries - 1); }, - else => err, - })).status; + else => return err, + }).status; return .{ .body = try writer.toOwnedSlice(), .status = status, diff --git a/src/main.zig b/src/main.zig index bd6442e60d6..882fdf0b150 100644 --- a/src/main.zig +++ b/src/main.zig @@ -56,8 +56,8 @@ const Args = struct { const Self = @This(); - pub fn init(allocator: std.mem.Allocator) !Self { - return try argparse.parse(allocator, Self, struct { + pub fn init(main_init: std.process.Init) !Self { + return try argparse.parse(main_init, Self, struct { fn errorCheck(a: Self, stderr: *std.Io.Writer) !bool { if ((a.access_token == null or a.access_token.?.len == 0) and a.json_input_file == null and !a.version) @@ -165,12 +165,11 @@ fn languages( ); } -pub fn main() !void { - var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); +pub fn main(init: std.process.Init) !void { + const allocator = init.gpa; + const io = init.io; - const args = try Args.init(allocator); + const args = try Args.init(init); defer args.deinit(allocator); if (args.silent) { log_level = .err; @@ -181,8 +180,8 @@ pub fn main() !void { } if (args.version) { - const stdout = std.fs.File.stdout(); - var writer = stdout.writer(&.{}); + const stdout = std.Io.File.stdout(); + var writer = stdout.writer(io, &.{}); try writer.interface.print( \\GitHub Stats version {s} \\https://github.com/jstrieb/github-stats @@ -193,12 +192,12 @@ pub fn main() !void { } if (args.dump_overview_template) |path| { - try writeFile(path, embedded_overview_template); + try writeFile(io, path, embedded_overview_template); return; } if (args.dump_languages_template) |path| { - try writeFile(path, embedded_languages_template); + try writeFile(io, path, embedded_languages_template); return; } @@ -216,16 +215,17 @@ pub fn main() !void { defer if (exclude_langs) |exclude| allocator.free(exclude); var stats: Statistics = if (args.json_input_file) |path| stats: { - const data = try readFile(allocator, path); + const data = try readFile(allocator, io, path); defer allocator.free(data); break :stats try Statistics.initFromJson(allocator, data); } else if (args.access_token) |access_token| stats: { std.log.info("Collecting statistics from GitHub API", .{}); - var client: HttpClient = try .init(allocator, access_token); + var client: HttpClient = try .init(allocator, io, access_token); defer client.deinit(); break :stats try Statistics.init( &client, allocator, + io, args.max_retries, ); } else unreachable; @@ -235,6 +235,7 @@ pub fn main() !void { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); try writeFile( + io, path, try std.json.Stringify.valueAlloc( arena.allocator(), @@ -245,8 +246,8 @@ pub fn main() !void { } var aggregate_stats: struct { - languages: std.StringArrayHashMap(u64), - language_colors: std.StringArrayHashMap([]const u8), + languages: std.array_hash_map.String(u64), + language_colors: std.array_hash_map.String([]const u8), contributions: usize, name: []const u8, languages_total: usize = 0, @@ -261,12 +262,12 @@ pub fn main() !void { stats.commit_contributions + stats.pr_contributions + stats.review_contributions, - .languages = .init(allocator), - .language_colors = .init(allocator), + .languages = try .init(allocator, &.{}, &.{}), + .language_colors = try .init(allocator, &.{}, &.{}), .name = stats.name, }; - defer aggregate_stats.languages.deinit(); - defer aggregate_stats.language_colors.deinit(); + defer aggregate_stats.languages.deinit(allocator); + defer aggregate_stats.language_colors.deinit(allocator); for (stats.repositories) |repository| { if (glob.matchAny(exclude_repos orelse &.{}, repository.name) or (args.exclude_private and repository.private)) @@ -283,11 +284,15 @@ pub fn main() !void { continue; } if (language.color) |color| { - try aggregate_stats.language_colors.put(language.name, color); + try aggregate_stats.language_colors.put( + allocator, + language.name, + color, + ); } var total = aggregate_stats.languages.get(language.name) orelse 0; total += language.size; - try aggregate_stats.languages.put(language.name, total); + try aggregate_stats.languages.put(allocator, language.name, total); aggregate_stats.languages_total += language.size; }; } @@ -304,24 +309,26 @@ pub fn main() !void { defer arena.deinit(); try writeFile( + io, args.overview_output_file orelse "overview.svg", try overview( &arena, aggregate_stats, if (args.overview_template) |template| - try readFile(arena.allocator(), template) + try readFile(arena.allocator(), io, template) else embedded_overview_template, ), ); try writeFile( + io, args.languages_output_file orelse "languages.svg", try languages( &arena, aggregate_stats, if (args.languages_template) |template| - try readFile(arena.allocator(), template) + try readFile(arena.allocator(), io, template) else embedded_languages_template, ), @@ -333,32 +340,37 @@ test { std.testing.refAllDecls(@This()); } -fn readFile(allocator: std.mem.Allocator, path: []const u8) ![]const u8 { +fn readFile( + allocator: std.mem.Allocator, + io: std.Io, + path: []const u8, +) ![]const u8 { std.log.info("Reading data from '{s}'", .{path}); const in = if (std.mem.eql(u8, path, "-")) - std.fs.File.stdin() + std.Io.File.stdin() else - try std.fs.cwd().openFile(path, .{}); - defer if (!std.mem.eql(u8, path, "-")) in.close(); + try std.Io.Dir.cwd().openFile(io, path, .{}); + defer if (!std.mem.eql(u8, path, "-")) in.close(io); var read_buffer: [64 * 1024]u8 = undefined; - var reader = in.reader(&read_buffer); + var reader = in.reader(io, &read_buffer); return try (&reader.interface).allocRemaining(allocator, .unlimited); } fn writeFile( + io: std.Io, path: []const u8, data: []const u8, ) !void { std.log.info("Writing data to '{s}'", .{path}); const out = if (std.mem.eql(u8, path, "-")) - std.fs.File.stdout() + std.Io.File.stdout() else - try std.fs.cwd().createFile(path, .{}); - defer if (!std.mem.eql(u8, path, "-")) out.close(); + try std.Io.Dir.cwd().createFile(io, path, .{}); + defer if (!std.mem.eql(u8, path, "-")) out.close(io); var write_buffer: [64 * 1024]u8 = undefined; - var writer = out.writer(&write_buffer); + var writer = out.writer(io, &write_buffer); try writer.interface.writeAll(data); try writer.interface.flush(); } diff --git a/src/statistics.zig b/src/statistics.zig index 83440da8771..4e434055447 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -106,6 +106,7 @@ const Language = struct { pub fn init( client: *HttpClient, allocator: std.mem.Allocator, + io: std.Io, max_retries: ?usize, ) !Statistics { var arena = std.heap.ArenaAllocator.init(allocator); @@ -113,7 +114,7 @@ pub fn init( var self: Statistics = try getRepos(allocator, &arena, client); errdefer self.deinit(allocator); - try self.getLinesChanged(&arena, client, max_retries); + try self.getLinesChanged(&arena, io, client, max_retries); return self; } @@ -546,9 +547,11 @@ fn getRepos( fn getLinesChanged( self: *Statistics, arena: *std.heap.ArenaAllocator, + io: std.Io, client: *HttpClient, max_retries: ?usize, ) !void { + const allocator = arena.allocator(); const T = struct { repo: *Repository, delay: i64, @@ -559,30 +562,30 @@ fn getLinesChanged( pub fn compareFn(_: void, lhs: T, rhs: T) std.math.Order { return std.math.order(lhs.timestamp, rhs.timestamp); } - }.compareFn) = .init(arena.allocator(), {}); - defer q.deinit(); + }.compareFn) = .empty; + defer q.deinit(allocator); for (self.repositories) |*repo| { if (repo.lines_changed > 0) { continue; } - try q.add(.{ + try q.push(allocator, .{ .repo = repo, .delay = 0, - .timestamp = std.time.timestamp(), + .timestamp = std.Io.Clock.real.now(io).toSeconds(), .retries = 0, }); } - while (q.count() > 0) { - var item = q.remove(); - const now = std.time.timestamp(); + while (q.pop()) |_item| { + var item = _item; + const now = std.Io.Clock.real.now(io).toSeconds(); if (item.timestamp > now) { - const delay: u64 = @intCast(item.timestamp - now); + const delay = item.timestamp - now; std.log.debug("Sleeping for {d}s. Waiting for {d} repo{s}.", .{ delay, q.count() + 1, if (q.count() + 1 != 0) "s" else "", }); - std.Thread.sleep(delay * std.time.ns_per_s); + try io.sleep(.fromSeconds(delay), .real); } switch (try item.repo.getLinesChanged(arena, client, self.user)) { .ok => {}, @@ -590,14 +593,16 @@ fn getLinesChanged( // locally to compute lines changed // https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api?apiVersion=2026-03-10#rate-limit-errors .accepted, .forbidden, .too_many_requests => { - item.timestamp = std.time.timestamp() + item.delay; + item.timestamp = + std.Io.Clock.real.now(io).toSeconds() + item.delay; // Note: this actually works way better with a very short delay, // hence no exponential backoff - item.delay = std.crypto.random.intRangeAtMost(i64, 0, 4); + const random: std.Random.IoSource = .{ .io = io }; + item.delay = random.interface().intRangeAtMost(i64, 0, 4); item.retries += 1; if (max_retries) |max| { if (item.retries <= max) { - try q.add(item); + try q.push(allocator, item); } else { std.log.info( "Cloning {s} to get lines changed...", @@ -605,6 +610,7 @@ fn getLinesChanged( ); item.repo.lines_changed = git.getLinesChanged( arena.allocator(), + io, self.user, client.token, item.repo.name, @@ -621,7 +627,7 @@ fn getLinesChanged( }); } } else { - try q.add(item); + try q.push(allocator, item); } }, else => |status| { From 9cc7dfa9ced51d8f069df95064a190342a04c451 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 25 Apr 2026 14:44:47 -0400 Subject: [PATCH 299/303] Build for new compiler targets added in Zig 0.16.0 --- build.zig | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/build.zig b/build.zig index cdcc952c4fc..3fb9ec33a04 100644 --- a/build.zig +++ b/build.zig @@ -45,37 +45,60 @@ pub fn build(b: *std.Build) !void { const release_targets: []const std.Target.Query = &.{ // Zig tier 1 supported compiler targets (manually tested) .{ .cpu_arch = .x86_64, .os_tag = .linux }, - .{ .cpu_arch = .x86_64, .os_tag = .macos }, // Zig tier 2 supported compiler targets (manually tested) + .{ .cpu_arch = .x86_64, .os_tag = .macos }, .{ .cpu_arch = .aarch64, .os_tag = .macos }, .{ .cpu_arch = .x86_64, .os_tag = .windows }, // Zig tier 2 supported compiler targets (untested) .{ .cpu_arch = .aarch64, .os_tag = .freebsd }, .{ .cpu_arch = .aarch64, .os_tag = .linux }, + .{ .cpu_arch = .aarch64_be, .os_tag = .linux }, + .{ .cpu_arch = .aarch64, .os_tag = .maccatalyst }, .{ .cpu_arch = .aarch64, .os_tag = .netbsd }, + .{ .cpu_arch = .aarch64_be, .os_tag = .netbsd }, + .{ .cpu_arch = .aarch64, .os_tag = .openbsd }, .{ .cpu_arch = .aarch64, .os_tag = .windows }, .{ .cpu_arch = .arm, .os_tag = .freebsd }, .{ .cpu_arch = .arm, .os_tag = .linux }, + .{ .cpu_arch = .armeb, .os_tag = .linux }, .{ .cpu_arch = .arm, .os_tag = .netbsd }, + .{ .cpu_arch = .armeb, .os_tag = .netbsd }, + .{ .cpu_arch = .arm, .os_tag = .openbsd }, + .{ .cpu_arch = .hexagon, .os_tag = .linux }, .{ .cpu_arch = .loongarch64, .os_tag = .linux }, + .{ .cpu_arch = .mips, .os_tag = .linux }, + .{ .cpu_arch = .mipsel, .os_tag = .linux }, + .{ .cpu_arch = .mips, .os_tag = .netbsd }, + .{ .cpu_arch = .mipsel, .os_tag = .netbsd }, + .{ .cpu_arch = .mips64, .os_tag = .linux }, + .{ .cpu_arch = .mips64el, .os_tag = .linux }, + .{ .cpu_arch = .mips64, .os_tag = .openbsd }, + .{ .cpu_arch = .mips64el, .os_tag = .openbsd }, .{ .cpu_arch = .powerpc, .os_tag = .linux }, .{ .cpu_arch = .powerpc, .os_tag = .netbsd }, + .{ .cpu_arch = .powerpc, .os_tag = .openbsd }, .{ .cpu_arch = .powerpc64, .os_tag = .freebsd }, - .{ .cpu_arch = .powerpc64, .os_tag = .linux }, .{ .cpu_arch = .powerpc64le, .os_tag = .freebsd }, + .{ .cpu_arch = .powerpc64, .os_tag = .linux }, .{ .cpu_arch = .powerpc64le, .os_tag = .linux }, - // Fails with errors (haven't investigated) - // .{ .cpu_arch = .riscv32, .os_tag = .linux }, + .{ .cpu_arch = .powerpc64, .os_tag = .openbsd }, + .{ .cpu_arch = .riscv32, .os_tag = .linux }, .{ .cpu_arch = .riscv64, .os_tag = .freebsd }, .{ .cpu_arch = .riscv64, .os_tag = .linux }, - .{ .cpu_arch = .thumb, .os_tag = .windows }, + .{ .cpu_arch = .riscv64, .os_tag = .openbsd }, + .{ .cpu_arch = .s390x, .os_tag = .linux }, .{ .cpu_arch = .thumb, .os_tag = .linux }, - // Fails with error due to networking - // .{ .cpu_arch = .wasm32, .os_tag = .wasi }, + .{ .cpu_arch = .thumbeb, .os_tag = .linux }, + .{ .cpu_arch = .wasm32, .os_tag = .wasi }, + .{ .cpu_arch = .thumb, .os_tag = .windows }, .{ .cpu_arch = .x86, .os_tag = .linux }, + .{ .cpu_arch = .x86, .os_tag = .netbsd }, + .{ .cpu_arch = .x86, .os_tag = .openbsd }, .{ .cpu_arch = .x86, .os_tag = .windows }, .{ .cpu_arch = .x86_64, .os_tag = .freebsd }, + .{ .cpu_arch = .x86_64, .os_tag = .maccatalyst }, .{ .cpu_arch = .x86_64, .os_tag = .netbsd }, + .{ .cpu_arch = .x86_64, .os_tag = .openbsd }, }; for (release_targets) |t| { const cross_exe = b.addExecutable(.{ From 1f5843f9a57c11b4e7d15a431b45f20c897e1491 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Sat, 25 Apr 2026 14:56:19 -0400 Subject: [PATCH 300/303] Bump minor version --- build.zig.zon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig.zon b/build.zig.zon index 2e2ab971681..93eaa070bd2 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .github_stats, - .version = "2.0.0", + .version = "2.0.1", .fingerprint = 0x80bb05a632422e37, // Changing this has security and trust implications. .minimum_zig_version = "0.16.0", .dependencies = .{}, From 7889e9e3969dce1d5b6ce99272f3c8f639fb3316 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 27 Apr 2026 00:57:18 -0400 Subject: [PATCH 301/303] Fix double free in error case Related to #153. --- src/statistics.zig | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/statistics.zig b/src/statistics.zig index 4e434055447..460f2115445 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -240,7 +240,7 @@ fn getReposByYear( "Getting {d} month{s} of data starting from {d}/{d}...", .{ months, if (months != 1) "s" else "", start_month + 1, year }, ); - var response = try context.client.graphql( + const response = try context.client.graphql( \\query ($from: DateTime, $to: DateTime) { \\ viewer { \\ contributionsCollection(from: $from, to: $to) { @@ -289,7 +289,7 @@ fn getReposByYear( ), }, ); - errdefer context.client.allocator.free(response.body); + defer context.client.allocator.free(response.body); if (response.status != .ok) { std.log.err( "Failed to get data from {d} ({?s})", @@ -328,7 +328,6 @@ fn getReposByYear( response.body, .{ .ignore_unknown_fields = true, .allocate = .alloc_always }, )).data.viewer.contributionsCollection; - context.client.allocator.free(response.body); std.log.info( "Parsed {d} total repositories from {d}", .{ stats.commitContributionsByRepository.len, year }, @@ -426,7 +425,7 @@ fn getReposByYear( "Getting views for {s}...", .{raw_repo.nameWithOwner}, ); - response = try context.client.rest( + const response2 = try context.client.rest( try std.mem.concat( context.arena.allocator(), u8, @@ -437,18 +436,18 @@ fn getReposByYear( }, ), ); - defer context.client.allocator.free(response.body); - if (response.status == .ok) { + defer context.client.allocator.free(response2.body); + if (response2.status == .ok) { repository.views = (try std.json.parseFromSliceLeaky( struct { count: u32 }, context.arena.allocator(), - response.body, + response2.body, .{ .ignore_unknown_fields = true }, )).count; } else { std.log.info( "Failed to get views for {s} ({?s})", - .{ raw_repo.nameWithOwner, response.status.phrase() }, + .{ raw_repo.nameWithOwner, response2.status.phrase() }, ); } From b974865c04fc2ce263e7cdeb153e66210e16d1b4 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 27 Apr 2026 01:08:12 -0400 Subject: [PATCH 302/303] Ignore parse errors on broken GitHub API endpoint Related to #153. --- src/statistics.zig | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/statistics.zig b/src/statistics.zig index 460f2115445..25e32545bda 100644 --- a/src/statistics.zig +++ b/src/statistics.zig @@ -56,7 +56,8 @@ const Repository = struct { ); defer client.allocator.free(response.body); if (response.status == .ok) { - const authors = (try std.json.parseFromSliceLeaky( + self.lines_changed = 0; + const authors = std.json.parseFromSliceLeaky( []struct { author: struct { login: []const u8 }, weeks: []struct { @@ -67,8 +68,16 @@ const Repository = struct { arena.allocator(), response.body, .{ .ignore_unknown_fields = true }, - )); - self.lines_changed = 0; + ) catch { + // TODO: Replace with proper exception propagation when GitHub + // gets their shit together and stops breaking this endpoint + std.log.info( + "Skipping lines changed by {s} in {s} due to invalid " ++ + "response from GitHub.", + .{ user, self.name }, + ); + return response.status; + }; for (authors) |o| { if (!std.mem.eql(u8, o.author.login, user)) { continue; From ef574fae2ce8311f3e1ebb43e07c0c0fae0a41b6 Mon Sep 17 00:00:00 2001 From: Jacob Strieb Date: Mon, 27 Apr 2026 23:28:25 -0400 Subject: [PATCH 303/303] Build debug builds by default When users have issues, now there will be full stack traces --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 525f981a772..cb8b7be5964 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,7 +27,7 @@ jobs: - name: Build run: | - zig build --release + zig build - name: Checkout history branch run: | @@ -45,7 +45,7 @@ jobs: EXCLUDE_REPOS: ${{ secrets.EXCLUDE_REPOS }} EXCLUDE_LANGS: ${{ secrets.EXCLUDE_LANGS }} EXCLUDE_PRIVATE: "false" - DEBUG: "false" + SILENT: "true" # TODO: Remove this when they get their API working again # https://github.com/orgs/community/discussions/192970 MAX_RETRIES: 5