feat: コミットレポートツール(コミネコ)初期コミット
Made-with: Cursor
This commit is contained in:
+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