-
Notifications
You must be signed in to change notification settings - Fork 0
API Reference
Makoto Horikawa edited this page Jul 9, 2025
·
1 revision
GitHub Power ScouterのAPI使用方法とリファレンス。GitHub REST API v3の効率的な活用方法を詳しく解説します。
| 項目 | 詳細 |
|---|---|
| ベースURL | https://api.github.com |
| 認証 | Personal Access Token |
| レート制限 | 未認証: 60req/h, 認証済み: 5000req/h |
| ドキュメント | GitHub API Docs |
graph TB
A[GitHub API] --> B[Users API]
A --> C[Repositories API]
A --> D[Events API]
A --> E[Rate Limit API]
B --> F[/users/{username}]
C --> G[/users/{username}/repos]
C --> H[/repos/{owner}/{repo}/languages]
D --> I[/users/{username}/events/public]
E --> J[/rate_limit]
export class GitHubApiClient {
private baseUrl = 'https://api.github.com';
private token?: string;
constructor(token?: string) {
this.token = token;
}
private getHeaders(): HeadersInit {
const headers: HeadersInit = {
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'GitHub-Power-Scouter/2.0',
};
if (this.token) {
headers['Authorization'] = `token ${this.token}`;
}
return headers;
}
private async request<T>(endpoint: string): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
headers: this.getHeaders(),
});
if (!response.ok) {
throw new ApiError(
`API request failed: ${response.status} ${response.statusText}`,
response.status,
await response.json()
);
}
return response.json();
}
// レート制限情報を取得
async getRateLimit(): Promise<GitHubRateLimit> {
return this.request<GitHubRateLimit>('/rate_limit');
}
// ユーザー情報を取得
async getUser(username: string): Promise<GitHubUser> {
return this.request<GitHubUser>(`/users/${username}`);
}
// ユーザーのリポジトリ一覧を取得
async getUserRepos(username: string, options?: RepoOptions): Promise<GitHubRepo[]> {
const params = new URLSearchParams({
per_page: '100',
sort: 'stars',
direction: 'desc',
...options,
});
return this.request<GitHubRepo[]>(`/users/${username}/repos?${params}`);
}
// リポジトリの言語統計を取得
async getRepoLanguages(owner: string, repo: string): Promise<LanguageStats> {
return this.request<LanguageStats>(`/repos/${owner}/${repo}/languages`);
}
// ユーザーの公開イベントを取得
async getUserEvents(username: string): Promise<GitHubEvent[]> {
return this.request<GitHubEvent[]>(`/users/${username}/events/public?per_page=100`);
}
// リポジトリのコンテンツを取得
async getRepoContents(owner: string, repo: string, path = ''): Promise<RepoContent[]> {
return this.request<RepoContent[]>(`/repos/${owner}/${repo}/contents/${path}`);
}
}export class ApiError extends Error {
constructor(
message: string,
public status: number,
public response?: any
) {
super(message);
this.name = 'ApiError';
}
// GitHub API固有エラーの判定
isRateLimitError(): boolean {
return this.status === 403 && this.response?.message?.includes('rate limit');
}
isNotFoundError(): boolean {
return this.status === 404;
}
isUnauthorizedError(): boolean {
return this.status === 401;
}
// ユーザーフレンドリーなエラーメッセージ
getUserMessage(): string {
switch (this.status) {
case 404:
return 'ユーザーまたはリポジトリが見つかりません';
case 401:
return 'GitHubトークンが無効です';
case 403:
if (this.isRateLimitError()) {
return 'API制限に達しました。しばらく待ってから再試行してください';
}
return 'アクセスが拒否されました';
case 500:
return 'GitHubサーバーエラーが発生しました';
default:
return `予期しないエラーが発生しました (${this.status})`;
}
}
}interface PowerLevelCalculator {
calculateBasePower(userData: GitHubUser): number;
calculateRepoBonus(repos: GitHubRepo[]): number;
calculateLanguageBonus(languages: ProcessedLanguage[]): number;
calculateActivityBonus(activity: ActivityData): number;
calculateTotalPower(
userData: GitHubUser,
repos: GitHubRepo[],
languages: ProcessedLanguage[],
activity: ActivityData
): PowerLevelResult;
}
export class PowerLevelCalculator implements PowerLevelCalculator {
// 基本パワーの計算
calculateBasePower(userData: GitHubUser): number {
const accountAge = this.getAccountAge(userData.created_at);
return (
userData.public_repos * 100 + // リポジトリ数
userData.followers * 30 + // フォロワー数
userData.following * 5 + // フォロー数
userData.public_gists * 50 + // Gist数
accountAge * 1000 + // アカウント年齢
(userData.bio ? 500 : 0) + // プロフィール記入
(userData.blog ? 500 : 0) + // ブログ記入
(userData.company ? 1000 : 0) + // 会社情報
(userData.location ? 300 : 0) + // 場所情報
(userData.hireable ? 2000 : 0) // 採用可能
);
}
// リポジトリボーナスの計算
calculateRepoBonus(repos: GitHubRepo[]): number {
let totalStars = 0;
let totalForks = 0;
let totalWatchers = 0;
let originalRepos = 0;
repos.forEach(repo => {
if (!repo.fork) {
originalRepos++;
totalStars += repo.stargazers_count;
totalForks += repo.forks_count;
totalWatchers += repo.watchers_count;
}
});
return (
originalRepos * 200 + // オリジナルリポジトリ
totalStars * 50 + // 獲得スター数
totalForks * 40 + // フォーク数
totalWatchers * 20 // ウォッチャー数
);
}
// 言語ボーナスの計算
calculateLanguageBonus(languages: ProcessedLanguage[]): number {
const diversityBonus = languages.length * 1000;
const dominanceBonus = languages.length > 0
? (languages[0].percentage > 80 ? 2000 : 0)
: 0;
return diversityBonus + dominanceBonus;
}
// 活動ボーナスの計算
calculateActivityBonus(activity: ActivityData): number {
return (
activity.commits * 10 + // コミット数
activity.pullRequests * 100 + // PR数
activity.issues * 50 + // イシュー数
activity.recentContributions * 20 // 最近の貢献
);
}
// 総パワーレベル計算
calculateTotalPower(
userData: GitHubUser,
repos: GitHubRepo[],
languages: ProcessedLanguage[],
activity: ActivityData
): PowerLevelResult {
const basePower = this.calculateBasePower(userData);
const repoBonus = this.calculateRepoBonus(repos);
const languageBonus = this.calculateLanguageBonus(languages);
const activityBonus = this.calculateActivityBonus(activity);
const totalPower = basePower + repoBonus + languageBonus + activityBonus;
return {
power: Math.floor(totalPower),
breakdown: {
base: basePower,
repo: repoBonus,
language: languageBonus,
activity: activityBonus,
},
stats: {
repos: userData.public_repos,
originalRepos: repos.filter(r => !r.fork).length,
stars: repos.reduce((sum, r) => sum + r.stargazers_count, 0),
forks: repos.reduce((sum, r) => sum + r.forks_count, 0),
followers: userData.followers,
following: userData.following,
accountAge: this.getAccountAge(userData.created_at),
languages,
activity,
gists: userData.public_gists,
},
};
}
private getAccountAge(createdAt: string): number {
const created = new Date(createdAt);
const now = new Date();
return Math.floor((now.getTime() - created.getTime()) / (1000 * 60 * 60 * 24 * 365));
}
}export class LanguageAnalyzer {
async analyzeUserLanguages(
repos: GitHubRepo[],
apiClient: GitHubApiClient
): Promise<ProcessedLanguage[]> {
const languageStats: Record<string, number> = {};
let totalBytes = 0;
// 上位リポジトリの言語統計を取得
const topRepos = repos
.filter(repo => !repo.fork)
.sort((a, b) => b.stargazers_count - a.stargazers_count)
.slice(0, 10);
for (const repo of topRepos) {
try {
const languages = await apiClient.getRepoLanguages(
repo.owner.login,
repo.name
);
Object.entries(languages).forEach(([lang, bytes]) => {
languageStats[lang] = (languageStats[lang] || 0) + bytes;
totalBytes += bytes;
});
} catch (error) {
console.warn(`Failed to fetch languages for ${repo.full_name}:`, error);
}
}
// 言語統計を処理
return Object.entries(languageStats)
.map(([name, bytes]) => ({
name,
bytes,
percentage: Math.round((bytes / totalBytes) * 100),
}))
.sort((a, b) => b.bytes - a.bytes)
.slice(0, 5); // 上位5言語
}
// 言語別の熟練度を推定
estimateProficiency(language: ProcessedLanguage): 'beginner' | 'intermediate' | 'advanced' {
if (language.percentage >= 50) return 'advanced';
if (language.percentage >= 20) return 'intermediate';
return 'beginner';
}
// 人気言語かどうかを判定
isPopularLanguage(languageName: string): boolean {
const popularLanguages = [
'JavaScript', 'Python', 'Java', 'TypeScript', 'C#',
'C++', 'PHP', 'Shell', 'C', 'Ruby', 'Go', 'Rust',
'Swift', 'Kotlin', 'Scala', 'Dart', 'R', 'Matlab',
];
return popularLanguages.includes(languageName);
}
}export class TechStackAnalyzer {
async analyzeTechStack(
repos: GitHubRepo[],
apiClient: GitHubApiClient
): Promise<TechStack> {
const techStack: TechStack = {
languages: {},
frameworks: new Set(),
devops: new Set(),
testing: new Set(),
databases: new Set(),
infrastructure: new Set(),
};
// 上位リポジトリを分析
const topRepos = repos
.filter(repo => !repo.fork)
.sort((a, b) => b.stargazers_count - a.stargazers_count)
.slice(0, 20);
for (const repo of topRepos) {
try {
const contents = await apiClient.getRepoContents(
repo.owner.login,
repo.name
);
await this.analyzeRepoContents(contents, techStack, apiClient, repo);
} catch (error) {
console.warn(`Failed to analyze ${repo.full_name}:`, error);
}
}
return techStack;
}
private async analyzeRepoContents(
contents: RepoContent[],
techStack: TechStack,
apiClient: GitHubApiClient,
repo: GitHubRepo
): Promise<void> {
for (const item of contents) {
const fileName = item.name.toLowerCase();
// 設定ファイルから技術スタックを推定
await this.analyzeConfigFiles(fileName, techStack, apiClient, repo);
// ディレクトリ構造から技術スタックを推定
if (item.type === 'dir') {
this.analyzeDirectoryStructure(fileName, techStack);
}
}
}
private async analyzeConfigFiles(
fileName: string,
techStack: TechStack,
apiClient: GitHubApiClient,
repo: GitHubRepo
): Promise<void> {
const configAnalyzers = {
'package.json': () => this.analyzePackageJson(apiClient, repo, techStack),
'requirements.txt': () => techStack.frameworks.add('Python'),
'pyproject.toml': () => techStack.frameworks.add('Python (Modern)'),
'composer.json': () => techStack.frameworks.add('PHP/Composer'),
'pom.xml': () => techStack.frameworks.add('Java/Maven'),
'build.gradle': () => techStack.frameworks.add('Java/Gradle'),
'cargo.toml': () => techStack.frameworks.add('Rust'),
'go.mod': () => techStack.frameworks.add('Go'),
'gemfile': () => techStack.frameworks.add('Ruby'),
'dockerfile': () => techStack.devops.add('Docker'),
};
const analyzer = configAnalyzers[fileName];
if (analyzer) {
await analyzer();
}
// YAML/YMLファイルの分析
if (fileName.endsWith('.yml') || fileName.endsWith('.yaml')) {
this.analyzeYamlFile(fileName, techStack);
}
}
private async analyzePackageJson(
apiClient: GitHubApiClient,
repo: GitHubRepo,
techStack: TechStack
): Promise<void> {
try {
const content = await apiClient.getRepoContents(
repo.owner.login,
repo.name,
'package.json'
);
if (Array.isArray(content)) return;
const packageData = JSON.parse(atob(content.content));
const allDeps = {
...packageData.dependencies,
...packageData.devDependencies,
};
// フレームワーク検出
const frameworks = {
'react': 'React',
'vue': 'Vue.js',
'@angular/core': 'Angular',
'next': 'Next.js',
'nuxt': 'Nuxt.js',
'express': 'Express.js',
'fastify': 'Fastify',
'nestjs': 'NestJS',
'svelte': 'Svelte',
'ember-source': 'Ember.js',
};
Object.entries(frameworks).forEach(([dep, name]) => {
if (allDeps[dep]) {
techStack.frameworks.add(name);
}
});
// テストツール検出
const testTools = {
'jest': 'Jest',
'mocha': 'Mocha',
'cypress': 'Cypress',
'playwright': 'Playwright',
'vitest': 'Vitest',
'testing-library': 'Testing Library',
};
Object.entries(testTools).forEach(([dep, name]) => {
if (allDeps[dep] || Object.keys(allDeps).some(key => key.includes(dep))) {
techStack.testing.add(name);
}
});
// ビルドツール検出
const buildTools = {
'webpack': 'Webpack',
'vite': 'Vite',
'rollup': 'Rollup',
'esbuild': 'ESBuild',
'parcel': 'Parcel',
'turbo': 'Turbo',
};
Object.entries(buildTools).forEach(([dep, name]) => {
if (allDeps[dep]) {
techStack.devops.add(name);
}
});
} catch (error) {
console.warn('Failed to analyze package.json:', error);
}
}
private analyzeYamlFile(fileName: string, techStack: TechStack): void {
if (fileName.includes('github')) {
techStack.devops.add('GitHub Actions');
} else if (fileName.includes('gitlab')) {
techStack.devops.add('GitLab CI');
} else if (fileName.includes('circle')) {
techStack.devops.add('CircleCI');
} else if (fileName.includes('travis')) {
techStack.devops.add('Travis CI');
} else if (fileName.includes('docker-compose')) {
techStack.devops.add('Docker Compose');
} else if (fileName.includes('kubernetes') || fileName.includes('k8s')) {
techStack.infrastructure.add('Kubernetes');
}
}
private analyzeDirectoryStructure(dirName: string, techStack: TechStack): void {
const directories = {
'test': () => techStack.testing.add('Test-Driven Development'),
'tests': () => techStack.testing.add('Test-Driven Development'),
'spec': () => techStack.testing.add('Test-Driven Development'),
'__tests__': () => techStack.testing.add('Test-Driven Development'),
'cypress': () => techStack.testing.add('Cypress'),
'e2e': () => techStack.testing.add('End-to-End Testing'),
'docker': () => techStack.devops.add('Docker'),
'kubernetes': () => techStack.infrastructure.add('Kubernetes'),
'terraform': () => techStack.infrastructure.add('Terraform'),
'ansible': () => techStack.infrastructure.add('Ansible'),
};
const analyzer = directories[dirName];
if (analyzer) {
analyzer();
}
}
}export class RateLimitManager {
private lastRequestTime = 0;
private requestCount = 0;
private resetTime = 0;
private remaining = 0;
async checkRateLimit(apiClient: GitHubApiClient): Promise<void> {
try {
const rateLimit = await apiClient.getRateLimit();
this.remaining = rateLimit.rate.remaining;
this.resetTime = rateLimit.rate.reset * 1000;
if (this.remaining === 0) {
const waitTime = this.resetTime - Date.now();
if (waitTime > 0) {
throw new ApiError(
`Rate limit exceeded. Reset in ${Math.ceil(waitTime / 1000)}s`,
429,
{ reset_at: new Date(this.resetTime) }
);
}
}
} catch (error) {
console.warn('Failed to check rate limit:', error);
}
}
async throttleRequest<T>(
request: () => Promise<T>,
minInterval = 100
): Promise<T> {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
if (timeSinceLastRequest < minInterval) {
await new Promise(resolve =>
setTimeout(resolve, minInterval - timeSinceLastRequest)
);
}
this.lastRequestTime = Date.now();
this.requestCount++;
try {
return await request();
} catch (error) {
if (error instanceof ApiError && error.isRateLimitError()) {
// 指数バックオフで再試行
await this.exponentialBackoff();
return this.throttleRequest(request, minInterval);
}
throw error;
}
}
private async exponentialBackoff(attempt = 1): Promise<void> {
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
await new Promise(resolve => setTimeout(resolve, delay));
}
getRemainingRequests(): number {
return this.remaining;
}
getResetTime(): Date {
return new Date(this.resetTime);
}
}export class MockGitHubApiClient implements GitHubApiClient {
async getRateLimit(): Promise<GitHubRateLimit> {
return {
rate: {
limit: 5000,
remaining: 4999,
reset: Math.floor(Date.now() / 1000) + 3600,
used: 1,
},
search: {
limit: 30,
remaining: 30,
reset: Math.floor(Date.now() / 1000) + 3600,
used: 0,
},
};
}
async getUser(username: string): Promise<GitHubUser> {
return {
login: username,
id: 1,
avatar_url: `https://github.com/${username}.png`,
html_url: `https://github.com/${username}`,
name: `${username} (Mock)`,
company: 'Mock Company',
blog: 'https://example.com',
location: 'Mock Location',
email: null,
hireable: true,
bio: 'Mock user for testing',
public_repos: 50,
public_gists: 10,
followers: 1000,
following: 100,
created_at: '2010-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
};
}
async getUserRepos(username: string): Promise<GitHubRepo[]> {
return [
{
id: 1,
name: 'awesome-project',
full_name: `${username}/awesome-project`,
private: false,
owner: {
login: username,
id: 1,
avatar_url: `https://github.com/${username}.png`,
},
html_url: `https://github.com/${username}/awesome-project`,
description: 'An awesome project',
fork: false,
url: `https://api.github.com/repos/${username}/awesome-project`,
languages_url: `https://api.github.com/repos/${username}/awesome-project/languages`,
stargazers_count: 100,
watchers_count: 50,
forks_count: 25,
open_issues_count: 5,
created_at: '2022-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
pushed_at: '2024-01-01T00:00:00Z',
language: 'TypeScript',
size: 1024,
},
];
}
}// メインアプリケーションでの使用
const useGitHubScouter = (token?: string) => {
const apiClient = useMemo(() => new GitHubApiClient(token), [token]);
const rateLimitManager = useMemo(() => new RateLimitManager(), []);
const powerCalculator = useMemo(() => new PowerLevelCalculator(), []);
const languageAnalyzer = useMemo(() => new LanguageAnalyzer(), []);
const techStackAnalyzer = useMemo(() => new TechStackAnalyzer(), []);
const scanUser = useCallback(async (username: string) => {
try {
// レート制限チェック
await rateLimitManager.checkRateLimit(apiClient);
// 並行でデータ取得
const [userData, repos] = await Promise.all([
rateLimitManager.throttleRequest(() => apiClient.getUser(username)),
rateLimitManager.throttleRequest(() => apiClient.getUserRepos(username)),
]);
// 詳細分析
const [languages, events, techStack] = await Promise.all([
languageAnalyzer.analyzeUserLanguages(repos, apiClient),
rateLimitManager.throttleRequest(() => apiClient.getUserEvents(username)),
techStackAnalyzer.analyzeTechStack(repos, apiClient),
]);
// 活動データ処理
const activity = processUserEvents(events);
// パワーレベル計算
const powerLevel = powerCalculator.calculateTotalPower(
userData,
repos,
languages,
activity
);
return {
userData,
repos,
languages,
techStack,
activity,
powerLevel,
};
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new Error(`Scan failed: ${error.message}`);
}
}, [apiClient, rateLimitManager, powerCalculator, languageAnalyzer, techStackAnalyzer]);
return { scanUser };
};🎯 このAPIリファレンスを参考に、効率的なGitHub APIの活用を行いましょう!
次はコンポーネントリファレンスでReactコンポーネントの詳細を確認できます。