mirror of
https://github.com/tiennm99/ghglance.git
synced 2026-10-11 16:12:56 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c081ac591 | ||
|
|
9e0387f307 |
No files matched your search
@@ -0,0 +1,52 @@
|
||||
# ghstats
|
||||
|
||||
Single-binary Go CLI that renders GitHub profile cards as SVG, wrapped by a
|
||||
published GitHub Action. `main.go` parses flags → `internal/github` fetches →
|
||||
`internal/card` renders → `internal/theme` supplies palettes.
|
||||
|
||||
## Adding or changing a config knob
|
||||
|
||||
A new flag, input, or default is not done when the code compiles. Every knob
|
||||
has a surface in five places, and a change that lands in some but not all of
|
||||
them ships a half-wired feature or a README that lies:
|
||||
|
||||
1. `main.go` — the CLI flag
|
||||
2. `action.yml` — the matching Action input and its default
|
||||
3. `entrypoint.sh` — the `INPUT_*` → flag translation
|
||||
4. `README.md` — **both** reference tables (Action inputs, CLI flags) **and
|
||||
the runnable examples**: the workflow YAML under "Use as a GitHub Action"
|
||||
and the `ghstats …` command under "Use as a CLI"
|
||||
5. `docs/system-architecture.md` — when the knob changes the fetch pipeline
|
||||
or its query cost
|
||||
|
||||
The examples are the step most often missed, and they are what people copy.
|
||||
Grep the flag name across the repo before calling the change complete; every
|
||||
hit that is a reference table or example should mention it.
|
||||
|
||||
## Behavior changes are user-visible
|
||||
|
||||
The Action is published to the Marketplace, so existing callers get whatever
|
||||
`@v1` points at. Default-off for anything that moves numbers on already-
|
||||
rendered cards, and say so in the input description. Releases are tag-driven:
|
||||
push `vX.Y.Z`, and `release.yml` tests, builds, and force-moves the floating
|
||||
`v1` tag.
|
||||
|
||||
## Cards and the demo gallery
|
||||
|
||||
Adding a card means updating `internal/card/card.go`, the card table in
|
||||
`README.md`, and the layout in `.github/workflows/demo.yml` — the demo
|
||||
generator embeds a hardcoded list of SVGs, so a new card renders into every
|
||||
theme directory but stays invisible in the gallery until it is added there.
|
||||
Per-theme pages mirror the table layout in the author's profile README at
|
||||
`tiennm99/tiennm99`.
|
||||
|
||||
## GitHub API constraints worth remembering
|
||||
|
||||
- `commitContributionsByRepository` caps at 100 repos per query and truncates
|
||||
silently. Contribution years are queried by quarter, and a saturated quarter
|
||||
is re-asked by month. Only widen windows with that ceiling in mind.
|
||||
- `contributionCalendar` is clipped to the exact `from`/`to`, with no
|
||||
week-boundary spillover, so disjoint windows never double-count days.
|
||||
- Test files named `*_windows_test.go` (or any GOOS/GOARCH suffix) are
|
||||
silently excluded from the build on other platforms — `go test` reports
|
||||
"ok" while running nothing.
|
||||
@@ -86,6 +86,7 @@ jobs:
|
||||
tz: Asia/Saigon
|
||||
include_forks: "true"
|
||||
include_private: "true"
|
||||
include_org_repos: "false" # "true" also counts org repos you administer (token needs read:org)
|
||||
commit_changes: "true"
|
||||
```
|
||||
|
||||
@@ -121,7 +122,7 @@ Then embed the cards in your `README.md`:
|
||||
| `tz` | `UTC` | IANA tz for the productive-time card (e.g. `Asia/Saigon`) |
|
||||
| `start_of_week` | `sunday` | First day of week for heatmap rows and weekday bars (`sunday`…`saturday`) |
|
||||
| `top_repos` | `0` | Optional cap on seed repos probed for commit history (`0` = unlimited) |
|
||||
| `commits_per_repo` | `500` | Max commits sampled per repo (covers last-year and all-time aggregates) |
|
||||
| `commits_per_repo` | `500` | Max commits sampled per repo, `0` = every commit (covers last-year and all-time aggregates) |
|
||||
| `include_forks` | `true` | Include forked repos in stats and commit probing |
|
||||
| `include_private` | `true` | Include private repos (requires PAT with `repo` scope; silently no-op otherwise) |
|
||||
| `include_org_repos`| `false` | Count org-owned repos you administer toward stars, repo count, languages, top-starred (needs `read:org`) |
|
||||
@@ -152,6 +153,12 @@ export GITHUB_TOKEN=ghp_xxx
|
||||
ghstats -user tiennm99 -themes dracula,github_dark -tz Asia/Saigon -out output
|
||||
```
|
||||
|
||||
Add `-include-org-repos` to also count org-owned repos you administer:
|
||||
|
||||
```sh
|
||||
ghstats -user tiennm99 -themes dracula -include-org-repos -out output
|
||||
```
|
||||
|
||||
| Flag | Default | Description |
|
||||
| ------------------- | --------------- | ---------------------------------------------------------------------- |
|
||||
| `-user` | *(required)* | GitHub username |
|
||||
@@ -161,7 +168,7 @@ ghstats -user tiennm99 -themes dracula,github_dark -tz Asia/Saigon -out output
|
||||
| `-tz` | `Local` | IANA timezone for productive-time cards |
|
||||
| `-start-of-week` | `sunday` | First day of week for heatmap rows and weekday bars (`sunday`…`saturday`) |
|
||||
| `-top-repos` | `0` | Optional cap on seed repos probed (`0` = unlimited) |
|
||||
| `-commits-per-repo` | `500` | Max commits sampled per repo |
|
||||
| `-commits-per-repo` | `500` | Max commits sampled per repo, `0` = every commit |
|
||||
| `-include-forks` | `true` | Include forked repos in the stats |
|
||||
| `-include-private` | `true` | Include private repos (requires `repo` PAT scope; silently no-op otherwise) |
|
||||
| `-include-org-repos`| `false` | Count org-owned repos you administer toward stars, repo count, languages, top-starred |
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ inputs:
|
||||
required: false
|
||||
default: "0"
|
||||
commits_per_repo:
|
||||
description: Max commits sampled per repo (covers last-year and all-time aggregates)
|
||||
description: Max commits sampled per repo, `0` for every commit (covers last-year and all-time aggregates)
|
||||
required: false
|
||||
default: "500"
|
||||
include_forks:
|
||||
|
||||
@@ -49,7 +49,7 @@ FetchContributionsAllTime(ctx, profile, opts)
|
||||
│ TotalCommitsAllTime
|
||||
│
|
||||
▼
|
||||
FetchProductive(ctx, profile, profile.SeedRepos, loc, commitsPerRepo)
|
||||
FetchProductive(ctx, profile, profile.SeedRepos, loc, commitsPerRepo) // 0 = no cap
|
||||
│ commitHistoryQuery × (#seeds × pages)
|
||||
│ per commit: t = committedDate in loc
|
||||
│ ProductiveAllTime[t.Hour]++
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package github
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestReachedCommitCap(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
seen int
|
||||
maxPerRepo int
|
||||
want bool
|
||||
}{
|
||||
{"under the cap", 100, 500, false},
|
||||
{"at the cap", 500, 500, true},
|
||||
{"past the cap", 700, 500, true},
|
||||
// Zero means unlimited, so even a fresh repo keeps paginating. The
|
||||
// old behavior stopped here and rendered empty commit-derived cards.
|
||||
{"zero cap, nothing seen yet", 0, 0, false},
|
||||
{"zero cap, deep into history", 100_000, 0, false},
|
||||
{"negative cap treated as unlimited", 10, -1, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := reachedCommitCap(tc.seen, tc.maxPerRepo); got != tc.want {
|
||||
t.Errorf("%s: reachedCommitCap(%d, %d) = %v, want %v",
|
||||
tc.name, tc.seen, tc.maxPerRepo, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,21 @@ type productiveGQL struct {
|
||||
// magnitude is irrelevant because the card renders percentages.
|
||||
const scaleFactor = 10_000
|
||||
|
||||
// reachedCommitCap reports whether a repo has given up enough commits to stop
|
||||
// paginating. A cap of zero or less means no cap — keep going until the
|
||||
// history runs out — matching how -top-repos treats zero. Without the guard a
|
||||
// zero cap would stop before the first page and quietly empty every
|
||||
// commit-derived card.
|
||||
func reachedCommitCap(seen, maxPerRepo int) bool {
|
||||
if maxPerRepo <= 0 {
|
||||
return false
|
||||
}
|
||||
return seen >= maxPerRepo
|
||||
}
|
||||
|
||||
// FetchProductive paginates the default-branch commit history (authored by
|
||||
// the target user) for each repo up to maxPerRepo commits, and fills two
|
||||
// parallel sets of aggregates on the Profile:
|
||||
// the target user) for each repo up to maxPerRepo commits (0 = all), and
|
||||
// fills two parallel sets of aggregates on the Profile:
|
||||
//
|
||||
// - Last-year: p.Productive (24h histogram) and p.CommitsByLanguage
|
||||
// - All-time: p.ProductiveAllTime and p.CommitsByLanguageAllTime
|
||||
@@ -62,7 +74,7 @@ func (c *Client) FetchProductive(ctx context.Context, p *Profile, repos []RepoIn
|
||||
var cursor *string
|
||||
seen := 0
|
||||
for {
|
||||
if seen >= maxPerRepo {
|
||||
if reachedCommitCap(seen, maxPerRepo) {
|
||||
break
|
||||
}
|
||||
owner := repo.Owner
|
||||
|
||||
@@ -24,7 +24,7 @@ func main() {
|
||||
themesFlag = flag.String("themes", "dracula", "comma-separated theme ids, or 'all'")
|
||||
tzName = flag.String("tz", "Local", "timezone for productive-time card (IANA name, e.g. Asia/Saigon)")
|
||||
topRepos = flag.Int("top-repos", 0, "optional cap on seed repos probed for commit history (0 = unlimited)")
|
||||
perRepo = flag.Int("commits-per-repo", 500, "max commits sampled per repo (covers both last-year and all-time aggregates)")
|
||||
perRepo = flag.Int("commits-per-repo", 500, "max commits sampled per repo, 0 = every commit (covers both last-year and all-time aggregates)")
|
||||
includeForks = flag.Bool("include-forks", true, "include forked repos in stats and commit probing")
|
||||
includePrivate = flag.Bool("include-private", true, "include private repos (requires PAT with repo scope; silently no-op otherwise)")
|
||||
includeOrgs = flag.Bool("include-org-repos", false, "count org-owned repos you administer toward stars, repo count, repos-per-language and top-starred")
|
||||
|
||||
Reference in new issue
Block a user