Go言語でGitHub統計ツールを作って学んだこと
こんにちは!今回、Go言語でGitHub活動を可視化するCLIツール「ghstat」を開発したので、その過程で学んだことをシェアします。
なぜghstatを作ったのか?
自分のGitHub活動を振り返りたいと思ったんですが、GitHub標準のグラフだけだと物足りない。「今月何コミットした?」「どの言語をよく使ってる?」「連続コミット記録は?」みたいな統計を、ターミナルでサクッと見たいと思ったのがきっかけです。
あと、前回画像処理ツール「imgai」を作ってGoの基礎は学んだので、次はAPI統合に挑戦したかった。
完成したもの
こんな感じのCLIツールができました:
# ユーザー情報表示
$ ghstat user
👤 GitHub User Information
─────────────────────────────────────
Username: @hiroki-abe-58
Name: Hiroki Abe
Repositories: 25
Followers: 42
Following: 18
# コミット統計
$ ghstat commits --days 30
📊 Analyzing commits for @hiroki-abe-58 (last 30 days)...
📈 Commit Summary
─────────────────────────────────────
Total Commits: 156
Period: 30 days
Average/Day: 5.2
🏆 Top Repositories
─────────────────────────────────────
1. imgai 45 ████████████████████
2. ghstat 32 ██████████████░░░░░░
3. notes-app 28 ████████████░░░░░░░░
# 言語統計
$ ghstat languages
🌍 Language Statistics
─────────────────────────────────────
Total Repositories: 25
With Language Info: 23
1. Go 8 repos 32.0% ██████████░░░░░░░░░░░░░░░░░░░░
2. Python 6 repos 24.0% ████████░░░░░░░░░░░░░░░░░░░░░░
3. JavaScript 5 repos 20.0% ██████░░░░░░░░░░░░░░░░░░░░░░░░
# 連続コミット日数
$ ghstat streak
🔥 Commit Streak Statistics
─────────────────────────────────────
Current Streak: 15 days
Longest Streak: 28 days
Total Commits: 487 (last 365 days)
📅 Last 30 Days
─────────────────────────────────────
Mon Tue Wed Thu Fri Sat Sun
W44 ▪ █ ▪ ░ █ ▪ ▪
W45 █ ▪ █ ▪ ░ ▪ █
W46 ▪ █ ▪ ▪ █ ░ ▪
W47 █ ▪ █ █ ▪ ░ ░
░ = 0 ▪ = 1-9 █ = 10+
🎉 Keep it up! You're on a 15-day streak!
見やすい! これがターミナルで瞬時に見られるのが最高です。
プロジェクト構成
ghstat/
├── cmd/ # CLIコマンド
│ ├── root.go # ルートコマンド
│ ├── user.go # ユーザー情報
│ ├── commits.go # コミット統計
│ ├── languages.go # 言語統計
│ ├── streak.go # 連続コミット日数
│ └── repos.go # リポジトリ一覧
├── pkg/ # コアロジック
│ ├── github/ # GitHub API クライアント
│ ├── stats/ # 統計計算
│ ├── display/ # 表示フォーマット
│ └── config/ # 設定管理
└── main.go
前回のimgaiと同じく、cmd(UI層)とpkg(ロジック層)を分離する設計にしました。この分離、本当に開発しやすい。
技術スタック
使ったライブラリ
| ライブラリ | 用途 | 理由 |
|---|---|---|
| Cobra | CLI framework | imgaiでも使って慣れてた |
| go-github | GitHub API | 公式推奨ライブラリ |
| oauth2 | 認証 | GitHub API認証に必須 |
シンプルな構成ですが、これだけで十分強力なツールが作れます。
GitHub APIの使い方
1. 認証の実装
まずは認証。GitHub APIはPersonal Access Tokenを使います:
package github
import (
"context"
"os"
"github.com/google/go-github/v56/github"
"golang.org/x/oauth2"
)
type Client struct {
client *github.Client
ctx context.Context
}
func NewClient() (*Client, error) {
// 環境変数からトークン取得
token := os.Getenv("GITHUB_TOKEN")
if token == "" {
return nil, errors.New("GITHUB_TOKEN not set")
}
ctx := context.Background()
// OAuth2でトークンを設定
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
tc := oauth2.NewClient(ctx, ts)
// GitHub クライアント作成
client := github.NewClient(tc)
return &Client{
client: client,
ctx: ctx,
}, nil
}
ポイント:
- トークンは絶対にハードコードしない(環境変数で渡す)
-
context.Contextは必須(タイムアウト管理に使う) - OAuth2の仕組みを理解すると応用が効く
2. ユーザー情報の取得
func (c *Client) GetAuthenticatedUser() (*github.User, error) {
// 空文字列 = 認証済みユーザー自身
user, _, err := c.client.Users.Get(c.ctx, "")
if err != nil {
return nil, err
}
return user, nil
}
シンプル! go-githubライブラリのおかげで、たった3行でユーザー情報が取れます。
3. リポジトリ一覧の取得(ページネーション対応)
ここが少し複雑。GitHubは大量データをページングで返すので、全部取るにはループが必要:
func (c *Client) ListRepositories() ([]*Repository, error) {
opt := &github.RepositoryListOptions{
ListOptions: github.ListOptions{PerPage: 100}, // 1ページ100件
Sort: "updated",
Direction: "desc",
}
var allRepos []*github.Repository
// ページネーションループ
for {
repos, resp, err := c.client.Repositories.List(c.ctx, "", opt)
if err != nil {
return nil, err
}
allRepos = append(allRepos, repos...)
// 次のページがなければ終了
if resp.NextPage == 0 {
break
}
opt.Page = resp.NextPage
}
return convertToOurStruct(allRepos), nil
}
ポイント:
-
PerPage: 100- 1回で最大100件取得 -
resp.NextPage- 次のページ番号(0なら最終ページ) - 無限ループに注意!(必ず終了条件を入れる)
4. コミット統計の取得
これが一番面倒でした。リポジトリごとにコミット履歴を取得して集計する必要があります:
func (c *Client) GetCommitStats(username string, days int) (*CommitStats, error) {
stats := &CommitStats{
ByDate: make(map[string]int),
ByRepo: make(map[string]int),
}
// 日付範囲を計算
since := time.Now().AddDate(0, 0, -days)
// 全リポジトリ取得
repos, err := c.ListRepositories()
if err != nil {
return nil, err
}
// 各リポジトリのコミットを取得
for _, repo := range repos {
commits, err := c.getRepoCommits(username, repo.Name, since)
if err != nil {
// アクセスできないリポジトリはスキップ
continue
}
for _, commit := range commits {
date := commit.Commit.Author.Date.Format("2006-01-02")
stats.ByDate[date]++
stats.ByRepo[repo.Name]++
stats.TotalCommits++
}
}
return stats, nil
}
苦労したポイント:
- アクセス権限 - Privateリポジトリはトークンの権限次第でエラーになる
- API制限 - 5000リクエスト/時間の制限がある
- 処理時間 - リポジトリが多いと数分かかる
解決策:
- エラーは無視してスキップ(
continue) - プログレスバーで進捗表示
- 結果をキャッシュ(将来の改善ポイント)
データ可視化のテクニック
ターミナルでグラフを描くのは楽しかった!
1. シンプルなバーチャート
func CreateBar(percentage, maxWidth int) string {
if percentage > 100 {
percentage = 100
}
filled := (percentage * maxWidth) / 100
bar := ""
for i := 0; i < maxWidth; i++ {
if i < filled {
bar += "█" // 塗りつぶし
} else {
bar += "░" // 空白
}
}
return bar
}
使用例:
Go 8 repos 32.0% ██████████░░░░░░░░░░░░░░░░░░░░
Python 6 repos 24.0% ████████░░░░░░░░░░░░░░░░░░░░░░
2. 日別コミットカレンダー
func displayStreakCalendar(byDate map[string]int, days int) {
fmt.Println(" Mon Tue Wed Thu Fri Sat Sun")
startDate := today.AddDate(0, 0, -days+1)
// 月曜日に揃える
for startDate.Weekday() != time.Monday {
startDate = startDate.AddDate(0, 0, -1)
}
week := ""
for d := startDate; !d.After(today); d = d.AddDate(0, 0, 1) {
commits := byDate[d.Format("2006-01-02")]
// コミット数でシンボルを変える
var symbol string
if commits == 0 {
symbol = " ░"
} else if commits < 5 {
symbol = " ▪"
} else {
symbol = " █"
}
week += symbol
// 日曜日で改行
if d.Weekday() == time.Sunday {
fmt.Println(week)
week = ""
}
}
}
出力:
Mon Tue Wed Thu Fri Sat Sun
W44 ▪ █ ▪ ░ █ ▪ ▪
W45 █ ▪ █ ▪ ░ ▪ █
GitHubのコントリビューショングラフっぽくなって、めちゃくちゃ気に入ってます。
開発で学んだこと
1. GitHub API制限との戦い
問題: リポジトリが多いと、API制限に引っかかる。
解決策:
// レート制限をチェック
func (c *Client) CheckRateLimit() error {
rate, _, err := c.client.RateLimits(c.ctx)
if err != nil {
return err
}
remaining := rate.Core.Remaining
if remaining < 100 {
resetTime := rate.Core.Reset.Time
return fmt.Errorf("API limit low (%d remaining), resets at %v",
remaining, resetTime)
}
return nil
}
2. nil ポインタの恐怖
go-githubはすべてポインタを返します。nil チェックを忘れると...パニック!
悪い例:
user, _, _ := client.Users.Get(ctx, "")
fmt.Println(*user.Name) // パニック!(Nameがnilの可能性)
良い例:
user, _, _ := client.Users.Get(ctx, "")
if user.Name != nil {
fmt.Println(*user.Name)
}
学び: GitHub APIのレスポンスは、すべての値がnilの可能性がある。必ずチェックすること!
3. 統計計算の分離
最初はコマンド内に統計ロジックを書いてたんですが、コードが汚くなりすぎて...
Before:
// cmd/commits.go に統計ロジックがベタ書き
func runCommits() {
// 100行のロジック...
total := 0
for _, count := range byRepo {
total += count
}
avg := float64(total) / float64(days)
// さらに続く...
}
After:
// pkg/stats/ に分離
func CalculateAverage(total, days int) float64 {
if days == 0 {
return 0
}
return float64(total) / float64(days)
}
// cmd/commits.go はスッキリ
func runCommits() {
avg := stats.CalculateAverage(total, days)
fmt.Printf("Average: %.1f\n", avg)
}
学び: ビジネスロジックとUI層は分離すると、テストも書きやすくなるし、再利用もできる。
4. 設定ファイルの実装
毎回環境変数を設定するの面倒だったので、設定ファイル機能を追加:
// ~/.config/ghstat/config.json
{
"github_token": "ghp_xxxxx",
"default_days": 30,
"default_limit": 20
}
package config
func Load() (*Config, error) {
home, _ := os.UserHomeDir()
configPath := filepath.Join(home, ".config", "ghstat", "config.json")
data, err := os.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
return DefaultConfig(), nil // ファイルがなければデフォルト
}
return nil, err
}
var cfg Config
json.Unmarshal(data, &cfg)
return &cfg, nil
}
ポイント:
- 設定ファイルはオプション(なくてもデフォルト値で動く)
- 環境変数が優先(
GITHUB_TOKEN> config.json) -
~/.config/配下に保存(Linux/macOSの慣習)
パフォーマンスの最適化
最初は遅かった(100リポジトリで5分とか)。改善したこと:
1. 並列処理
// リポジトリごとに並列でコミット取得
var wg sync.WaitGroup
results := make(chan *RepoCommits, len(repos))
for _, repo := range repos {
wg.Add(1)
go func(r *Repository) {
defer wg.Done()
commits := getRepoCommits(r)
results <- commits
}(repo)
}
wg.Wait()
close(results)
結果: 5分 → 30秒に短縮!
2. ページネーションの最適化
// 1ページ100件(デフォルト30件)
opt := &github.ListOptions{PerPage: 100}
結果: API呼び出し回数が1/3に。
つまずいたポイント
1. 時刻のフォーマット
Goの時刻フォーマットは独特:
// ❌ 間違い(Pythonっぽく書くとエラー)
date.Format("%Y-%m-%d")
// ✅ 正しい(2006-01-02は「リファレンス日時」)
date.Format("2006-01-02")
なんで2006-01-02?
- Go誕生日(2006年1月2日)がリファレンス
- 覚え方:
01/02 03:04:05 PM '06 -0700= 1,2,3,4,5,6,7
2. mapの初期化忘れ
var stats CommitStats
stats.ByDate["2024-11-09"]++ // パニック!mapがnil
正しい初期化:
stats := &CommitStats{
ByDate: make(map[string]int), // 必須!
}
まとめ
ghstatを作って学んだこと:
技術面:
- ✅ GitHub API v3の使い方
- ✅ OAuth2認証の仕組み
- ✅ ページネーション処理
- ✅ Goでの並行処理(goroutine)
- ✅ ターミナルでのデータ可視化
設計面:
- ✅ API層とビジネスロジックの分離
- ✅ 統計計算ロジックの独立
- ✅ 表示フォーマットの共通化
- ✅ エラーハンドリングの統一
実用面:
- ✅ 自分で毎日使うツールができた
- ✅ GitHub活動を数値で把握できるようになった
- ✅ モチベーション管理に役立ってる
次の改善予定
- キャッシュ機能(処理速度改善)
- 週次/月次レポート機能
- HTML出力機能
- GitHub Actions連携
- チーム統計機能
ターミナルで動くツールって、作るのも使うのも楽しいですね。次は何作ろうかな?
それでは、Happy Coding!