feat: コミットレポートツール(コミネコ)初期コミット
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
---
|
||||
name: github-api
|
||||
description: GitHub REST APIの使い方。コミット取得、差分取得、リポジトリ情報取得に使用。
|
||||
allowed-tools: Bash(node:*), Bash(gh:*)
|
||||
---
|
||||
|
||||
# GitHub API ガイド
|
||||
|
||||
GitHub REST API の使用方法を説明します。
|
||||
|
||||
## ⚠️ 重要: コミット取得はスクリプトを使用
|
||||
|
||||
**前日のコミット取得は必ず以下のスクリプトを使用してください。**
|
||||
日付計算を手動で行わないでください。
|
||||
|
||||
### 全ブランチ一括取得(推奨)
|
||||
|
||||
```bash
|
||||
# ⚠️ 必ず --output オプションを使用(シェルリダイレクトは使わない)
|
||||
node .claude/skills/github-api/scripts/get-all-branch-commits.js <owner> <repo> --output /tmp/all-commits.json
|
||||
```
|
||||
|
||||
全ブランチのコミットを一括で取得します。daily-reportではこちらを使用してください。
|
||||
|
||||
**重要**: `--output` オプションを使用してファイルに直接書き込んでください。
|
||||
シェルリダイレクト(`> file`)は stdout/stderr が混在する問題があります。
|
||||
|
||||
**最適化版**: GraphQLで最近アクティブなブランチのみを抽出してからコミット取得。
|
||||
309ブランチ → 約20ブランチに絞り込み(約90%のAPI呼び出し削減)。
|
||||
|
||||
環境変数 `ACTIVE_DAYS` でアクティブ判定期間を変更可能(デフォルト: 7日)。
|
||||
|
||||
### パスフィルタリング
|
||||
|
||||
```bash
|
||||
node .claude/skills/github-api/scripts/filter-commits-by-path.js \
|
||||
--owner <owner> --repo <repo> --paths "app/,web/,supabase/"
|
||||
```
|
||||
|
||||
`get-all-branch-commits.js` の出力を受け取り、指定パスに関連するコミットのみを抽出します。
|
||||
各コミットの変更ファイルを GitHub API で取得し、パスマッチングを行います。
|
||||
|
||||
**⚠️ 重要: コミットのパスフィルタリングは必ずこのスクリプトを使用してください。**
|
||||
アドホックなフィルタリングコードは全件処理を保証できません。
|
||||
|
||||
| 引数 | 必須 | 説明 |
|
||||
|------|------|------|
|
||||
| `--input <file>` | No | 入力ファイル(省略時stdin) |
|
||||
| `--owner <name>` | Yes | リポジトリオーナー |
|
||||
| `--repo <name>` | Yes | リポジトリ名 |
|
||||
| `--paths <list>` | Yes | カンマ区切りパス(例: "app/,web/") |
|
||||
| `--concurrency <n>` | No | 並列数(デフォルト: 5) |
|
||||
|
||||
使用例:
|
||||
```bash
|
||||
# パイプライン使用
|
||||
node .claude/skills/github-api/scripts/get-all-branch-commits.js owner repo \
|
||||
| node .claude/skills/github-api/scripts/filter-commits-by-path.js \
|
||||
--owner owner --repo repo --paths "app/,web/,supabase/"
|
||||
|
||||
# ファイル入力
|
||||
node .claude/skills/github-api/scripts/filter-commits-by-path.js \
|
||||
--input /tmp/commits.json \
|
||||
--owner owner --repo repo --paths "app/,web/,supabase/"
|
||||
```
|
||||
|
||||
出力形式は入力と同じ構造 + 追加フィールド:
|
||||
- `metadata.filter_paths`: フィルタ対象パス
|
||||
- `metadata.original_commits`: フィルタ前のコミット数
|
||||
- `metadata.filtered_commits`: フィルタ後のコミット数
|
||||
- 各コミットに `matched_files` フィールド(マッチしたファイル情報)
|
||||
|
||||
### 単一ブランチ取得
|
||||
|
||||
```bash
|
||||
node .claude/skills/github-api/scripts/get-commits.js <owner> <repo> [branch]
|
||||
```
|
||||
|
||||
## 出力形式
|
||||
|
||||
スクリプトは正規化されたJSONを出力します。**jqでの追加処理は不要です。**
|
||||
|
||||
### 全ブランチ一括取得の出力
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"target_date": "2026-01-23",
|
||||
"start_utc": "2026-01-22T15:00:00Z",
|
||||
"end_utc": "2026-01-23T14:59:59Z",
|
||||
"total_branches": 308,
|
||||
"checked_branches": 18,
|
||||
"active_branches": 6,
|
||||
"total_commits": 31,
|
||||
"default_branch": "main",
|
||||
"active_days_filter": 7,
|
||||
"failed_branches": 0,
|
||||
"has_errors": false
|
||||
},
|
||||
"branches": {
|
||||
"main": {
|
||||
"commits": [...],
|
||||
"is_default": true
|
||||
},
|
||||
"feature-branch": {
|
||||
"commits": [...],
|
||||
"is_default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| フィールド | 説明 |
|
||||
|-----------|------|
|
||||
| `metadata.target_date` | 対象日(JST) |
|
||||
| `metadata.total_branches` | 全ブランチ数 |
|
||||
| `metadata.checked_branches` | チェックしたブランチ数(最適化後) |
|
||||
| `metadata.active_branches` | コミットがあるブランチ数 |
|
||||
| `metadata.default_branch` | デフォルトブランチ名 |
|
||||
| `metadata.active_days_filter` | アクティブ判定期間(日数) |
|
||||
| `metadata.failed_branches` | 取得失敗したブランチ数 |
|
||||
| `metadata.has_errors` | エラーがあったかどうか |
|
||||
| `branches[name].commits` | コミット配列 |
|
||||
| `branches[name].is_default` | デフォルトブランチかどうか |
|
||||
| `errors` | 失敗したブランチの詳細(エラー時のみ) |
|
||||
|
||||
### エラーハンドリング(自動リカバリ)
|
||||
|
||||
スクリプトは以下の自動リカバリ機能を持ちます:
|
||||
|
||||
1. **個別リトライ**: 各ブランチ取得は最大3回リトライ(指数バックオフ: 1秒、2秒、4秒)
|
||||
2. **一括再試行**: 1回目のループで失敗したブランチは、10秒後にまとめて再試行
|
||||
3. **確実な失敗検出**: 再試行でも失敗した場合、exit 1 で終了
|
||||
|
||||
**動作フロー:**
|
||||
```
|
||||
1回目のループ (308ブランチ)
|
||||
→ 各ブランチ最大3回リトライ
|
||||
→ 失敗: 2ブランチ
|
||||
|
||||
10秒待機
|
||||
|
||||
再試行ループ (2ブランチ)
|
||||
→ 各ブランチ最大3回リトライ
|
||||
→ 失敗: 0ブランチ → 正常終了 (exit 0)
|
||||
→ 失敗: 1ブランチ以上 → 異常終了 (exit 1)
|
||||
```
|
||||
|
||||
**GitHub Actionsでの動作:**
|
||||
- exit 1 → ワークフロー失敗として記録
|
||||
- 「Re-run jobs」で再実行可能
|
||||
- 一時的なAPI不安定は自動リカバリで解決する場合が多い
|
||||
|
||||
### 単一ブランチ取得の出力
|
||||
|
||||
### 保証される項目
|
||||
| フィールド | 説明 |
|
||||
|-----------|------|
|
||||
| `sha` | コミットハッシュ |
|
||||
| `message` | コミットメッセージ |
|
||||
| `date` | コミット日時(ISO 8601) |
|
||||
| `author.login` | ユーザー名(**必ず存在**) |
|
||||
| `author.avatar_url` | アバターURL(**必ず存在**) |
|
||||
| `html_url` | GitHubへのリンク |
|
||||
|
||||
### 出力例
|
||||
```json
|
||||
[
|
||||
{
|
||||
"sha": "abc1234567890",
|
||||
"message": "コミットメッセージ",
|
||||
"date": "2026-01-19T10:00:00Z",
|
||||
"author": {
|
||||
"login": "username",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/123?v=4"
|
||||
},
|
||||
"html_url": "https://github.com/..."
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### フォールバック
|
||||
GitHub APIで `author: null` の場合(メールがGitHubに紐付いていない):
|
||||
- `login`: コミッター名を使用
|
||||
- `avatar_url`: Gravatar identicon(メールハッシュから生成)
|
||||
|
||||
フォールバック発生時は標準エラーにログ出力:
|
||||
```
|
||||
[Avatar] フォールバック: Unknown User (8cca2ef)
|
||||
```
|
||||
|
||||
### 例
|
||||
```bash
|
||||
# your-username/your-web-app の main ブランチ
|
||||
node .claude/skills/github-api/scripts/get-commits.js your-username your-web-app main
|
||||
|
||||
# 出力例(標準エラー):
|
||||
# === 検索期間 ===
|
||||
# 対象日(JST): 2026-01-18
|
||||
# 開始(UTC): 2026-01-17T15:00:00Z
|
||||
# 終了(UTC): 2026-01-18T14:59:59Z
|
||||
# ブランチ: main
|
||||
# ================
|
||||
```
|
||||
|
||||
### 環境変数
|
||||
- `GH_TOKEN`: GitHub API認証トークン(必須)
|
||||
- `TARGET_DATE`: 対象日を指定(省略時は前日JST)形式: `YYYY-MM-DD`
|
||||
|
||||
## 禁止事項
|
||||
|
||||
❌ `date -u` や `date -d` で日付計算を手動実行
|
||||
❌ `since=$TODAY` のような変数を自分で定義
|
||||
❌ タイムゾーン変換を手動で計算
|
||||
|
||||
## 理由
|
||||
|
||||
日付計算は以下の問題が起きやすい:
|
||||
- `date -u` はUTCの「当日」を返す(JSTではない)
|
||||
- macOS と Linux で `date` のオプションが異なる
|
||||
- タイムゾーン変換の計算ミス
|
||||
|
||||
スクリプトはテスト済みで、毎回同じ結果を保証します。
|
||||
|
||||
---
|
||||
|
||||
## 認証
|
||||
|
||||
環境変数 `GH_TOKEN` を使用:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer $GH_TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"https://api.github.com/..."
|
||||
```
|
||||
|
||||
## エンドポイント
|
||||
|
||||
### コミット一覧取得
|
||||
|
||||
```
|
||||
GET /repos/{owner}/{repo}/commits
|
||||
```
|
||||
|
||||
パラメータ:
|
||||
- `since`: ISO 8601形式の日時(この日時以降のコミット)
|
||||
- `author`: 作者でフィルタ
|
||||
|
||||
レスポンス:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"sha": "abc1234567890",
|
||||
"commit": {
|
||||
"message": "コミットメッセージ",
|
||||
"author": {
|
||||
"name": "Author Name",
|
||||
"date": "2025-01-01T12:00:00Z"
|
||||
}
|
||||
},
|
||||
"author": {
|
||||
"login": "github-username",
|
||||
"id": 1234567,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1234567?v=4"
|
||||
},
|
||||
"html_url": "https://github.com/..."
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### ⚠️ 図解で使用する重要フィールド
|
||||
|
||||
HTML図解を生成する際、以下のフィールドを**必ず**APIレスポンスから取得すること:
|
||||
|
||||
| フィールド | 用途 | 注意 |
|
||||
|-----------|------|------|
|
||||
| `author.login` | ユーザー名表示 | サンプルの値をコピーしない |
|
||||
| `author.avatar_url` | アバター画像 | **必ずAPIから取得** |
|
||||
| `commit.author.date` | 日時表示 | ISO 8601形式 |
|
||||
|
||||
**禁止事項:**
|
||||
- サンプルHTMLのURLをそのまま使用
|
||||
- 架空のavatar_urlを生成
|
||||
- 他のユーザーのavatar_urlを流用
|
||||
|
||||
### コミット詳細取得(差分含む)
|
||||
|
||||
```
|
||||
GET /repos/{owner}/{repo}/commits/{sha}
|
||||
```
|
||||
|
||||
レスポンス:
|
||||
```json
|
||||
{
|
||||
"sha": "abc1234567890",
|
||||
"files": [
|
||||
{
|
||||
"filename": "src/index.ts",
|
||||
"status": "modified",
|
||||
"additions": 10,
|
||||
"deletions": 5,
|
||||
"patch": "@@ -1,5 +1,10 @@\n-old\n+new"
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"additions": 10,
|
||||
"deletions": 5,
|
||||
"total": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### リポジトリ情報取得
|
||||
|
||||
```
|
||||
GET /repos/{owner}/{repo}
|
||||
```
|
||||
|
||||
レスポンス:
|
||||
```json
|
||||
{
|
||||
"name": "repo-name",
|
||||
"description": "Repository description",
|
||||
"language": "TypeScript",
|
||||
"default_branch": "main"
|
||||
}
|
||||
```
|
||||
|
||||
### ブランチ一覧取得
|
||||
|
||||
```
|
||||
GET /repos/{owner}/{repo}/branches
|
||||
```
|
||||
|
||||
パラメータ:
|
||||
- `per_page`: 1ページあたりの件数(最大100)
|
||||
|
||||
例:
|
||||
```bash
|
||||
curl -H "Authorization: Bearer $GH_TOKEN" \
|
||||
"https://api.github.com/repos/owner/repo/branches?per_page=100"
|
||||
```
|
||||
|
||||
レスポンス:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "main",
|
||||
"commit": {
|
||||
"sha": "abc1234567890",
|
||||
"url": "https://api.github.com/repos/owner/repo/commits/abc1234567890"
|
||||
},
|
||||
"protected": true
|
||||
},
|
||||
{
|
||||
"name": "feature/video-player",
|
||||
"commit": {
|
||||
"sha": "def5678901234",
|
||||
"url": "https://api.github.com/repos/owner/repo/commits/def5678901234"
|
||||
},
|
||||
"protected": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### ブランチ別コミット取得
|
||||
|
||||
特定ブランチのコミットを取得するには `sha` パラメータにブランチ名を指定:
|
||||
|
||||
```
|
||||
GET /repos/{owner}/{repo}/commits?sha={branch_name}&since={date}
|
||||
```
|
||||
|
||||
**⚠️ 日付計算は必ずスクリプトを使用してください**(上記「禁止事項」参照)。
|
||||
|
||||
### ブランチの累計コミット取得
|
||||
|
||||
ブランチの履歴を把握するため、直近N件のコミットを取得:
|
||||
|
||||
```bash
|
||||
# 直近30件のコミットを取得(ブランチの概要把握用)
|
||||
curl -H "Authorization: Bearer $GH_TOKEN" \
|
||||
"https://api.github.com/repos/owner/repo/commits?sha=feature/video-player&per_page=30"
|
||||
```
|
||||
|
||||
**用途:**
|
||||
- ブランチの目的をAIで要約
|
||||
- 累計コミット数の表示
|
||||
- ブランチ開始日の特定(最古のコミット日時)
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
| ステータス | 意味 | 対処 |
|
||||
|-----------|------|------|
|
||||
| 401 | 認証エラー | GH_TOKEN を確認 |
|
||||
| 403 | レート制限 | 待機または認証確認 |
|
||||
| 404 | リポジトリ不在 | owner/repo を確認 |
|
||||
|
||||
## レート制限
|
||||
|
||||
- 認証済み: 5000リクエスト/時
|
||||
- `X-RateLimit-Remaining` ヘッダーで残り確認可能
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* コミットをパスでフィルタリングするスクリプト
|
||||
*
|
||||
* get-all-branch-commits.js の出力を受け取り、指定パスに関連するコミットのみを抽出。
|
||||
* 各コミットの変更ファイルを GitHub API で取得し、パスマッチングを行う。
|
||||
*
|
||||
* 使い方:
|
||||
* # パイプライン
|
||||
* node get-all-branch-commits.js owner repo | node filter-commits-by-path.js --owner owner --repo repo --paths "app/,web/"
|
||||
*
|
||||
* # ファイル入力
|
||||
* node filter-commits-by-path.js --input /tmp/commits.json --owner owner --repo repo --paths "app/,web/"
|
||||
*
|
||||
* 引数:
|
||||
* --input <file> 入力ファイル(省略時stdin)
|
||||
* --owner <name> リポジトリオーナー(必須)
|
||||
* --repo <name> リポジトリ名(必須)
|
||||
* --paths <list> カンマ区切りパス(必須)例: "app/,web/"
|
||||
* --concurrency <n> 並列数(デフォルト: 5)
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* コマンドライン引数を解析する
|
||||
* @returns {{ input: string|null, owner: string, repo: string, paths: string[], concurrency: number }}
|
||||
*/
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const parsed = { input: null, owner: null, repo: null, paths: null, concurrency: 5 };
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--input':
|
||||
parsed.input = args[++i];
|
||||
break;
|
||||
case '--owner':
|
||||
parsed.owner = args[++i];
|
||||
break;
|
||||
case '--repo':
|
||||
parsed.repo = args[++i];
|
||||
break;
|
||||
case '--paths':
|
||||
parsed.paths = args[++i];
|
||||
break;
|
||||
case '--concurrency':
|
||||
parsed.concurrency = parseInt(args[++i], 10);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsed.owner || !parsed.repo || !parsed.paths) {
|
||||
console.error('Usage: node filter-commits-by-path.js --owner <owner> --repo <repo> --paths <paths>');
|
||||
console.error('');
|
||||
console.error('Required:');
|
||||
console.error(' --owner <name> Repository owner');
|
||||
console.error(' --repo <name> Repository name');
|
||||
console.error(' --paths <list> Comma-separated paths (e.g. "app/,web/,supabase/")');
|
||||
console.error('');
|
||||
console.error('Optional:');
|
||||
console.error(' --input <file> Input file (default: stdin)');
|
||||
console.error(' --concurrency <n> Parallel requests (default: 5)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return {
|
||||
input: parsed.input,
|
||||
owner: parsed.owner,
|
||||
repo: parsed.repo,
|
||||
paths: parsed.paths.split(',').map(p => p.trim()).filter(Boolean),
|
||||
concurrency: parsed.concurrency
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* stdinまたはファイルからJSONを読み込む
|
||||
* @param {string|null} inputFile
|
||||
* @returns {object}
|
||||
*/
|
||||
function readInput(inputFile) {
|
||||
let raw;
|
||||
if (inputFile) {
|
||||
raw = fs.readFileSync(inputFile, 'utf-8');
|
||||
} else {
|
||||
raw = fs.readFileSync(0, 'utf-8'); // stdin
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
console.error('JSON解析エラー:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* コミットの変更ファイル一覧を GitHub API で取得する
|
||||
* @param {string} owner
|
||||
* @param {string} repo
|
||||
* @param {string} sha
|
||||
* @returns {{ filename: string, status: string, additions: number, deletions: number }[] | null}
|
||||
*/
|
||||
function getCommitFiles(owner, repo, sha) {
|
||||
const cmd = `gh api "repos/${owner}/${repo}/commits/${sha}" --jq '[.files[] | {filename, status, additions, deletions}]'`;
|
||||
try {
|
||||
const result = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
|
||||
return JSON.parse(result);
|
||||
} catch (error) {
|
||||
console.error(`[${sha.substring(0, 7)}] ファイル取得エラー: ${error.message.split('\n')[0]}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* セマフォパターンで並列数を制限しながら処理を実行する
|
||||
* @param {Array} items
|
||||
* @param {function} processor
|
||||
* @param {number} concurrency
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async function processWithConcurrency(items, processor, concurrency) {
|
||||
const results = new Array(items.length);
|
||||
let index = 0;
|
||||
let completed = 0;
|
||||
|
||||
async function worker() {
|
||||
while (index < items.length) {
|
||||
const i = index++;
|
||||
results[i] = await processor(items[i], i);
|
||||
completed++;
|
||||
if (completed % 10 === 0) {
|
||||
console.error(` 進捗: ${completed}/${items.length}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const workers = [];
|
||||
for (let i = 0; i < Math.min(concurrency, items.length); i++) {
|
||||
workers.push(worker());
|
||||
}
|
||||
await Promise.all(workers);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* メイン処理
|
||||
*/
|
||||
async function main() {
|
||||
const { input, owner, repo, paths, concurrency } = parseArgs();
|
||||
|
||||
console.error('=== パスフィルタリング ===');
|
||||
console.error(`リポジトリ: ${owner}/${repo}`);
|
||||
console.error(`対象パス: ${paths.join(', ')}`);
|
||||
console.error(`並列数: ${concurrency}`);
|
||||
console.error('=========================');
|
||||
|
||||
// 入力読み込み
|
||||
const data = readInput(input);
|
||||
|
||||
// 全ブランチから全コミットを収集(重複排除)
|
||||
/** @type {{ sha: string, message: string, date: string, author: object, html_url: string, branch: string }[]} */
|
||||
const allCommits = [];
|
||||
const seenShas = new Set();
|
||||
|
||||
for (const [branchName, branchData] of Object.entries(data.branches)) {
|
||||
for (const commit of branchData.commits) {
|
||||
if (!seenShas.has(commit.sha)) {
|
||||
seenShas.add(commit.sha);
|
||||
allCommits.push({ ...commit, branch: branchName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const originalCount = allCommits.length;
|
||||
console.error(`全コミット数: ${originalCount}(重複排除済み)`);
|
||||
|
||||
if (originalCount === 0) {
|
||||
// コミットがない場合はそのまま出力
|
||||
const result = {
|
||||
metadata: {
|
||||
...data.metadata,
|
||||
filter_paths: paths,
|
||||
original_commits: 0,
|
||||
filtered_commits: 0
|
||||
},
|
||||
branches: {}
|
||||
};
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
// 並列でファイル取得 & パスマッチング
|
||||
console.error(`ファイル情報を取得中(並列数: ${concurrency})...`);
|
||||
|
||||
const matchResults = await processWithConcurrency(
|
||||
allCommits,
|
||||
(commit) => {
|
||||
const files = getCommitFiles(owner, repo, commit.sha);
|
||||
if (!files) return { commit, matchedFiles: [] };
|
||||
|
||||
const matchedFiles = files.filter(f =>
|
||||
paths.some(p => f.filename.startsWith(p))
|
||||
);
|
||||
return { commit, matchedFiles };
|
||||
},
|
||||
concurrency
|
||||
);
|
||||
|
||||
// マッチしたコミットのみでブランチ構造を再構築
|
||||
const filteredBranches = {};
|
||||
let filteredCount = 0;
|
||||
|
||||
for (const { commit, matchedFiles } of matchResults) {
|
||||
if (matchedFiles.length === 0) continue;
|
||||
|
||||
filteredCount++;
|
||||
const branchName = commit.branch;
|
||||
|
||||
if (!filteredBranches[branchName]) {
|
||||
const originalBranch = data.branches[branchName];
|
||||
filteredBranches[branchName] = {
|
||||
commits: [],
|
||||
is_default: originalBranch.is_default
|
||||
};
|
||||
}
|
||||
|
||||
// ブランチ情報はコミットから除外(出力形式を元の構造と合わせる)
|
||||
const { branch, ...commitData } = commit;
|
||||
filteredBranches[branchName].commits.push({
|
||||
...commitData,
|
||||
matched_files: matchedFiles
|
||||
});
|
||||
}
|
||||
|
||||
// 結果出力
|
||||
const result = {
|
||||
metadata: {
|
||||
...data.metadata,
|
||||
filter_paths: paths,
|
||||
original_commits: originalCount,
|
||||
filtered_commits: filteredCount
|
||||
},
|
||||
branches: filteredBranches
|
||||
};
|
||||
|
||||
// 更新されたメタデータ
|
||||
result.metadata.active_branches = Object.keys(filteredBranches).length;
|
||||
result.metadata.total_commits = filteredCount;
|
||||
|
||||
console.error('');
|
||||
console.error(`=== フィルタ結果 ===`);
|
||||
console.error(`元のコミット: ${originalCount}`);
|
||||
console.error(`フィルタ後: ${filteredCount}`);
|
||||
console.error(`アクティブブランチ: ${Object.keys(filteredBranches).length}`);
|
||||
console.error('====================');
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,315 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 全ブランチの前日(JST)コミットを一括取得するスクリプト
|
||||
*
|
||||
* 最適化: GraphQLで最近アクティブなブランチのみを抽出してからコミット取得
|
||||
*
|
||||
* 使い方:
|
||||
* node get-all-branch-commits.js <owner> <repo> [--output <file>]
|
||||
*
|
||||
* 環境変数:
|
||||
* GH_TOKEN: GitHub API認証トークン
|
||||
* TARGET_DATE: 対象日(省略時は前日JST)形式: YYYY-MM-DD
|
||||
* ACTIVE_DAYS: アクティブとみなす日数(省略時は7日)
|
||||
*
|
||||
* 出力:
|
||||
* 全ブランチのコミットをJSON形式で出力
|
||||
* --output 指定時はファイルに書き込み(シェルリダイレクトより確実)
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const { normalizeCommits } = require('./lib/normalize');
|
||||
const { getYesterdayJST, getTargetDateJST } = require('./lib/date-utils');
|
||||
|
||||
// 除外するブランチのプレフィックス
|
||||
const EXCLUDED_PREFIXES = ['dependabot/', 'renovate/'];
|
||||
|
||||
// GraphQLでブランチ一覧と最終コミット日を取得(ページネーション対応)
|
||||
function getAllBranchesWithDates(owner, repo) {
|
||||
const allBranches = [];
|
||||
let hasNextPage = true;
|
||||
let cursor = null;
|
||||
|
||||
while (hasNextPage) {
|
||||
const afterClause = cursor ? `, after: "${cursor}"` : '';
|
||||
const query = `
|
||||
{
|
||||
repository(owner: "${owner}", name: "${repo}") {
|
||||
defaultBranchRef {
|
||||
name
|
||||
}
|
||||
refs(refPrefix: "refs/heads/", first: 100${afterClause}) {
|
||||
nodes {
|
||||
name
|
||||
target {
|
||||
... on Commit {
|
||||
committedDate
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
try {
|
||||
const result = execSync(
|
||||
`gh api graphql -f query='${query.replace(/'/g, "'\\''")}'`,
|
||||
{ encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 }
|
||||
);
|
||||
const data = JSON.parse(result);
|
||||
const refs = data.data.repository.refs;
|
||||
const defaultBranch = data.data.repository.defaultBranchRef?.name || 'main';
|
||||
|
||||
for (const node of refs.nodes) {
|
||||
if (!EXCLUDED_PREFIXES.some(prefix => node.name.startsWith(prefix))) {
|
||||
allBranches.push({
|
||||
name: node.name,
|
||||
lastCommitDate: node.target?.committedDate || null,
|
||||
isDefault: node.name === defaultBranch
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
hasNextPage = refs.pageInfo.hasNextPage;
|
||||
cursor = refs.pageInfo.endCursor;
|
||||
} catch (error) {
|
||||
console.error('GraphQLエラー:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
return allBranches;
|
||||
}
|
||||
|
||||
// 特定ブランチのコミットを取得(リトライ機能付き)
|
||||
function getBranchCommits(owner, repo, branch, startUTC, endUTC, maxRetries = 3) {
|
||||
const cmd = `gh api "repos/${owner}/${repo}/commits?sha=${encodeURIComponent(branch)}&since=${startUTC}&until=${endUTC}" --paginate`;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const rawResult = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
|
||||
const rawCommits = JSON.parse(rawResult);
|
||||
return { success: true, commits: normalizeCommits(rawCommits) };
|
||||
} catch (error) {
|
||||
const errorMsg = error.message.split('\n')[0];
|
||||
if (attempt < maxRetries) {
|
||||
console.error(`[${branch}] リトライ ${attempt}/${maxRetries}: ${errorMsg}`);
|
||||
const waitMs = Math.pow(2, attempt - 1) * 1000;
|
||||
execSync(`sleep ${waitMs / 1000}`);
|
||||
} else {
|
||||
console.error(`[${branch}] ⚠️ 取得失敗(${maxRetries}回リトライ後): ${errorMsg}`);
|
||||
return { success: false, commits: [], error: errorMsg };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { success: false, commits: [], error: 'Unknown error' };
|
||||
}
|
||||
|
||||
// アクティブブランチをフィルタリング
|
||||
function filterActiveBranches(allBranches, activeDays, targetDateStr) {
|
||||
const targetDate = new Date(targetDateStr);
|
||||
const cutoffDate = new Date(targetDate);
|
||||
cutoffDate.setDate(cutoffDate.getDate() - activeDays);
|
||||
const cutoffStr = cutoffDate.toISOString();
|
||||
|
||||
return allBranches.filter(branch => {
|
||||
// デフォルトブランチは常に含める
|
||||
if (branch.isDefault) return true;
|
||||
// 最終コミット日がない場合はスキップ
|
||||
if (!branch.lastCommitDate) return false;
|
||||
// 最終コミット日がカットオフ以降なら含める
|
||||
return branch.lastCommitDate >= cutoffStr;
|
||||
});
|
||||
}
|
||||
|
||||
// 引数パース
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
let owner = null;
|
||||
let repo = null;
|
||||
let outputFile = null;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--output' || args[i] === '-o') {
|
||||
outputFile = args[++i];
|
||||
} else if (!owner) {
|
||||
owner = args[i];
|
||||
} else if (!repo) {
|
||||
repo = args[i];
|
||||
}
|
||||
}
|
||||
|
||||
return { owner, repo, outputFile };
|
||||
}
|
||||
|
||||
// メイン処理
|
||||
function main() {
|
||||
const { owner, repo, outputFile } = parseArgs();
|
||||
|
||||
if (!owner || !repo) {
|
||||
console.error('Usage: node get-all-branch-commits.js <owner> <repo> [--output <file>]');
|
||||
console.error('');
|
||||
console.error('Options:');
|
||||
console.error(' --output, -o <file> Write JSON to file instead of stdout (recommended)');
|
||||
console.error('');
|
||||
console.error('Environment variables:');
|
||||
console.error(' GH_TOKEN: GitHub API token (required)');
|
||||
console.error(' TARGET_DATE: Target date in YYYY-MM-DD format (optional, defaults to yesterday JST)');
|
||||
console.error(' ACTIVE_DAYS: Days to consider a branch active (optional, defaults to 7)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 日付計算
|
||||
const targetDate = process.env.TARGET_DATE;
|
||||
const activeDays = parseInt(process.env.ACTIVE_DAYS || '7', 10);
|
||||
const { date, startUTC, endUTC } = targetDate
|
||||
? getTargetDateJST(targetDate)
|
||||
: getYesterdayJST();
|
||||
|
||||
console.error('=== 全ブランチコミット取得(最適化版) ===');
|
||||
console.error(`対象日(JST): ${date}`);
|
||||
console.error(`開始(UTC): ${startUTC}`);
|
||||
console.error(`終了(UTC): ${endUTC}`);
|
||||
console.error(`アクティブ判定: 過去${activeDays}日以内`);
|
||||
console.error('==========================================');
|
||||
|
||||
// ステップ1: GraphQLで全ブランチと最終コミット日を取得
|
||||
console.error('');
|
||||
console.error('[ステップ1] GraphQLでブランチ一覧を取得中...');
|
||||
const allBranches = getAllBranchesWithDates(owner, repo);
|
||||
const defaultBranch = allBranches.find(b => b.isDefault)?.name || 'main';
|
||||
console.error(`総ブランチ数: ${allBranches.length}`);
|
||||
console.error(`デフォルトブランチ: ${defaultBranch}`);
|
||||
|
||||
// ステップ2: アクティブブランチをフィルタリング
|
||||
console.error('');
|
||||
console.error('[ステップ2] アクティブブランチをフィルタリング...');
|
||||
const activeBranches = filterActiveBranches(allBranches, activeDays, date);
|
||||
console.error(`アクティブブランチ: ${activeBranches.length}件(${allBranches.length}件中)`);
|
||||
activeBranches.forEach(b => {
|
||||
const marker = b.isDefault ? '(default)' : '';
|
||||
console.error(` - ${b.name} ${marker}`);
|
||||
});
|
||||
|
||||
// ステップ3: アクティブブランチのコミットを取得
|
||||
console.error('');
|
||||
console.error('[ステップ3] コミットを取得中...');
|
||||
const branches = {};
|
||||
let activeBranchCount = 0;
|
||||
let totalCommits = 0;
|
||||
let failedBranches = [];
|
||||
|
||||
for (let i = 0; i < activeBranches.length; i++) {
|
||||
const branch = activeBranches[i];
|
||||
console.error(`[${i + 1}/${activeBranches.length}] [${branch.name}] コミット取得中...`);
|
||||
const result = getBranchCommits(owner, repo, branch.name, startUTC, endUTC);
|
||||
|
||||
if (!result.success) {
|
||||
failedBranches.push({ name: branch.name, error: result.error });
|
||||
}
|
||||
|
||||
if (result.commits.length > 0) {
|
||||
branches[branch.name] = {
|
||||
commits: result.commits,
|
||||
is_default: branch.isDefault
|
||||
};
|
||||
activeBranchCount++;
|
||||
totalCommits += result.commits.length;
|
||||
console.error(`[${branch.name}] ${result.commits.length}件のコミットを検出`);
|
||||
}
|
||||
}
|
||||
|
||||
// 失敗したブランチがあれば、10秒待ってから再試行
|
||||
if (failedBranches.length > 0) {
|
||||
console.error('');
|
||||
console.error(`=== 失敗ブランチの再取得 (${failedBranches.length}件) ===`);
|
||||
console.error('10秒待機してから再試行...');
|
||||
execSync('sleep 10');
|
||||
|
||||
const stillFailed = [];
|
||||
for (let i = 0; i < failedBranches.length; i++) {
|
||||
const branchName = failedBranches[i].name;
|
||||
const branchInfo = activeBranches.find(b => b.name === branchName);
|
||||
console.error(`[再試行 ${i + 1}/${failedBranches.length}] [${branchName}] コミット取得中...`);
|
||||
const result = getBranchCommits(owner, repo, branchName, startUTC, endUTC);
|
||||
|
||||
if (!result.success) {
|
||||
stillFailed.push({ name: branchName, error: result.error });
|
||||
} else if (result.commits.length > 0) {
|
||||
branches[branchName] = {
|
||||
commits: result.commits,
|
||||
is_default: branchInfo?.isDefault || false
|
||||
};
|
||||
activeBranchCount++;
|
||||
totalCommits += result.commits.length;
|
||||
console.error(`[${branchName}] ✅ 再取得成功: ${result.commits.length}件のコミット`);
|
||||
} else {
|
||||
console.error(`[${branchName}] ✅ 再取得成功: コミットなし`);
|
||||
}
|
||||
}
|
||||
|
||||
failedBranches = stillFailed;
|
||||
console.error(`再取得完了: 残り失敗 ${failedBranches.length}件`);
|
||||
}
|
||||
|
||||
// 結果を出力
|
||||
const result = {
|
||||
metadata: {
|
||||
target_date: date,
|
||||
start_utc: startUTC,
|
||||
end_utc: endUTC,
|
||||
total_branches: allBranches.length,
|
||||
checked_branches: activeBranches.length,
|
||||
active_branches: activeBranchCount,
|
||||
total_commits: totalCommits,
|
||||
default_branch: defaultBranch,
|
||||
active_days_filter: activeDays,
|
||||
failed_branches: failedBranches.length,
|
||||
has_errors: failedBranches.length > 0
|
||||
},
|
||||
branches: branches
|
||||
};
|
||||
|
||||
if (failedBranches.length > 0) {
|
||||
result.errors = failedBranches;
|
||||
}
|
||||
|
||||
console.error('');
|
||||
console.error(`=== 結果 ===`);
|
||||
console.error(`総ブランチ: ${allBranches.length}`);
|
||||
console.error(`チェック対象: ${activeBranches.length}(最適化で${Math.round((1 - activeBranches.length / allBranches.length) * 100)}%削減)`);
|
||||
console.error(`コミットあり: ${activeBranchCount}`);
|
||||
console.error(`総コミット数: ${totalCommits}`);
|
||||
if (failedBranches.length > 0) {
|
||||
console.error(`❌ 取得失敗ブランチ: ${failedBranches.length}件`);
|
||||
failedBranches.forEach(fb => {
|
||||
console.error(` - ${fb.name}: ${fb.error}`);
|
||||
});
|
||||
}
|
||||
console.error('============');
|
||||
|
||||
// JSON出力
|
||||
const jsonOutput = JSON.stringify(result, null, 2);
|
||||
if (outputFile) {
|
||||
// ファイルに書き込み(stdout/stderrの混在を防ぐ)
|
||||
fs.writeFileSync(outputFile, jsonOutput, 'utf-8');
|
||||
console.error(`✅ 出力ファイル: ${outputFile}`);
|
||||
} else {
|
||||
// 標準出力(後方互換性)
|
||||
console.log(jsonOutput);
|
||||
}
|
||||
|
||||
// 失敗があれば exit 1 で終了
|
||||
if (failedBranches.length > 0) {
|
||||
console.error('');
|
||||
console.error('❌ 一部ブランチの取得に失敗しました。処理を中断します。');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 前日(JST)のコミットを確実に取得するスクリプト
|
||||
*
|
||||
* 使い方:
|
||||
* node get-commits.js <owner> <repo> [branch]
|
||||
*
|
||||
* 環境変数:
|
||||
* GH_TOKEN: GitHub API認証トークン
|
||||
* TARGET_DATE: 対象日(省略時は前日JST)形式: YYYY-MM-DD
|
||||
*
|
||||
* 出力:
|
||||
* 正規化されたJSONを標準出力に出力(jq処理不要)
|
||||
* - sha, message, date, author.login, author.avatar_url, html_url を保証
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const { normalizeCommits } = require('./lib/normalize');
|
||||
const { getYesterdayJST, getTargetDateJST } = require('./lib/date-utils');
|
||||
|
||||
// メイン処理
|
||||
function main() {
|
||||
const [owner, repo, branch = 'main'] = process.argv.slice(2);
|
||||
|
||||
if (!owner || !repo) {
|
||||
console.error('Usage: node get-commits.js <owner> <repo> [branch]');
|
||||
console.error('');
|
||||
console.error('Environment variables:');
|
||||
console.error(' GH_TOKEN: GitHub API token (required)');
|
||||
console.error(' TARGET_DATE: Target date in YYYY-MM-DD format (optional, defaults to yesterday JST)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 日付計算
|
||||
const targetDate = process.env.TARGET_DATE;
|
||||
const { date, startUTC, endUTC } = targetDate
|
||||
? getTargetDateJST(targetDate)
|
||||
: getYesterdayJST();
|
||||
|
||||
// ログ出力(デバッグ用、標準エラーへ)
|
||||
console.error('=== 検索期間 ===');
|
||||
console.error(`対象日(JST): ${date}`);
|
||||
console.error(`開始(UTC): ${startUTC}`);
|
||||
console.error(`終了(UTC): ${endUTC}`);
|
||||
console.error(`ブランチ: ${branch}`);
|
||||
console.error('================');
|
||||
|
||||
// gh API呼び出し
|
||||
const cmd = `gh api "repos/${owner}/${repo}/commits?sha=${branch}&since=${startUTC}&until=${endUTC}" --paginate`;
|
||||
|
||||
try {
|
||||
const rawResult = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
|
||||
const rawCommits = JSON.parse(rawResult);
|
||||
const normalized = normalizeCommits(rawCommits);
|
||||
// 正規化されたJSON出力(標準出力)
|
||||
console.log(JSON.stringify(normalized, null, 2));
|
||||
} catch (error) {
|
||||
console.error('API呼び出しエラー:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,45 @@
|
||||
function getYesterdayJST() {
|
||||
const now = new Date();
|
||||
const jstOffset = 9 * 60 * 60 * 1000;
|
||||
const jstNow = new Date(now.getTime() + jstOffset);
|
||||
|
||||
const yesterday = new Date(jstNow);
|
||||
yesterday.setUTCDate(yesterday.getUTCDate() - 1);
|
||||
|
||||
const year = yesterday.getUTCFullYear();
|
||||
const month = yesterday.getUTCMonth();
|
||||
const day = yesterday.getUTCDate();
|
||||
|
||||
const start = new Date(Date.UTC(year, month, day, 0, 0, 0));
|
||||
start.setTime(start.getTime() - jstOffset);
|
||||
|
||||
const end = new Date(Date.UTC(year, month, day, 23, 59, 59));
|
||||
end.setTime(end.getTime() - jstOffset);
|
||||
|
||||
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
|
||||
return {
|
||||
date: dateStr,
|
||||
startUTC: start.toISOString().replace('.000Z', 'Z'),
|
||||
endUTC: end.toISOString().replace('.000Z', 'Z')
|
||||
};
|
||||
}
|
||||
|
||||
function getTargetDateJST(dateStr) {
|
||||
const [year, month, day] = dateStr.split('-').map(Number);
|
||||
const jstOffset = 9 * 60 * 60 * 1000;
|
||||
|
||||
const start = new Date(Date.UTC(year, month - 1, day, 0, 0, 0));
|
||||
start.setTime(start.getTime() - jstOffset);
|
||||
|
||||
const end = new Date(Date.UTC(year, month - 1, day, 23, 59, 59));
|
||||
end.setTime(end.getTime() - jstOffset);
|
||||
|
||||
return {
|
||||
date: dateStr,
|
||||
startUTC: start.toISOString().replace('.000Z', 'Z'),
|
||||
endUTC: end.toISOString().replace('.000Z', 'Z')
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { getYesterdayJST, getTargetDateJST };
|
||||
@@ -0,0 +1,30 @@
|
||||
const crypto = require('crypto');
|
||||
|
||||
function normalizeCommits(rawCommits) {
|
||||
return rawCommits.map(commit => {
|
||||
let author;
|
||||
if (commit.author?.login && commit.author?.avatar_url) {
|
||||
author = { login: commit.author.login, avatar_url: commit.author.avatar_url };
|
||||
} else {
|
||||
const email = commit.commit?.author?.email || '';
|
||||
const name = commit.commit?.author?.name || 'Unknown';
|
||||
const hash = email
|
||||
? crypto.createHash('md5').update(email.toLowerCase().trim()).digest('hex')
|
||||
: '00000000000000000000000000000000';
|
||||
author = {
|
||||
login: name,
|
||||
avatar_url: `https://www.gravatar.com/avatar/${hash}?d=identicon&s=80`
|
||||
};
|
||||
console.error(`[Avatar] フォールバック: ${name} (${commit.sha.substring(0, 7)})`);
|
||||
}
|
||||
return {
|
||||
sha: commit.sha,
|
||||
message: commit.commit.message,
|
||||
date: commit.commit.author.date,
|
||||
author,
|
||||
html_url: commit.html_url
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { normalizeCommits };
|
||||
Reference in New Issue
Block a user