UNPKG

github-mcp-auto-git

Version:

GitHub MCP Auto Git v3.0 - メモリ効率化・統合MCP・モジュール化完了の完全自動Git操作システム

604 lines 26.1 kB
import simpleGit from 'simple-git'; import { Octokit } from '@octokit/rest'; import { promises as fs } from 'fs'; import { join, basename, dirname } from 'path'; import { fileURLToPath } from 'url'; import { SubAgentManager } from './subagent-manager.js'; import { ErrorRecoverySystem } from './error-recovery.js'; import { ResilientExecutor } from './resilient-executor.js'; import { SecurityManager, SecurityLevel } from './security-manager.js'; import { UnifiedMCPManager } from './unified-mcp-manager.js'; import { ConstitutionalAIChecker } from './constitutional-ai-checker.js'; // パッケージ内のエージェントディレクトリを取得 function getAgentsDirectory() { try { // __filename の代替として import.meta.url を使用 const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // パッケージルートから agents ディレクトリを探す const packageRoot = join(__dirname, '..', '..'); const agentsPath = join(packageRoot, 'src', 'agents'); return agentsPath; } catch (error) { // フォールバック: 従来の相対パス console.warn('⚠️ Could not determine package agents directory, using fallback'); return './src/agents'; } } export class GitOperations { constructor(config, projectPath = process.cwd()) { this.config = config; this.projectPath = projectPath; this.git = simpleGit(projectPath); this.octokit = new Octokit({ auth: config.github.token }); this.subAgentManager = new SubAgentManager(getAgentsDirectory(), projectPath); this.errorRecovery = new ErrorRecoverySystem(); this.resilientExecutor = new ResilientExecutor(); this.securityManager = new SecurityManager(); this.mcpManager = new UnifiedMCPManager(config); this.constitutionalChecker = new ConstitutionalAIChecker(this.projectPath); } async initialize() { try { // セキュリティ検証: 設定の妥当性チェック const configValidation = this.securityManager.validateInput(this.config, 'object', SecurityLevel.INTERNAL); if (!configValidation.isValid) { const criticalThreats = configValidation.threats.filter(t => t.severity === 'critical'); if (criticalThreats.length > 0) { throw new Error(`設定にセキュリティ問題があります: ${criticalThreats.map(t => t.description).join(', ')}`); } } // GitHub トークンの検証 if (this.config.github.token) { const tokenValidation = await this.securityManager.validateToken(this.config.github.token, 'github'); if (!tokenValidation.isValid) { console.warn('⚠️ GitHub トークンの検証に失敗しました'); } else { console.log(`✅ GitHub トークン検証成功 (権限: ${tokenValidation.permissions.join(', ')})`); } } await this.git.init(); const status = await this.subAgentManager.getAgentStatus(); if (status.errors.length > 0) { console.warn('⚠️ Some sub-agents failed to load:', status.errors); } console.log(`✅ Git operations initialized with ${status.available.length} sub-agents`); // Unified MCP Manager の初期化 try { await this.mcpManager.initialize(); console.log(`✅ Unified MCP Manager 初期化完了`); } catch (error) { console.warn('⚠️ MCP Manager 初期化に失敗(フォールバックモードで継続):', error); } } catch (error) { return await this.errorRecovery.handleError(error, { operation: 'initialize', timestamp: new Date(), workingDir: this.projectPath, attempt: 1 }, async () => { // 軽量な初期化のフォールバック console.log('🔧 フォールバック初期化を実行中...'); return; }); } } async analyzeChanges(files) { try { const status = await this.git.status(); const diff = await this.git.diff(['--cached']); const changedFiles = files || [ ...status.modified, ...status.created, ...status.deleted, ...status.renamed.map(r => r.to) ]; const linesAdded = (diff.match(/^\+/gm) || []).length; const linesDeleted = (diff.match(/^-/gm) || []).length; const changeType = this.determineChangeType(changedFiles, diff); const impact = this.calculateImpact(changedFiles.length, linesAdded + linesDeleted); return { type: changeType, impact, files: changedFiles, description: this.generateChangeDescription(changedFiles, changeType), metrics: { linesAdded, linesDeleted, complexity: this.calculateComplexity(diff, changedFiles) } }; } catch (error) { throw new Error(`Failed to analyze changes: ${error}`); } } async executeGitWorkflow(files, options = {}) { const result = await this.resilientExecutor.execute(async () => this._executeGitWorkflowInternal(files, options), { name: 'git-workflow', workingDir: this.projectPath, files, metadata: { options } }, { maxRetries: 2, timeoutMs: 60000, critical: true, fallbackRequired: true, description: 'Complete Git workflow execution', claudeCodeOptimized: true, adaptiveTimeout: true, priorityLevel: 'high' }); if (!result.success) { return { success: false, message: `Git操作が失敗しました: ${result.error?.message}`, details: {}, warnings: result.warnings, executionTime: result.executionTime }; } return result.data; } async _executeGitWorkflowInternal(files, options = {}) { const startTime = Date.now(); const warnings = []; try { // セキュリティ検証: ファイルパスの検証 if (files && files.length > 0) { for (const file of files) { const fileValidation = this.securityManager.validateInput(file, 'string', SecurityLevel.RESTRICTED); if (!fileValidation.isValid) { const criticalThreats = fileValidation.threats.filter(t => t.severity === 'critical'); if (criticalThreats.length > 0) { throw new Error(`ファイルパスにセキュリティ問題があります: ${file} - ${criticalThreats.map(t => t.description).join(', ')}`); } warnings.push(`ファイルパス警告: ${file} - ${fileValidation.threats.map(t => t.description).join(', ')}`); } } } // オプションの検証 const optionsValidation = this.securityManager.validateInput(options, 'object', SecurityLevel.PUBLIC); if (!optionsValidation.isValid) { const criticalThreats = optionsValidation.threats.filter(t => t.severity === 'critical'); if (criticalThreats.length > 0) { throw new Error(`オプションにセキュリティ問題があります: ${criticalThreats.map(t => t.description).join(', ')}`); } } const status = await this.git.status(); if (status.isClean() && !files) { return { success: false, message: '変更がありません。コミットする内容がありません。', details: {}, warnings: ['変更がないため、Git操作をスキップしました'], executionTime: Date.now() - startTime }; } if (files && files.length > 0) { await this.git.add(files); } else { await this.git.add('.'); } const changes = await this.analyzeChanges(files); const diff = await this.git.diff(['--cached']); const workflowResult = await this.subAgentManager.executeGitWorkflow({ files: changes.files, diff, changes, branchName: options.branchName || await this.getCurrentBranch(), targetBranch: options.targetBranch }); if (workflowResult.errors.length > 0) { warnings.push(...workflowResult.errors); } if (workflowResult.safety.level === 'DANGER') { return { success: false, message: '❌ 安全性チェックで重大な問題が検出されました。Git操作を停止します。', details: { safety: workflowResult.safety }, warnings: workflowResult.safety.recommendations, executionTime: Date.now() - startTime }; } if (workflowResult.safety.level === 'WARNING' && workflowResult.safety.safetyScore < 70) { warnings.push('⚠️ 安全性に関する警告があります。慎重に進めてください。'); } let commitHash = ''; if (options.autoCommit !== false) { const commitResult = await this.git.commit(workflowResult.commitMessage.title); commitHash = commitResult.commit; } let branchName = options.branchName; if (!branchName) { branchName = await this.getCurrentBranch(); } if (options.autoPush !== false && commitHash) { await this.git.push('origin', branchName); } let prNumber; if (options.createPR && workflowResult.prManagement && commitHash) { try { const pr = await this.createPullRequest(workflowResult.prManagement, branchName, options.targetBranch || 'main'); prNumber = pr.number; } catch (error) { warnings.push(`PR作成に失敗しました: ${error}`); } } return { success: true, message: this.buildSuccessMessage(workflowResult, commitHash, prNumber), details: { commit: commitHash, branch: branchName, pr: prNumber, safety: workflowResult.safety, commitMessage: workflowResult.commitMessage, prManagement: workflowResult.prManagement }, warnings, executionTime: Date.now() - startTime }; } catch (error) { return { success: false, message: `Git操作が失敗しました: ${error}`, details: {}, warnings, executionTime: Date.now() - startTime }; } } async createPullRequest(prManagement, branchName, targetBranch = 'main') { try { // Unified MCP Manager を優先使用 if (this.mcpManager.isServerAvailable('github')) { console.log('🔗 Unified MCP経由でPR作成中...'); const mcpResult = await this.mcpManager.createPullRequest({ title: prManagement.prTitle, body: prManagement.prBody, head: branchName, base: targetBranch, draft: false, maintainer_can_modify: true }); if (mcpResult.success && mcpResult.data) { const prNumber = mcpResult.data.number; // ラベルとレビュアーの設定はOctokitでフォローアップ await this.configurePullRequestSettings(prNumber, prManagement); return { number: prNumber, url: mcpResult.data.url }; } else { console.warn('⚠️ MCP PR作成失敗、Octokitでフォールバック:', mcpResult.error); } } // フォールバック: 従来のOctokit方式 console.log('🔗 Octokit経由でPR作成中...'); const response = await this.octokit.rest.pulls.create({ owner: this.config.github.owner, repo: this.config.github.repo, title: prManagement.prTitle, body: prManagement.prBody, head: branchName, base: targetBranch }); const prNumber = response.data.number; // PR設定の適用 await this.configurePullRequestSettings(prNumber, prManagement); return { number: prNumber, url: response.data.html_url }; } catch (error) { throw new Error(`Failed to create pull request: ${error}`); } } /** * PR設定(ラベル、レビュアー、自動マージ)の適用 */ async configurePullRequestSettings(prNumber, prManagement) { try { // ラベルの追加 if (prManagement.labels.length > 0) { await this.octokit.rest.issues.addLabels({ owner: this.config.github.owner, repo: this.config.github.repo, issue_number: prNumber, labels: prManagement.labels }); } // レビュアーの追加 if (prManagement.reviewers.length > 0) { await this.octokit.rest.pulls.requestReviewers({ owner: this.config.github.owner, repo: this.config.github.repo, pull_number: prNumber, reviewers: prManagement.reviewers }); } // アサインの追加 if (prManagement.assignees.length > 0) { await this.octokit.rest.issues.addAssignees({ owner: this.config.github.owner, repo: this.config.github.repo, issue_number: prNumber, assignees: prManagement.assignees }); } // 自動マージのスケジュール if (prManagement.autoMerge) { setTimeout(async () => { try { await this.attemptAutoMergeMCP(prNumber, prManagement.mergeStrategy); } catch (error) { console.warn(`自動マージに失敗しました: ${error}`); } }, 30000); // 30秒後に自動マージを試行 } } catch (error) { console.warn('⚠️ PR設定の一部に失敗しました:', error); } } /** * MCP対応の自動マージ試行 */ async attemptAutoMergeMCP(prNumber, mergeStrategy = 'squash') { try { console.log(`🔀 PR #${prNumber} の自動マージを試行中...`); // Unified MCP Manager を優先使用 if (this.mcpManager.isServerAvailable('github')) { console.log('🔗 MCP経由でPR状態確認中...'); const statusResult = await this.mcpManager.getPullRequestStatus(prNumber); if (!statusResult.success) { console.warn('⚠️ MCP PR状態確認失敗、Octokitでフォールバック'); return await this.attemptAutoMerge(prNumber, mergeStrategy); } const prStatus = statusResult.data; if (!prStatus.mergeable || prStatus.mergeable_state !== 'clean') { console.log(`⏸️ PR #${prNumber} はマージ可能状態ではありません`); return false; } // MCP経由でマージ実行 console.log('🔗 MCP経由でPRマージ実行中...'); const mergeResult = await this.mcpManager.mergePullRequest({ pullNumber: prNumber, mergeMethod: mergeStrategy, commitTitle: `Merge PR #${prNumber}`, commitMessage: `Auto-merge via GitHub MCP` }); if (mergeResult.success) { console.log(`✅ PR #${prNumber} のマージ成功(MCP経由)`); // ブランチ削除を試行 try { await this.mcpManager.deleteBranch(`pr-${prNumber}`); } catch (error) { console.warn('⚠️ ブランチ削除に失敗:', error); } return true; } else { console.warn('⚠️ MCP マージ失敗、Octokitでフォールバック:', mergeResult.error); } } // フォールバック: 従来のOctokit方式 return await this.attemptAutoMerge(prNumber, mergeStrategy); } catch (error) { console.error(`❌ 自動マージ試行エラー: ${error}`); return false; } } async attemptAutoMerge(prNumber, mergeStrategy = 'squash') { try { const pr = await this.octokit.rest.pulls.get({ owner: this.config.github.owner, repo: this.config.github.repo, pull_number: prNumber }); if (!pr.data.mergeable || pr.data.mergeable_state !== 'clean') { return false; } const checks = await this.octokit.rest.checks.listForRef({ owner: this.config.github.owner, repo: this.config.github.repo, ref: pr.data.head.sha }); const allChecksPassed = checks.data.check_runs.every(check => check.status === 'completed' && check.conclusion === 'success'); if (!allChecksPassed) { return false; } await this.octokit.rest.pulls.merge({ owner: this.config.github.owner, repo: this.config.github.repo, pull_number: prNumber, merge_method: mergeStrategy }); if (this.config.github.token && pr.data.head.ref !== 'main') { try { await this.octokit.rest.git.deleteRef({ owner: this.config.github.owner, repo: this.config.github.repo, ref: `heads/${pr.data.head.ref}` }); } catch (error) { console.warn(`ブランチ削除に失敗しました: ${error}`); } } return true; } catch (error) { console.error(`Auto-merge failed: ${error}`); return false; } } async getProjectContext() { try { const packageJsonPath = join(this.projectPath, 'package.json'); let packageJson = {}; try { const content = await fs.readFile(packageJsonPath, 'utf-8'); packageJson = JSON.parse(content); } catch { // package.json が存在しない場合 } const log = await this.git.log({ maxCount: 10 }); const branches = await this.git.branch(); return { name: packageJson.name || basename(this.projectPath), type: this.detectProjectType(packageJson), language: this.detectLanguage(packageJson), framework: this.detectFramework(packageJson), dependencies: Object.keys(packageJson.dependencies || {}), gitHistory: { recentCommits: log.all.map(commit => commit.message), branches: branches.all, contributors: [...new Set(log.all.map(commit => commit.author_name))] } }; } catch (error) { throw new Error(`Failed to get project context: ${error}`); } } async getCurrentBranch() { const status = await this.git.status(); return status.current || 'main'; } determineChangeType(files, diff) { const testFiles = files.filter(f => f.includes('test') || f.includes('spec')); const docFiles = files.filter(f => f.endsWith('.md') || f.includes('doc')); const styleFiles = files.filter(f => f.endsWith('.css') || f.endsWith('.scss') || f.endsWith('.less')); if (testFiles.length > files.length * 0.7) return 'test'; if (docFiles.length > files.length * 0.7) return 'docs'; if (styleFiles.length > files.length * 0.7) return 'style'; if (diff.includes('fix') || diff.includes('bug') || diff.includes('error')) { return 'bugfix'; } if (diff.includes('refactor') || diff.includes('clean') || diff.includes('optimize')) { return 'refactor'; } return 'feature'; } calculateImpact(fileCount, lineCount) { if (fileCount <= 3 && lineCount <= 50) return 'low'; if (fileCount <= 10 && lineCount <= 200) return 'medium'; return 'high'; } calculateComplexity(diff, files) { let complexity = 0; complexity += files.length * 2; complexity += (diff.match(/^\+/gm) || []).length * 0.5; complexity += (diff.match(/^-/gm) || []).length * 0.3; const patterns = [ /function\s+\w+/g, /class\s+\w+/g, /interface\s+\w+/g, /if\s*\(/g, /for\s*\(/g, /while\s*\(/g ]; patterns.forEach(pattern => { const matches = diff.match(pattern) || []; complexity += matches.length * 3; }); return Math.min(complexity, 100); } generateChangeDescription(files, type) { const fileCount = files.length; const typeDesc = { feature: '新機能追加', bugfix: 'バグ修正', refactor: 'リファクタリング', docs: 'ドキュメント更新', test: 'テスト追加・修正', style: 'スタイル調整' }; return `${typeDesc[type]} (${fileCount}ファイル変更)`; } detectProjectType(packageJson) { if (packageJson.scripts?.dev?.includes('next')) return 'Next.js'; if (packageJson.dependencies?.react) return 'React'; if (packageJson.dependencies?.vue) return 'Vue.js'; if (packageJson.dependencies?.express) return 'Express'; if (packageJson.type === 'module') return 'ES Module'; return 'Node.js'; } detectLanguage(packageJson) { if (packageJson.dependencies?.typescript || packageJson.devDependencies?.typescript) { return 'TypeScript'; } return 'JavaScript'; } detectFramework(packageJson) { const frameworks = { 'next': 'Next.js', 'react': 'React', 'vue': 'Vue.js', 'express': 'Express', 'fastify': 'Fastify', 'nest': 'NestJS' }; for (const [dep, framework] of Object.entries(frameworks)) { if (packageJson.dependencies?.[dep] || packageJson.devDependencies?.[dep]) { return framework; } } return undefined; } buildSuccessMessage(result, commitHash, prNumber) { let message = `✅ Git操作が完了しました\n\n`; message += `📝 コミット: ${result.commitMessage.title}\n`; message += `🔒 安全性: ${result.safety.level} (スコア: ${result.safety.safetyScore})\n`; message += `🆔 ハッシュ: ${commitHash.substring(0, 8)}\n`; if (prNumber) { message += `🔀 PR: #${prNumber}\n`; if (result.prManagement?.autoMerge) { message += `⚡ 自動マージ予定\n`; } } return message; } /** * Cleanup all resources including MCP connections * Fail Fast: Comprehensive cleanup with error handling */ async cleanup() { try { console.log('🧹 GitOperations リソースクリーンアップ中...'); // Cleanup MCP Manager await this.mcpManager.cleanup(); console.log('✅ GitOperations クリーンアップ完了'); } catch (error) { console.error('❌ GitOperations クリーンアップエラー:', error); throw error; } } } //# sourceMappingURL=git-operations.js.map