Files
ghstats/internal/github/contributions_all_time.go
T
tiennm99 2943db667c feat: seed-based repo scanning + fork/private visibility flags
Replace the top-10-starred sampling with a seed list built from
contributionsCollection.commitContributionsByRepository, unioned across
every active contributionYear. Commit-history probes now land only on
repos where the user actually committed, covering all owned repos plus
forks and repos owned by others when allowed.

New visibility knobs (default off — public-facing READMEs stay
safe):
- -include-forks / include_forks    : include forked repos
- -include-private / include_private: include private repos (requires
                                      PAT with repo scope)

Compatibility:
- -top-repos default changed 10 → 0 (unlimited); still usable as a cap
  for fast local runs.
- commitHistoryQuery now takes $owner so probes can target forks or
  repos outside the user's ownership.
- FetchProfile now accepts FetchOptions; PublicRepos counts only repos
  that pass the visibility filter.

Seed-list approach mirrors github-profile-summary-cards' own repo
sourcing but keeps our byte-weighted commit attribution.
2026-04-18 22:01:26 +07:00

100 lines
2.8 KiB
Go

package github
import (
"sort"
"time"
)
// contributionYearGQL mirrors contributionYearQuery.
type contributionYearGQL struct {
User *struct {
ContributionsCollection struct {
TotalCommitContributions int `json:"totalCommitContributions"`
ContributionCalendar struct {
Weeks []struct {
ContributionDays []struct {
ContributionCount int `json:"contributionCount"`
Date string `json:"date"`
} `json:"contributionDays"`
} `json:"weeks"`
} `json:"contributionCalendar"`
CommitContributionsByRepository []struct {
Contributions struct {
TotalCount int `json:"totalCount"`
} `json:"contributions"`
Repository repoNode `json:"repository"`
} `json:"commitContributionsByRepository"`
} `json:"contributionsCollection"`
} `json:"user"`
}
// FetchContributionsAllTime iterates p.ContributionYears and issues one
// contributionsCollection query per year. Each year's payload contributes:
//
// - Days → p.DailyContributionsAllTime
// - Commit count → p.TotalCommitsAllTime
// - Repos the user committed in → p.SeedRepos (deduplicated by owner/name)
//
// Fork and private repos are filtered client-side per opts so the caller can
// run the same pipeline with different visibility policies.
func (c *Client) FetchContributionsAllTime(p *Profile, opts FetchOptions) error {
years := append([]int(nil), p.ContributionYears...)
sort.Ints(years) // ascending so the concatenated series is oldest→newest
seen := map[string]int{} // "owner/name" → index in p.SeedRepos
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(contributionYearQuery, vars, &resp); err != nil {
return err
}
if resp.User == nil {
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 _, 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
}