feat: count org repos and fix contribution-window truncation

Split each contribution year into quarterly windows.
commitContributionsByRepository caps at 100 repos per query and drops
the remainder without signalling it, which a prolific year hits: 2026
returned exactly 100 where the quarterly union returns 108. Warn when a
window still comes back at the ceiling instead of reporting a complete
list.

Add -include-org-repos / include_org_repos so org-owned repos the user
administers count toward stars, repo count, repos-per-language and
top-starred. Off by default, since enabling it moves those totals for
every existing caller. Commit-driven cards already covered org repos
through the seed list. Org repos carrying only read or write access are
dropped via viewerPermission.

Rename Profile.PublicRepos to RepoCount and drop "public" from the card
label: the count tracks the fetch filters and includes private repos
whenever the token can see them.
This commit is contained in:
2026-08-13 13:20:41 +07:00
parent 9f7c16f835
commit 939fcdc33a
12 changed files with 294 additions and 79 deletions
+12 -4
View File
@@ -16,7 +16,7 @@ Cards rendered:
| # | Card | What it shows |
| --- | --- | --- |
| 0 | Profile details | Login (Name) title + Octicon-labelled rows for company, location, link, join date (with age), followers/following, public repos |
| 0 | Profile details | Login (Name) title + Octicon-labelled rows for company, location, link, join date (with age), followers/following, repo count |
| 1 | Repos per language | Donut + legend: how many owned non-fork repos use each language as primary |
| 2 | Most commit language (last year) | Donut + legend: last-year commits byte-weighted across each repo's language breakdown |
| 3 | Stats | Star, commit (lifetime + last-year), PR, issue, PR-review, contributed-to totals |
@@ -35,7 +35,7 @@ Cards rendered:
## Preview — dracula theme
Live render against the author's profile, committed by [`.github/workflows/demo.yml`](./.github/workflows/demo.yml) on every push to `main`. Rendered with `start_of_week: monday` so the heatmap rows and weekday bars start on Mon. Other 64 themes in the [**demo gallery**](./demo).
Live render against the author's profile, committed by [`.github/workflows/demo.yml`](./.github/workflows/demo.yml) on every push to `main`. Rendered with `start_of_week: monday` so the heatmap rows and weekday bars start on Mon, and `include_org_repos` on so repos under the author's orgs count toward the repo and language totals. Other 64 themes in the [**demo gallery**](./demo).
<div align="center">
@@ -124,6 +124,7 @@ Then embed the cards in your `README.md`:
| `commits_per_repo` | `500` | Max commits sampled per repo (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`) |
| `commit_changes` | `false` | Commit generated cards back to the repo |
| `commit_message` | `chore: update ghstats cards` | Commit message |
| `commit_branch` | *(current ref)* | Target branch for auto-commit |
@@ -163,11 +164,14 @@ ghstats -user tiennm99 -themes dracula,github_dark -tz Asia/Saigon -out output
| `-commits-per-repo` | `500` | Max commits sampled per repo |
| `-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 |
| `-list-themes` | | Print available theme ids and exit |
## How attribution works
**Repo sampling** uses a seed list built from `contributionsCollection.commitContributionsByRepository`, unioned across every active contribution year. This catches every repo you've committed in — not just your top-starred ones.
**Repo sampling** uses a seed list built from `contributionsCollection.commitContributionsByRepository`, unioned across every active contribution year. This catches every repo you've committed in — not just your top-starred ones. Each year is queried a quarter at a time: the API caps that field at 100 repos per query and drops the rest without saying so, which a prolific year hits easily.
**Which repos count where.** The commit-driven cards (most-commit-language, productive time, productive weekday, and everything derived from the contribution calendar) cover repos in *any* namespace you committed to — your own, your orgs', and upstream repos you sent PRs to. The repo-driven cards (stars, repo count, repos-per-language, top-starred) look only at repos you own. Set `include_org_repos` / `-include-org-repos` to also count org-owned repos where your permission is `ADMIN`; org repos you merely have read or write access to are never counted.
**Commit-to-language** is byte-weighted: each commit credits every language in the repo, proportional to linguist's byte share. A commit to a 60% Go / 40% Python repo adds 0.6 to Go and 0.4 to Python, regardless of which file was touched. Caveats:
@@ -175,7 +179,7 @@ ghstats -user tiennm99 -themes dracula,github_dark -tz Asia/Saigon -out output
- For per-file accuracy, a future `-accurate-languages` mode is planned (per-commit REST + go-enry).
**Cost per run** (current defaults, typical user):
- ~1 profile query + ~1 query per active year + ~50 commit-history pages ≈ **50-70 GraphQL calls**.
- ~1 profile query + ~4 queries per active year + ~50 commit-history pages ≈ **80-100 GraphQL calls**.
- Zero REST calls. Well under the 5000 points/hr budget.
## Themes
@@ -224,6 +228,10 @@ defaults to `true` so those commits are counted automatically once the token
has `repo` scope; pass `include_private: "false"` if you want to keep private
work out of the rendered cards even when the token can see it.
Enabling `include_org_repos` additionally needs `read:org` on the token, and
SSO authorization for any org that enforces it — otherwise those repos stay
invisible and the input silently changes nothing.
## Credits & inspiration
- [**github-profile-summary-cards**](https://github.com/vn7n24fzkq/github-profile-summary-cards) by [@vn7n24fzkq](https://github.com/vn7n24fzkq) — card layout, chart styles, theme palette, Octicon selection, and output structure.
+7
View File
@@ -47,6 +47,13 @@ inputs:
description: Include private repos (requires PAT with repo scope; silently no-op otherwise)
required: false
default: "true"
include_org_repos:
description: |
Count org-owned repos you administer toward stars, repo count, repos-per-language
and top-starred. Off by default because it changes those totals; commit-based cards
already include org repos either way. Needs a PAT with `read:org`.
required: false
default: "false"
commit_changes:
description: Whether to commit the generated cards back to the repo
required: false
+9 -5
View File
@@ -26,6 +26,8 @@ main.go
FetchProfile(ctx, login, opts)
│ profileQuery × N pages (owned repos, STARGAZERS desc, 100/page)
│ ownerAffiliations = [OWNER] (+ ORGANIZATION_MEMBER when
│ opts.IncludeOrgRepos; non-ADMIN org repos dropped client-side)
│ yields: Profile.{identity, stars, forks, PRs, issues,
│ TopRepos, ReposByLanguage,
│ ContributionYears,
@@ -34,10 +36,12 @@ FetchProfile(ctx, login, opts)
FetchContributionsAllTime(ctx, profile, opts)
│ contributionYearQuery × len(ContributionYears)
│ per year: totalCommitContributions +
│ contributionYearQuery × 4 quarters × len(ContributionYears)
│ per quarter: totalCommitContributions +
│ contributionCalendar.weeks +
│ commitContributionsByRepository(maxRepositories: 100)
│ quarters keep each window under the 100-repo ceiling, which a
│ year-wide window silently truncates at
│ yields: SeedRepos (deduped),
│ DailyContributionsAllTime,
│ TotalCommitsAllTime
@@ -64,14 +68,14 @@ All three queries live in `internal/github/queries.go`.
| Query | Purpose | Cost estimate |
| --- | --- | --- |
| `profileQuery` | Profile identity + totals + owned repos + last-year calendar | 110 calls (100 repos/page × ≤10 pages safety cap) |
| `contributionYearQuery` | Per-year calendar + seed list | 1 call per active year (typically 110) |
| `contributionYearQuery` | Per-quarter calendar + seed list | 4 calls per active year (typically 440) |
| `commitHistoryQuery` | Authored commits on default branch | 1 call per 100 commits per seed repo |
Typical run (8 active years, 30 seed repos, avg 50 commits each):
- profile: 1 call
- year loop: 8 calls
- quarter loop: 8 × 4 = 32 calls
- commit history: 30 × 1 = 30 calls
- **≈ 39 GraphQL calls, 0 REST calls**
- **≈ 63 GraphQL calls, 0 REST calls**
## Attribution model
+3 -1
View File
@@ -16,6 +16,7 @@ top_repos="${INPUT_TOP_REPOS:-0}"
commits_per_repo="${INPUT_COMMITS_PER_REPO:-500}"
include_forks="${INPUT_INCLUDE_FORKS:-true}"
include_private="${INPUT_INCLUDE_PRIVATE:-true}"
include_org_repos="${INPUT_INCLUDE_ORG_REPOS:-false}"
commit_changes="${INPUT_COMMIT_CHANGES:-false}"
commit_message="${INPUT_COMMIT_MESSAGE:-chore: update ghstats cards}"
commit_branch="${INPUT_COMMIT_BRANCH:-}"
@@ -40,7 +41,8 @@ ghstats \
-top-repos "$top_repos" \
-commits-per-repo "$commits_per_repo" \
-include-forks="$include_forks" \
-include-private="$include_private"
-include-private="$include_private" \
-include-org-repos="$include_org_repos"
if [ "$commit_changes" = "true" ]; then
workspace="${GITHUB_WORKSPACE:-/github/workspace}"
+2 -2
View File
@@ -23,7 +23,7 @@ func TestRenderAll(t *testing.T) {
Company: "VNG & <Corp>",
Followers: 12,
Following: 7,
PublicRepos: 42,
RepoCount: 42,
TotalStars: 1234,
ReposByLanguage: []github.LangStat{
{Name: "Go", Color: "#00ADD8", Value: 5},
@@ -376,7 +376,7 @@ func adversarialProfile() *github.Profile {
Website: "https://example-with-a-very-long-domain.example.com/profile",
Followers: 1_234_567,
Following: 98_765,
PublicRepos: 4_321,
RepoCount: 4_321,
TotalStars: 10_000_000,
TotalCommits: 123_456,
TotalCommitsAllTime: 9_876_543,
+1 -1
View File
@@ -95,7 +95,7 @@ func buildProfileRows(p *github.Profile) []profileRow {
})
rows = append(rows, profileRow{
icon: iconRepos,
value: fmt.Sprintf("%s public repos", formatInt(p.PublicRepos)),
value: fmt.Sprintf("%s repos", formatInt(p.RepoCount)),
})
return rows
}
@@ -0,0 +1,94 @@
package github
import (
"testing"
"time"
)
func TestContributionWindowsFullYear(t *testing.T) {
now := time.Date(2026, 8, 13, 0, 0, 0, 0, time.UTC)
got := contributionWindows(2024, now)
if len(got) != 4 {
t.Fatalf("want 4 quarters for a completed year, got %d", len(got))
}
wantStarts := []string{"2024-01-01", "2024-04-01", "2024-07-01", "2024-10-01"}
wantEnds := []string{"2024-03-31", "2024-06-30", "2024-09-30", "2024-12-31"}
for i, w := range got {
if s := w[0].Format("2006-01-02"); s != wantStarts[i] {
t.Errorf("quarter %d start = %s, want %s", i, s, wantStarts[i])
}
if e := w[1].Format("2006-01-02"); e != wantEnds[i] {
t.Errorf("quarter %d end = %s, want %s", i, e, wantEnds[i])
}
}
// Windows must not overlap, or days would be counted twice in the
// all-time contribution series.
for i := 1; i < len(got); i++ {
if !got[i][0].After(got[i-1][1]) {
t.Errorf("quarter %d starts at/before the end of quarter %d", i, i-1)
}
}
}
func TestContributionWindowsCurrentYearStopsAtNow(t *testing.T) {
now := time.Date(2026, 8, 13, 10, 30, 0, 0, time.UTC)
got := contributionWindows(2026, now)
if len(got) != 3 {
t.Fatalf("want 3 quarters through August, got %d", len(got))
}
if last := got[2][1]; !last.Equal(now) {
t.Errorf("final window end = %s, want clamped to now (%s)", last, now)
}
}
func TestContributionWindowsFutureYearIsEmpty(t *testing.T) {
now := time.Date(2026, 8, 13, 0, 0, 0, 0, time.UTC)
if got := contributionWindows(2027, now); len(got) != 0 {
t.Fatalf("want no windows for a future year, got %d", len(got))
}
}
func TestRepoAffiliationsAndOwnership(t *testing.T) {
orgAdmin := repoNode{Name: "chambai", ViewerPermission: "ADMIN"}
orgAdmin.Owner = &struct {
Login string `json:"login"`
}{Login: "miti99dev"}
orgWrite := orgAdmin
orgWrite.ViewerPermission = "WRITE"
own := repoNode{Name: "ghstats"}
own.Owner = &struct {
Login string `json:"login"`
}{Login: "tiennm99"}
off := FetchOptions{}
on := FetchOptions{IncludeOrgRepos: true}
if got := repoAffiliations(off); len(got) != 1 || got[0] != "OWNER" {
t.Errorf("affiliations with org repos off = %v, want [OWNER]", got)
}
if got := repoAffiliations(on); len(got) != 2 {
t.Errorf("affiliations with org repos on = %v, want OWNER + ORGANIZATION_MEMBER", got)
}
cases := []struct {
name string
node repoNode
opts FetchOptions
want bool
}{
{"own repo, org off", own, off, true},
{"own repo, org on", own, on, true},
{"org admin repo, org off", orgAdmin, off, false},
{"org admin repo, org on", orgAdmin, on, true},
{"org write-only repo, org on", orgWrite, on, false},
}
for _, tc := range cases {
if got := ownedByUser(tc.node, "tiennm99", tc.opts); got != tc.want {
t.Errorf("%s: ownedByUser = %v, want %v", tc.name, got, tc.want)
}
}
}
+100 -49
View File
@@ -31,8 +31,38 @@ type contributionYearGQL struct {
} `json:"user"`
}
// maxRepositoriesPerWindow mirrors the maxRepositories argument in
// contributionYearQuery. GitHub caps it at 100, and a window that returns
// exactly that many repos is almost certainly truncated.
const maxRepositoriesPerWindow = 100
// contributionWindows splits one calendar year into quarters, dropping any
// quarter that starts after now and clamping the last one to now.
//
// A year-wide window silently loses repos once the user commits in more than
// maxRepositoriesPerWindow of them in that year — the query returns the top
// 100 and says nothing about the rest. Quarters keep each window well under
// the ceiling. The API clips contributionCalendar to the exact window (no
// week-boundary spillover), so concatenating quarters yields each day once.
func contributionWindows(year int, now time.Time) [][2]time.Time {
var out [][2]time.Time
for q := 0; q < 4; q++ {
from := time.Date(year, time.Month(q*3+1), 1, 0, 0, 0, 0, time.UTC)
if from.After(now) {
break
}
to := from.AddDate(0, 3, 0).Add(-time.Second)
if to.After(now) {
to = now
}
out = append(out, [2]time.Time{from, to})
}
return out
}
// FetchContributionsAllTime iterates p.ContributionYears and issues one
// contributionsCollection query per year. Each year's payload contributes:
// contributionsCollection query per quarter. Each window's payload
// contributes:
//
// - Days → p.DailyContributionsAllTime
// - Commit count → p.TotalCommitsAllTime
@@ -45,62 +75,83 @@ func (c *Client) FetchContributionsAllTime(ctx context.Context, p *Profile, opts
sort.Ints(years) // ascending so the concatenated series is oldest→newest
seen := map[string]int{} // "owner/name" → index in p.SeedRepos
now := time.Now().UTC()
for _, y := range years {
from := time.Date(y, 1, 1, 0, 0, 0, 0, time.UTC)
to := time.Date(y, 12, 31, 23, 59, 59, 0, time.UTC)
if now := time.Now().UTC(); to.After(now) {
to = now
}
vars := map[string]any{
"login": p.Login,
"from": from.Format(time.RFC3339),
"to": to.Format(time.RFC3339),
}
var resp contributionYearGQL
if err := c.query(ctx, contributionYearQuery, vars, &resp); err != nil {
return err
}
if resp.User == nil {
// Don't abort the run — other years may still yield data — but
// make the partial-data case visible instead of rendering an
// empty all-time card silently.
fmt.Fprintf(os.Stderr, "warn: contribution year %d returned no user data\n", y)
continue
}
cc := resp.User.ContributionsCollection
p.TotalCommitsAllTime += cc.TotalCommitContributions
for _, w := range cc.ContributionCalendar.Weeks {
for _, d := range w.ContributionDays {
t, err := time.Parse("2006-01-02", d.Date)
if err != nil {
continue
}
p.DailyContributionsAllTime = append(p.DailyContributionsAllTime, DailyContribution{
Date: t,
Count: d.ContributionCount,
})
for _, w := range contributionWindows(y, now) {
if err := c.fetchContributionWindow(ctx, p, opts, w[0], w[1], seen); err != nil {
return err
}
}
}
return nil
}
for _, cr := range cc.CommitContributionsByRepository {
r := cr.Repository
if r.IsFork && !opts.IncludeForks {
// fetchContributionWindow folds a single contributionsCollection window into
// the profile. Split out from FetchContributionsAllTime so the quarter loop
// stays readable.
func (c *Client) fetchContributionWindow(
ctx context.Context,
p *Profile,
opts FetchOptions,
from, to time.Time,
seen map[string]int,
) error {
vars := map[string]any{
"login": p.Login,
"from": from.Format(time.RFC3339),
"to": to.Format(time.RFC3339),
}
var resp contributionYearGQL
if err := c.query(ctx, contributionYearQuery, vars, &resp); err != nil {
return err
}
if resp.User == nil {
// Don't abort the run — other windows may still yield data — but
// make the partial-data case visible instead of rendering an
// empty all-time card silently.
fmt.Fprintf(os.Stderr, "warn: contributions %s..%s returned no user data\n",
from.Format("2006-01-02"), to.Format("2006-01-02"))
return nil
}
cc := resp.User.ContributionsCollection
p.TotalCommitsAllTime += cc.TotalCommitContributions
for _, w := range cc.ContributionCalendar.Weeks {
for _, d := range w.ContributionDays {
t, err := time.Parse("2006-01-02", d.Date)
if err != nil {
continue
}
if r.IsPrivate && !opts.IncludePrivate {
continue
}
info := r.toRepoInfo(p.Login)
key := info.Owner + "/" + info.Name
if _, ok := seen[key]; ok {
continue
}
seen[key] = len(p.SeedRepos)
p.SeedRepos = append(p.SeedRepos, info)
p.DailyContributionsAllTime = append(p.DailyContributionsAllTime, DailyContribution{
Date: t,
Count: d.ContributionCount,
})
}
}
// Surface a hit ceiling rather than pretending the list is complete.
if len(cc.CommitContributionsByRepository) >= maxRepositoriesPerWindow {
fmt.Fprintf(os.Stderr,
"warn: contributions %s..%s hit the %d-repo ceiling; some repos are missing from commit probing\n",
from.Format("2006-01-02"), to.Format("2006-01-02"), maxRepositoriesPerWindow)
}
for _, cr := range cc.CommitContributionsByRepository {
r := cr.Repository
if r.IsFork && !opts.IncludeForks {
continue
}
if r.IsPrivate && !opts.IncludePrivate {
continue
}
info := r.toRepoInfo(p.Login)
key := info.Owner + "/" + info.Name
if _, ok := seen[key]; ok {
continue
}
seen[key] = len(p.SeedRepos)
p.SeedRepos = append(p.SeedRepos, info)
}
return nil
}
+15 -9
View File
@@ -14,9 +14,12 @@ type Profile struct {
Website string
CreatedAt time.Time
Followers int
Following int
PublicRepos int
Followers int
Following int
// RepoCount is how many repos survived the fetch filters, so it tracks
// -include-forks / -include-private / -include-org-repos rather than
// GitHub's public-only publicRepos count.
RepoCount int
// Totals for the stats card.
TotalStars int
@@ -132,12 +135,15 @@ type LangEdge struct {
// repoNode is the GraphQL shape of one repository node; kept here because
// it's shared by the profile fetcher and the productive-time fetcher.
type repoNode struct {
Name string `json:"name"`
IsPrivate bool `json:"isPrivate"`
IsFork bool `json:"isFork"`
StargazerCount int `json:"stargazerCount"`
ForkCount int `json:"forkCount"`
Owner *struct {
Name string `json:"name"`
IsPrivate bool `json:"isPrivate"`
IsFork bool `json:"isFork"`
StargazerCount int `json:"stargazerCount"`
ForkCount int `json:"forkCount"`
// ViewerPermission is only requested by profileQuery, where it separates
// org repos the user administers from ones they merely have access to.
ViewerPermission string `json:"viewerPermission"`
Owner *struct {
Login string `json:"login"`
} `json:"owner"`
PrimaryLanguage *struct {
+40 -4
View File
@@ -66,6 +66,36 @@ type profileGQL struct {
type FetchOptions struct {
IncludeForks bool
IncludePrivate bool
// IncludeOrgRepos widens the repo-owned aggregates (stars, forks, repo
// count, repos-per-language, top-starred) beyond the user's own namespace
// to org-owned repos they administer. Off by default: turning it on
// changes every one of those numbers, so it stays an explicit opt-in.
IncludeOrgRepos bool
}
// adminPermission is the viewerPermission value that marks an org repo as
// effectively the user's own. Members with WRITE/READ on a company repo they
// never created would otherwise inflate every owned-repo aggregate.
const adminPermission = "ADMIN"
// repoAffiliations maps opts onto the ownerAffiliations argument. OWNER alone
// is GitHub's "repos in your own namespace"; ORGANIZATION_MEMBER adds repos
// owned by orgs the user belongs to, which viewerPermission then narrows.
func repoAffiliations(opts FetchOptions) []string {
if opts.IncludeOrgRepos {
return []string{"OWNER", "ORGANIZATION_MEMBER"}
}
return []string{"OWNER"}
}
// ownedByUser reports whether a repo node counts as the user's own for the
// repo-derived cards: anything in their namespace, plus org repos they
// administer when opts allows it.
func ownedByUser(r repoNode, login string, opts FetchOptions) bool {
if r.Owner == nil || r.Owner.Login == login {
return true
}
return opts.IncludeOrgRepos && r.ViewerPermission == adminPermission
}
// FetchProfile collects profile, stats and repos-per-language data for the
@@ -79,12 +109,15 @@ func (c *Client) FetchProfile(ctx context.Context, login string, opts FetchOptio
p := &Profile{Login: login}
reposPerLang := map[string]int64{}
langColor := map[string]string{}
publicRepoCount := 0
repoCount := 0
var cursor *string
const maxPages = 10
for page := 0; page < maxPages; page++ {
vars := map[string]any{"login": login}
vars := map[string]any{
"login": login,
"affiliations": repoAffiliations(opts),
}
if cursor != nil {
vars["after"] = *cursor
}
@@ -137,13 +170,16 @@ func (c *Client) FetchProfile(ctx context.Context, login string, opts FetchOptio
}
for _, r := range u.Repositories.Nodes {
if !ownedByUser(r, login, opts) {
continue
}
if r.IsFork && !opts.IncludeForks {
continue
}
if r.IsPrivate && !opts.IncludePrivate {
continue
}
publicRepoCount++
repoCount++
p.TotalStars += r.StargazerCount
p.TotalForks += r.ForkCount
@@ -170,7 +206,7 @@ func (c *Client) FetchProfile(ctx context.Context, login string, opts FetchOptio
}
p.ReposByLanguage = sortLangStats(reposPerLang, langColor)
p.PublicRepos = publicRepoCount
p.RepoCount = repoCount
return p, nil
}
+7 -2
View File
@@ -3,8 +3,11 @@ package github
// profileQuery pulls everything needed for the profile, stats and languages
// cards in one round trip. Fork/private filtering is done client-side so one
// query handles all combinations of -include-forks / -include-private.
// $affiliations decides whether org-owned repos are in scope at all; repos the
// user only has read/write access to are dropped client-side via
// viewerPermission.
const profileQuery = `
query($login: String!, $after: String) {
query($login: String!, $after: String, $affiliations: [RepositoryAffiliation]) {
user(login: $login) {
id
login
@@ -44,13 +47,15 @@ query($login: String!, $after: String) {
repositories(
first: 100
after: $after
ownerAffiliations: OWNER
ownerAffiliations: $affiliations
orderBy: { field: STARGAZERS, direction: DESC }
) {
totalCount
pageInfo { hasNextPage endCursor }
nodes {
name
owner { login }
viewerPermission
isPrivate
isFork
stargazerCount
+4 -2
View File
@@ -27,6 +27,7 @@ func main() {
perRepo = flag.Int("commits-per-repo", 500, "max commits sampled per repo (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")
timeout = flag.Duration("timeout", 30*time.Minute, "overall deadline for fetch phase (0 = no limit)")
startOfWeek = flag.String("start-of-week", "sunday", "first day of week for heatmap rows and weekday bars (sunday|monday|tuesday|…)")
listThemes = flag.Bool("list-themes", false, "print available theme ids and exit")
@@ -65,8 +66,9 @@ func main() {
}
opts := github.FetchOptions{
IncludeForks: *includeForks,
IncludePrivate: *includePrivate,
IncludeForks: *includeForks,
IncludePrivate: *includePrivate,
IncludeOrgRepos: *includeOrgs,
}
// Overall fetch budget. Ctrl-C cancels in-flight HTTP requests cleanly.