Files
ghstats/docs/codebase-summary.md
T
tiennm99 e08bf6316b chore: drop tracked output/ sample in favor of CI-built demo gallery (#4)
The dracula sample under output/dracula/ was a reference render for the
README. Now that demo/ auto-generates every card × every theme on each
push, the sample is redundant — and keeping it trackers means every
behavioral tweak shows up as a diff in committed SVGs.

- Delete output/dracula/*.svg.
- .gitignore: collapse `output/*` + `!output/dracula/` to a plain `output/`.
- demo.yml: drop `output/**` from paths-ignore (no longer part of any push).
- README / docs: point readers at demo/ instead of output/dracula/.
2026-04-19 09:03:44 +07:00

118 lines
6.8 KiB
Markdown
Raw 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 histogram + 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, header, footer
│ │ ├── axis.go # niceTicks (d3-style 1/2/5 × 10^k), formatTick
│ │ ├── 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)
│ │ ├── 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 — every card × every theme + README
# (`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`, `CommitsByLanguage`, `ProductiveAllTime`, `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 cards)
- `renderContributions` — smooth area chart (used by both contributions cards)
- `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 + CommitsByLanguage (+ AllTime variants)
14 SVG files per theme
```
## Test coverage
- `internal/card/card_test.go``RenderAll` produces 14 valid SVGs; XML escape through real render pipeline; `formatInt` cases; `TestDonutSingleSlice` (guards the empty-arc regression); `TestDonutEmpty` (no-data fallback).
- `internal/github/profile_test.go``sortLangStats` ordering and tiebreak.
- `main_test.go``TestUTCOffsetLabel` covers UTC, Asia/Saigon, half-hour (Kolkata), quarter-hour (Kathmandu) zones.
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`).