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

124 lines
7.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```go
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.go``RenderAll` 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.go``TestPadToWeekGridRotatesByWeekStart` (leading pad matches configured start day), `TestRenderWeekdayRespectsWeekStart` (bar order rotates with `WeekStart`), `TestRenderHeatmapLabelsRespectWeekStart` (heatmap row labels rotate accordingly).
- `internal/github/profile_test.go``sortLangStats` ordering and tiebreak.
- `main_test.go``TestUTCOffsetLabel` 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`).