Files
ghstats/internal/github/model.go
T
tiennm99 939fcdc33a 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.
2026-08-13 13:20:41 +07:00

191 lines
6.4 KiB
Go

package github
import "time"
// Profile is the aggregate of data other packages render into cards.
type Profile struct {
ID string
Login string
Name string
Bio string
AvatarURL string
Company string
Location string
Website string
CreatedAt time.Time
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
TotalForks int
TotalCommits int // last year, from contributionsCollection
TotalCommitsAllTime int // sum across contributionYears
TotalPRs int
TotalIssues int
TotalReviews int
TotalContributedTo int
TotalContributionsLastYear int // contributionCalendar.totalContributions + restrictedContributionsCount (last year)
// Count of owned repos grouped by primary language, sorted desc by Value.
ReposByLanguage []LangStat
// Count of commits (last year, by this user) attributed to each repo's
// primary language, sorted desc. Populated by FetchProductive.
CommitsByLanguage []LangStat
// Commit counts grouped by hour-of-day (0-23) in the configured timezone.
// Productive is the last-year slice; ProductiveAllTime is the lifetime
// slice derived from the same paginated commit history so we pay for
// pagination once.
Productive [24]int
ProductiveAllTime [24]int
// Same pagination also feeds day-of-week histograms (index 0 = Sunday
// to match time.Weekday). Last-year and all-time kept separately so the
// weekday cards mirror the hour-of-day pair.
Weekday [7]int
WeekdayAllTime [7]int
// CommitsByLanguageAllTime is the lifetime counterpart of
// CommitsByLanguage, computed from the same commit stream.
CommitsByLanguageAllTime []LangStat
// DailyContributions is the raw per-day contribution calendar covering
// the most recent year. The area chart aggregates it into monthly
// buckets; kept granular here so any downstream card can re-bin freely.
DailyContributions []DailyContribution
// DailyContributionsAllTime concatenates contribution calendars across
// every year the user has been active (user.contributionYears), so the
// all-time area chart can show history beyond the default 1-year window.
DailyContributionsAllTime []DailyContribution
// TopRepos are owned non-fork repos sorted by stargazer count desc,
// populated by FetchProfile. Used for the profile's stars/forks totals
// and the repos-per-language card.
TopRepos []RepoInfo
// SeedRepos are the repos where the user actually committed, unioned
// across every contribution year via commitContributionsByRepository.
// Used by FetchProductive so commit-history probes land where there is
// signal instead of across the long tail of empty owned repos.
SeedRepos []RepoInfo
// ContributionYears lists every calendar year the user has been active
// on GitHub, newest first. Used by FetchContributionsAllTime to iterate
// per-year contributionsCollection queries.
ContributionYears []int
// UTCOffsetLabel is the configured timezone rendered as "UTC±N.NN" for
// display on time-based cards (e.g. "UTC+7.00" for Asia/Saigon). Filled
// by the CLI after loading -tz.
UTCOffsetLabel string
// WeekStart is the weekday used as row 0 on the heatmap and the first
// bar on the productive-weekday chart. Defaults to time.Sunday to match
// GitHub's own contribution calendar; -start-of-week on the CLI can flip
// it to time.Monday (or any other weekday) without touching the data —
// only the presentation order changes.
WeekStart time.Weekday
}
// DailyContribution is a single day in the contributions calendar.
type DailyContribution struct {
Date time.Time
Count int
}
// LangStat is one row in a language breakdown card. Value is repo count or
// commit count depending on which slice holds it.
type LangStat struct {
Name string
Color string
Value int64
}
// RepoInfo is the minimal repo summary used for downstream fetches. Owner is
// kept so the history query can target forks / repos belonging to other users.
type RepoInfo struct {
Owner string
Name string
Stars int
IsPrivate bool
IsFork bool
PrimaryLanguage string
PrimaryColor string
// Languages is the repo's language byte breakdown as reported by GitHub
// linguist. Used to attribute each commit fractionally across all
// languages the repo contains, rather than crediting only the primary.
Languages []LangEdge
}
// LangEdge is a single entry in a repo's language-bytes breakdown.
type LangEdge struct {
Name string
Color string
Bytes int64
}
// 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"`
// 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 {
Name string `json:"name"`
Color string `json:"color"`
} `json:"primaryLanguage"`
Languages struct {
Edges []struct {
Size int64 `json:"size"`
Node struct {
Name string `json:"name"`
Color string `json:"color"`
} `json:"node"`
} `json:"edges"`
} `json:"languages"`
}
// toRepoInfo converts the GraphQL shape into our public RepoInfo, defaulting
// owner to the given login if the node omits it (profile query doesn't
// request owner since it's implicit on user.repositories).
func (r repoNode) toRepoInfo(defaultOwner string) RepoInfo {
info := RepoInfo{
Owner: defaultOwner,
Name: r.Name,
Stars: r.StargazerCount,
IsPrivate: r.IsPrivate,
IsFork: r.IsFork,
}
if r.Owner != nil {
info.Owner = r.Owner.Login
}
if r.PrimaryLanguage != nil {
info.PrimaryLanguage = r.PrimaryLanguage.Name
info.PrimaryColor = r.PrimaryLanguage.Color
}
for _, e := range r.Languages.Edges {
info.Languages = append(info.Languages, LangEdge{
Name: e.Node.Name,
Color: e.Node.Color,
Bytes: e.Size,
})
}
return info
}