mirror of
https://github.com/tiennm99/ghstats.git
synced 2026-09-10 06:20:29 +00:00
refactor(github): attribute commits by repo language byte share
Each commit now contributes a full "vote" partitioned across the repo's
languages proportional to linguist byte counts, instead of crediting only
the primary language. A 60% Go / 40% Python repo adds 0.6 to Go and 0.4
to Python per commit.
- RepoInfo gains []LangEdge capturing the full byte breakdown already
returned by profileQuery.
- FetchProductive distributes each commit via a fixed-point scaleFactor
(int64 preserved, percentages unchanged in the card).
- Fallback to primary language only when linguist reports zero bytes
(empty repo).
Caveat: linguist excludes prose languages (Markdown, AsciiDoc, reST) from
its byte output, so Markdown-heavy repos still skew toward the detected
code fraction. Fixing that case requires per-commit file classification
via REST /commits/{sha} + go-enry — tracked as future work.
This commit is contained in:
@@ -56,6 +56,17 @@ type RepoInfo struct {
|
||||
Name string
|
||||
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
|
||||
|
||||
@@ -23,11 +23,25 @@ type productiveGQL struct {
|
||||
} `json:"repository"`
|
||||
}
|
||||
|
||||
// scaleFactor is the fixed-point multiplier used when distributing a single
|
||||
// commit across several languages by byte share. Stored in LangStat.Value
|
||||
// (int64) so the existing sort + percentage math keeps working; the absolute
|
||||
// magnitude is irrelevant because the card renders percentages.
|
||||
const scaleFactor = 10_000
|
||||
|
||||
// FetchProductive fills p.Productive with a 24-hour commit histogram over the
|
||||
// last year and p.CommitsByLanguage with commit counts attributed to each
|
||||
// repo's primary language. Commits are gathered from the given repos (usually
|
||||
// p.TopRepos[:N]); each repo is sampled up to maxPerRepo commits to keep the
|
||||
// cost bounded.
|
||||
// last year and p.CommitsByLanguage with commit counts distributed across each
|
||||
// repo's language byte breakdown. Commits are gathered from the given repos
|
||||
// (usually p.TopRepos[:N]); each repo is sampled up to maxPerRepo commits to
|
||||
// keep the cost bounded.
|
||||
//
|
||||
// Attribution model: each commit contributes a whole scaleFactor unit,
|
||||
// partitioned across the repo's languages proportional to linguist byte
|
||||
// counts. A repo that is 60% Go / 40% Python credits 0.6 to Go and 0.4 to
|
||||
// Python per commit — a strict upgrade over the previous primary-language-
|
||||
// only model. Prose languages (Markdown, AsciiDoc, …) remain excluded by
|
||||
// linguist itself, so blog-style repos still skew toward their detected
|
||||
// code fraction; fixing that requires per-commit file classification.
|
||||
//
|
||||
// The timezone loc is applied to CommittedDate so the heatmap reflects when
|
||||
// the user actually commits, not UTC.
|
||||
@@ -72,12 +86,7 @@ func (c *Client) FetchProductive(p *Profile, repos []RepoInfo, loc *time.Locatio
|
||||
continue
|
||||
}
|
||||
p.Productive[t.In(loc).Hour()]++
|
||||
if repo.PrimaryLanguage != "" {
|
||||
commitsByLang[repo.PrimaryLanguage]++
|
||||
if _, ok := langColor[repo.PrimaryLanguage]; !ok {
|
||||
langColor[repo.PrimaryLanguage] = repo.PrimaryColor
|
||||
}
|
||||
}
|
||||
attributeCommit(repo, commitsByLang, langColor)
|
||||
seen++
|
||||
}
|
||||
if !h.PageInfo.HasNextPage {
|
||||
@@ -91,3 +100,32 @@ func (c *Client) FetchProductive(p *Profile, repos []RepoInfo, loc *time.Locatio
|
||||
p.CommitsByLanguage = sortLangStats(commitsByLang, langColor)
|
||||
return nil
|
||||
}
|
||||
|
||||
// attributeCommit distributes a single commit across the repo's languages
|
||||
// proportional to byte share. Falls back to the primary language when no
|
||||
// byte breakdown is available (empty repo or linguist-free repo).
|
||||
func attributeCommit(repo RepoInfo, commitsByLang map[string]int64, langColor map[string]string) {
|
||||
var total int64
|
||||
for _, l := range repo.Languages {
|
||||
total += l.Bytes
|
||||
}
|
||||
if total == 0 {
|
||||
if repo.PrimaryLanguage != "" {
|
||||
commitsByLang[repo.PrimaryLanguage] += scaleFactor
|
||||
if _, ok := langColor[repo.PrimaryLanguage]; !ok {
|
||||
langColor[repo.PrimaryLanguage] = repo.PrimaryColor
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, l := range repo.Languages {
|
||||
share := int64(scaleFactor) * l.Bytes / total
|
||||
if share == 0 {
|
||||
continue
|
||||
}
|
||||
commitsByLang[l.Name] += share
|
||||
if _, ok := langColor[l.Name]; !ok {
|
||||
langColor[l.Name] = l.Color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,16 +116,20 @@ func (c *Client) FetchProfile(login string) (*Profile, error) {
|
||||
langColor[r.PrimaryLanguage.Name] = r.PrimaryLanguage.Color
|
||||
}
|
||||
}
|
||||
p.TopRepos = append(p.TopRepos, info)
|
||||
|
||||
// Capture secondary language colors so productive-time's
|
||||
// per-language aggregation can color them even if that language
|
||||
// isn't the primary of any other repo.
|
||||
// Carry the repo's full language-bytes breakdown so commit
|
||||
// attribution can distribute each commit across all languages
|
||||
// the repo contains, not just the primary.
|
||||
for _, e := range r.Languages.Edges {
|
||||
info.Languages = append(info.Languages, LangEdge{
|
||||
Name: e.Node.Name,
|
||||
Color: e.Node.Color,
|
||||
Bytes: e.Size,
|
||||
})
|
||||
if _, ok := langColor[e.Node.Name]; !ok {
|
||||
langColor[e.Node.Name] = e.Node.Color
|
||||
}
|
||||
}
|
||||
p.TopRepos = append(p.TopRepos, info)
|
||||
}
|
||||
|
||||
if !u.Repositories.PageInfo.HasNextPage {
|
||||
|
||||
Reference in New Issue
Block a user