fix(github): split a saturated quarter by month to recover dropped repos

Quarterly windows were not enough for the busiest quarter: a live run
warned that 2026-04-01..2026-06-30 still came back at the 100-repo
ceiling, so its seed list was truncated and those repos never got
probed for commit history.

Re-ask a saturated quarter month by month and merge only the repo
lists. The quarter has already contributed its calendar days and commit
totals, so the month passes skip that fold; verified by forcing the
ceiling down to 20 and confirming all six calendar-derived cards stay
byte-identical. Warn only when a month itself saturates, since there is
no finer window left to ask for.
This commit is contained in:
2026-08-13 13:34:08 +07:00
parent 7832842c5d
commit 2c3667d28c
4 changed files with 104 additions and 28 deletions
@@ -92,3 +92,37 @@ func TestRepoAffiliationsAndOwnership(t *testing.T) {
}
}
}
func TestMonthWindowsCoverQuarterExactly(t *testing.T) {
from := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
to := time.Date(2026, 6, 30, 23, 59, 59, 0, time.UTC)
got := monthWindows(from, to)
if len(got) != 3 {
t.Fatalf("want 3 months in a quarter, got %d", len(got))
}
if !got[0][0].Equal(from) {
t.Errorf("first month starts at %s, want %s", got[0][0], from)
}
if !got[len(got)-1][1].Equal(to) {
t.Errorf("last month ends at %s, want the quarter's end %s", got[len(got)-1][1], to)
}
for i := 1; i < len(got); i++ {
if !got[i][0].After(got[i-1][1]) {
t.Errorf("month %d overlaps month %d", i, i-1)
}
}
}
func TestMonthWindowsClampPartialFinalMonth(t *testing.T) {
// A current quarter clamped to "now" mid-month must not hand back a
// window running past it.
from := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
now := time.Date(2026, 8, 13, 10, 30, 0, 0, time.UTC)
got := monthWindows(from, now)
if len(got) != 2 {
t.Fatalf("want July + partial August, got %d", len(got))
}
if !got[1][1].Equal(now) {
t.Errorf("final month ends at %s, want clamped to %s", got[1][1], now)
}
}
+63 -23
View File
@@ -41,9 +41,10 @@ const maxRepositoriesPerWindow = 100
//
// 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.
// 100 and says nothing about the rest. Quarters bring most years under the
// ceiling; a quarter that still saturates gets split again by month. 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++ {
@@ -79,24 +80,58 @@ func (c *Client) FetchContributionsAllTime(ctx context.Context, p *Profile, opts
for _, y := range years {
for _, w := range contributionWindows(y, now) {
if err := c.fetchContributionWindow(ctx, p, opts, w[0], w[1], seen); err != nil {
saturated, err := c.fetchContributionWindow(ctx, p, opts, w[0], w[1], seen, true)
if err != nil {
return err
}
if !saturated {
continue
}
// The quarter came back at the ceiling, so its repo list is
// truncated. Re-ask month by month to recover the tail. Only the
// repo lists are merged — the quarter already contributed its
// days and commit totals, and folding them again would
// double-count.
for _, m := range monthWindows(w[0], w[1]) {
if _, err := c.fetchContributionWindow(ctx, p, opts, m[0], m[1], seen, false); err != nil {
return err
}
}
}
}
return nil
}
// monthWindows splits an arbitrary window into calendar months, clamped to the
// window's own bounds so the pieces cover exactly the same span.
func monthWindows(from, to time.Time) [][2]time.Time {
var out [][2]time.Time
start := from
for start.Before(to) || start.Equal(to) {
next := time.Date(start.Year(), start.Month(), 1, 0, 0, 0, 0, time.UTC).AddDate(0, 1, 0)
end := next.Add(-time.Second)
if end.After(to) {
end = to
}
out = append(out, [2]time.Time{start, end})
start = next
}
return out
}
// fetchContributionWindow folds a single contributionsCollection window into
// the profile. Split out from FetchContributionsAllTime so the quarter loop
// stays readable.
// the profile and reports whether its repo list came back at the ceiling (and
// so is truncated). foldCalendar is false for the month re-queries that follow
// a saturated quarter: those exist only to recover repos, and re-folding their
// days would count the same contributions twice.
func (c *Client) fetchContributionWindow(
ctx context.Context,
p *Profile,
opts FetchOptions,
from, to time.Time,
seen map[string]int,
) error {
foldCalendar bool,
) (bool, error) {
vars := map[string]any{
"login": p.Login,
"from": from.Format(time.RFC3339),
@@ -104,7 +139,7 @@ func (c *Client) fetchContributionWindow(
}
var resp contributionYearGQL
if err := c.query(ctx, contributionYearQuery, vars, &resp); err != nil {
return err
return false, err
}
if resp.User == nil {
// Don't abort the run — other windows may still yield data — but
@@ -112,28 +147,33 @@ func (c *Client) fetchContributionWindow(
// 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
return false, 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 foldCalendar {
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,
})
}
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 {
saturated := len(cc.CommitContributionsByRepository) >= maxRepositoriesPerWindow
if saturated && !foldCalendar {
// A month at the ceiling has nowhere finer to go, so this is as
// complete as the API will answer. Say so instead of implying the
// list covers everything.
fmt.Fprintf(os.Stderr,
"warn: contributions %s..%s hit the %d-repo ceiling; some repos are missing from commit probing\n",
"warn: contributions %s..%s hit the %d-repo ceiling even at month granularity; some repos are missing from commit probing\n",
from.Format("2006-01-02"), to.Format("2006-01-02"), maxRepositoriesPerWindow)
}
@@ -153,5 +193,5 @@ func (c *Client) fetchContributionWindow(
seen[key] = len(p.SeedRepos)
p.SeedRepos = append(p.SeedRepos, info)
}
return nil
return saturated, nil
}