feat: let commits-per-repo 0 mean every commit

The pagination guard broke on seen >= maxPerRepo, so a cap of 0 stopped
before the first page and rendered empty productive-time,
productive-weekday and most-commit-language cards. That inverted the
meaning 0 carries for top-repos, where it already means unlimited.

Treat a cap of 0 or less as no cap. Verified against a real repo: a cap
of 100 still stops at 100, while 500 and 0 both walk the full 382-commit
history.
This commit is contained in:
2026-08-13 14:19:41 +07:00
parent 9e0387f307
commit 7c081ac591
6 changed files with 47 additions and 8 deletions
+2 -2
View File
@@ -122,7 +122,7 @@ Then embed the cards in your `README.md`:
| `tz` | `UTC` | IANA tz for the productive-time card (e.g. `Asia/Saigon`) |
| `start_of_week` | `sunday` | First day of week for heatmap rows and weekday bars (`sunday``saturday`) |
| `top_repos` | `0` | Optional cap on seed repos probed for commit history (`0` = unlimited) |
| `commits_per_repo` | `500` | Max commits sampled per repo (covers last-year and all-time aggregates) |
| `commits_per_repo` | `500` | Max commits sampled per repo, `0` = every commit (covers last-year and all-time aggregates) |
| `include_forks` | `true` | Include forked repos in stats and commit probing |
| `include_private` | `true` | Include private repos (requires PAT with `repo` scope; silently no-op otherwise) |
| `include_org_repos`| `false` | Count org-owned repos you administer toward stars, repo count, languages, top-starred (needs `read:org`) |
@@ -168,7 +168,7 @@ ghstats -user tiennm99 -themes dracula -include-org-repos -out output
| `-tz` | `Local` | IANA timezone for productive-time cards |
| `-start-of-week` | `sunday` | First day of week for heatmap rows and weekday bars (`sunday``saturday`) |
| `-top-repos` | `0` | Optional cap on seed repos probed (`0` = unlimited) |
| `-commits-per-repo` | `500` | Max commits sampled per repo |
| `-commits-per-repo` | `500` | Max commits sampled per repo, `0` = every commit |
| `-include-forks` | `true` | Include forked repos in the stats |
| `-include-private` | `true` | Include private repos (requires `repo` PAT scope; silently no-op otherwise) |
| `-include-org-repos`| `false` | Count org-owned repos you administer toward stars, repo count, languages, top-starred |
+1 -1
View File
@@ -36,7 +36,7 @@ inputs:
required: false
default: "0"
commits_per_repo:
description: Max commits sampled per repo (covers last-year and all-time aggregates)
description: Max commits sampled per repo, `0` for every commit (covers last-year and all-time aggregates)
required: false
default: "500"
include_forks:
+1 -1
View File
@@ -49,7 +49,7 @@ FetchContributionsAllTime(ctx, profile, opts)
│ TotalCommitsAllTime
FetchProductive(ctx, profile, profile.SeedRepos, loc, commitsPerRepo)
FetchProductive(ctx, profile, profile.SeedRepos, loc, commitsPerRepo) // 0 = no cap
│ commitHistoryQuery × (#seeds × pages)
│ per commit: t = committedDate in loc
│ ProductiveAllTime[t.Hour]++
+27
View File
@@ -0,0 +1,27 @@
package github
import "testing"
func TestReachedCommitCap(t *testing.T) {
cases := []struct {
name string
seen int
maxPerRepo int
want bool
}{
{"under the cap", 100, 500, false},
{"at the cap", 500, 500, true},
{"past the cap", 700, 500, true},
// Zero means unlimited, so even a fresh repo keeps paginating. The
// old behavior stopped here and rendered empty commit-derived cards.
{"zero cap, nothing seen yet", 0, 0, false},
{"zero cap, deep into history", 100_000, 0, false},
{"negative cap treated as unlimited", 10, -1, false},
}
for _, tc := range cases {
if got := reachedCommitCap(tc.seen, tc.maxPerRepo); got != tc.want {
t.Errorf("%s: reachedCommitCap(%d, %d) = %v, want %v",
tc.name, tc.seen, tc.maxPerRepo, got, tc.want)
}
}
}
+15 -3
View File
@@ -30,9 +30,21 @@ type productiveGQL struct {
// magnitude is irrelevant because the card renders percentages.
const scaleFactor = 10_000
// reachedCommitCap reports whether a repo has given up enough commits to stop
// paginating. A cap of zero or less means no cap — keep going until the
// history runs out — matching how -top-repos treats zero. Without the guard a
// zero cap would stop before the first page and quietly empty every
// commit-derived card.
func reachedCommitCap(seen, maxPerRepo int) bool {
if maxPerRepo <= 0 {
return false
}
return seen >= maxPerRepo
}
// FetchProductive paginates the default-branch commit history (authored by
// the target user) for each repo up to maxPerRepo commits, and fills two
// parallel sets of aggregates on the Profile:
// the target user) for each repo up to maxPerRepo commits (0 = all), and
// fills two parallel sets of aggregates on the Profile:
//
// - Last-year: p.Productive (24h histogram) and p.CommitsByLanguage
// - All-time: p.ProductiveAllTime and p.CommitsByLanguageAllTime
@@ -62,7 +74,7 @@ func (c *Client) FetchProductive(ctx context.Context, p *Profile, repos []RepoIn
var cursor *string
seen := 0
for {
if seen >= maxPerRepo {
if reachedCommitCap(seen, maxPerRepo) {
break
}
owner := repo.Owner
+1 -1
View File
@@ -24,7 +24,7 @@ func main() {
themesFlag = flag.String("themes", "dracula", "comma-separated theme ids, or 'all'")
tzName = flag.String("tz", "Local", "timezone for productive-time card (IANA name, e.g. Asia/Saigon)")
topRepos = flag.Int("top-repos", 0, "optional cap on seed repos probed for commit history (0 = unlimited)")
perRepo = flag.Int("commits-per-repo", 500, "max commits sampled per repo (covers both last-year and all-time aggregates)")
perRepo = flag.Int("commits-per-repo", 500, "max commits sampled per repo, 0 = every commit (covers both last-year and all-time aggregates)")
includeForks = flag.Bool("include-forks", true, "include forked repos in stats and commit probing")
includePrivate = flag.Bool("include-private", true, "include private repos (requires PAT with repo scope; silently no-op otherwise)")
includeOrgs = flag.Bool("include-org-repos", false, "count org-owned repos you administer toward stars, repo count, repos-per-language and top-starred")