Files
ghstats/docs/codebase-summary.md
tiennm99 1763422570 feat(card): configurable start of week (#25)
* feat(card): configurable start of week for heatmap + weekday cards

New -start-of-week CLI flag (and start_of_week action input) rotates the
contribution-heatmap rows and the productive-weekday bars so users whose
calendars start on Monday (or any other day) get matching output. Default
stays Sunday to preserve existing renders.

* docs: note start-of-week in design-guidelines, codebase-summary, deployment-guide

- design-guidelines: heatmap row order + productive-weekday bar order now derive from Profile.WeekStart
- codebase-summary: list new weekday_start_test.go cases + TestParseWeekday
- deployment-guide: mention start_of_week as an optional action input in the workflow template
2026-04-21 20:57:21 +07:00

7.9 KiB
Raw Permalink Blame History

Codebase Summary

Layout

ghstats/
├── main.go                              # CLI entry point; wires flags → fetchers → renderers
├── action.yml                           # GitHub Action metadata
├── entrypoint.sh                        # Action runtime; maps INPUT_* env → CLI flags
├── Dockerfile                           # Multi-stage build for the Action image
├── go.mod                               # Module declaration; no external deps
├── internal/
│   ├── github/                          # GraphQL client + fetchers + models
│   │   ├── client.go                    # HTTP POST to /graphql, error decoding
│   │   ├── queries.go                   # profileQuery, commitHistoryQuery, contributionYearQuery
│   │   ├── model.go                     # Profile, RepoInfo, LangStat, LangEdge, DailyContribution
│   │   ├── profile.go                   # FetchProfile — user + owned repos + stats + calendar
│   │   ├── productive.go                # FetchProductive — commit history → hour + weekday histograms + lang buckets
│   │   ├── contributions_all_time.go    # FetchContributionsAllTime — per-year loop → seed list + daily series
│   │   └── profile_test.go              # sortLangStats tiebreak
│   ├── card/                            # SVG renderers; one file per card
│   │   ├── card.go                      # Card interface, RenderAll, allCards slice
│   │   ├── svg.go                       # escapeXML, formatInt, truncate, header (auto-fit title), footer
│   │   ├── axis.go                      # niceTicks (d3-style, last tick ≥ max), formatTick (1500→"1.5k")
│   │   ├── icons.go                     # Octicon path strings
│   │   ├── profile.go                   # profile-details
│   │   ├── repos_per_language.go        # repos-per-language
│   │   ├── most_commit_language.go      # most-commit-language
│   │   ├── most_commit_language_all_time.go  # most-commit-language-all-time
│   │   ├── stats.go                     # stats
│   │   ├── productive.go                # productive-time (+ all-time)
│   │   ├── productive_weekday.go        # productive-weekday (+ all-time)
│   │   ├── contributions.go             # contributions (+ all-time)
│   │   ├── contributions_heatmap.go     # contributions-heatmap (7×53 calendar grid; row 0 = Profile.WeekStart)
│   │   ├── contributions_by_year.go     # contributions-by-year bar chart
│   │   ├── streak.go                    # streak (current/longest/active days)
│   │   ├── top_starred_repos.go         # top-starred-repos bar list
│   │   ├── donut_chart.go               # renderDonutCard — shared by language cards
│   │   └── card_test.go                 # Rendering + escape + format tests
│   └── theme/
│       └── theme.go                     # 65-palette map ported from github-profile-summary-cards
├── .github/workflows/
│   ├── ci.yml                           # go vet + go test on push/PR
│   ├── release.yml                      # GHCR image + cross-platform binaries on tag
│   └── demo.yml                         # Renders every theme for the repo owner on push to main
├── docs/                                # This directory
├── plans/                               # Research reports + implementation plans
└── demo/                                # Auto-generated gallery
    ├── README.md                         # Lightweight index (links only, zero images)
    └── <theme>/                          # Per-theme page: 15 SVGs + README pairing LY / AT variants
                                          # (`output/` is entirely gitignored; see demo/ for reference renders)

Module responsibilities

internal/github

All network I/O. Exposes a *Client with three fetchers; every call takes a context.Context so pagination aborts cleanly on timeout or Ctrl-C:

Fetcher Input Populates
FetchProfile(ctx, login, opts) username, visibility flags Profile basics, totals, owned-repos aggregation, last-year daily calendar, TopRepos
FetchContributionsAllTime(ctx, p, opts) Profile SeedRepos, DailyContributionsAllTime, TotalCommitsAllTime
FetchProductive(ctx, p, repos, loc, cap) Profile + seed + tz + cap Productive, Weekday, CommitsByLanguage, ProductiveAllTime, WeekdayAllTime, CommitsByLanguageAllTime

Call order in main.go: Profile → AllTime → Productive. Client.query handles GitHub rate limits transparently — on 429 or 403 with X-RateLimit-Remaining: 0, it honors Retry-After / X-RateLimit-Reset (capped at 5 minutes) and retries once.

internal/card

Pure rendering. Every card implements the Card interface:

type Card interface {
    Filename() string
    SVG(*github.Profile, theme.Theme) ([]byte, error)
}

RenderAll iterates allCards, writes each to <outDir>/<themeID>/<Filename>.

Shared helpers:

  • renderDonutCard — language donut + legend (used by 3 language cards)
  • renderProductiveTime — 24h bar chart (used by both productive-time cards)
  • renderWeekday — 7-bar day-of-week chart (used by both productive-weekday cards)
  • renderContributions — smooth area chart (used by both contributions cards)
  • renderHeatmap — 7×N calendar grid with mixHex-derived intensity ramp
  • mixHex / parseHex#rrggbb blending (used by heatmap, by-year, weekday for peak-vs-dim bars)
  • header, footer — SVG chrome
  • niceTicks, formatTick — axis math

internal/theme

Static map of 65 themes. Each theme specifies title/text/background/stroke/accent/muted plus StrokeOpacity for correct light-theme borders.

Card ↔ data flow

profileQuery ─────► Profile.{identity, owned repos, totals, last-year calendar}
                                │
contributionYearQuery ─┬──► SeedRepos + DailyContributionsAllTime + TotalCommitsAllTime
                       │
                       └─ seed into ─►
                                │
commitHistoryQuery ──► Productive + Weekday + CommitsByLanguage (+ AllTime variants)
                                │
                                ▼
                          15 SVG files per theme

Test coverage

  • internal/card/card_test.goRenderAll produces 15 valid SVGs; XML escape through real render pipeline; formatInt cases; TestDonutSingleSlice / TestDonutEmpty (donut edge cases); TestCardsFitFrame (renders every card against an adversarial profile and asserts text + coordinates stay in the 340×200 frame); TestFitTitleFontSize (pins the auto-shrink table for every real title); TestNiceTicksCoversMax (guards the yMax ≥ dataMax invariant so bars can't overflow chartH).
  • internal/card/weekday_start_test.goTestPadToWeekGridRotatesByWeekStart (leading pad matches configured start day), TestRenderWeekdayRespectsWeekStart (bar order rotates with WeekStart), TestRenderHeatmapLabelsRespectWeekStart (heatmap row labels rotate accordingly).
  • internal/github/profile_test.gosortLangStats ordering and tiebreak.
  • main_test.goTestUTCOffsetLabel covers UTC, Asia/Saigon, half-hour (Kolkata), quarter-hour (Kathmandu) zones. TestParseWeekday covers the -start-of-week input parsing.

No network-touching tests; real runs verified via -token + local build.

Naming conventions

  • Go files use snake_case for multi-word names (repos_per_language.go, contributions_all_time.go).
  • Cards' Filename() returns a plain kebab-case name (profile-details.svg, most-commit-language-all-time.svg, …). Embedders reference by name, so no numeric prefix is needed.
  • Themes in snake_case to match upstream (github_dark, nord_bright).