package github import ( "errors" "sort" "time" ) // profileGQL mirrors the GraphQL response for profileQuery. type profileGQL struct { User *struct { ID string `json:"id"` Login string `json:"login"` Name string `json:"name"` Bio string `json:"bio"` AvatarURL string `json:"avatarUrl"` Company string `json:"company"` Location string `json:"location"` Website string `json:"websiteUrl"` CreatedAt string `json:"createdAt"` Followers struct{ TotalCount int } `json:"followers"` Following struct{ TotalCount int } `json:"following"` PullRequests struct{ TotalCount int } `json:"pullRequests"` Issues struct{ TotalCount int } `json:"issues"` RepositoriesContributedTo struct{ TotalCount int } `json:"repositoriesContributedTo"` ContributionsCollection struct { TotalCommitContributions int `json:"totalCommitContributions"` TotalIssueContributions int `json:"totalIssueContributions"` TotalPullRequestContributions int `json:"totalPullRequestContributions"` TotalPullRequestReviewContributions int `json:"totalPullRequestReviewContributions"` TotalRepositoryContributions int `json:"totalRepositoryContributions"` RestrictedContributionsCount int `json:"restrictedContributionsCount"` ContributionCalendar struct { TotalContributions int `json:"totalContributions"` } `json:"contributionCalendar"` } `json:"contributionsCollection"` Repositories struct { TotalCount int `json:"totalCount"` PageInfo struct { HasNextPage bool `json:"hasNextPage"` EndCursor string `json:"endCursor"` } `json:"pageInfo"` Nodes []repoNode `json:"nodes"` } `json:"repositories"` } `json:"user"` } // FetchProfile collects profile, stats and language data for the given user. // Repositories are paginated up to 10 pages (1000 owned repos) as a safety cap. func (c *Client) FetchProfile(login string) (*Profile, error) { if login == "" { return nil, errors.New("empty user") } p := &Profile{Login: login} langBytes := map[string]int64{} langColor := map[string]string{} var cursor *string const maxPages = 10 for page := 0; page < maxPages; page++ { vars := map[string]any{"login": login} if cursor != nil { vars["after"] = *cursor } var resp profileGQL if err := c.query(profileQuery, vars, &resp); err != nil { return nil, err } if resp.User == nil { return nil, errors.New("user not found") } u := resp.User if page == 0 { p.ID = u.ID p.Name = u.Name p.Bio = u.Bio p.AvatarURL = u.AvatarURL p.Company = u.Company p.Location = u.Location p.Website = u.Website if t, err := time.Parse(time.RFC3339, u.CreatedAt); err == nil { p.CreatedAt = t } p.Followers = u.Followers.TotalCount p.Following = u.Following.TotalCount p.PublicRepos = u.Repositories.TotalCount p.TotalPRs = u.PullRequests.TotalCount p.TotalIssues = u.Issues.TotalCount p.TotalContributedTo = u.RepositoriesContributedTo.TotalCount cc := u.ContributionsCollection p.TotalCommits = cc.TotalCommitContributions p.TotalReviews = cc.TotalPullRequestReviewContributions p.TotalContributions = cc.ContributionCalendar.TotalContributions + cc.RestrictedContributionsCount } for _, r := range u.Repositories.Nodes { p.TotalStars += r.StargazerCount p.TotalForks += r.ForkCount p.TopRepos = append(p.TopRepos, r.Name) for _, e := range r.Languages.Edges { langBytes[e.Node.Name] += e.Size if _, ok := langColor[e.Node.Name]; !ok { langColor[e.Node.Name] = e.Node.Color } } } if !u.Repositories.PageInfo.HasNextPage { break } end := u.Repositories.PageInfo.EndCursor cursor = &end } p.Languages = sortLanguages(langBytes, langColor) return p, nil } func sortLanguages(bytes map[string]int64, color map[string]string) []LangStat { out := make([]LangStat, 0, len(bytes)) for name, b := range bytes { out = append(out, LangStat{Name: name, Color: color[name], Bytes: b}) } sort.Slice(out, func(i, j int) bool { if out[i].Bytes != out[j].Bytes { return out[i].Bytes > out[j].Bytes } return out[i].Name < out[j].Name }) return out }