import { Octokit } from '@octokit/rest'; import { GitHubPort, GitHubConfig, PullRequestInfo, IssueInfo } from '../../domain/ports/GitHubPort'; import { ReviewReport, ReviewComment } from '../../domain/entities'; export class GitHubAdapter implements GitHubPort { private octokit: Octokit; constructor(config: GitHubConfig) { this.octokit = new Octokit({ auth: config.token, ...(config.baseUrl && { baseUrl: config.baseUrl }), userAgent: 'ai-developer-assistant', }); } async postReviewComment( owner: string, repo: string, pullNumber: number, comment: ReviewComment ): Promise { try { // Get the latest commit from the PR to use as commit_id const prResponse = await this.octokit.rest.pulls.get({ owner, repo, pull_number: pullNumber, }); await this.octokit.rest.pulls.createReviewComment({ owner, repo, pull_number: pullNumber, body: this.formatReviewComment(comment), commit_id: prResponse.data.head.sha, path: comment.filePath, line: comment.lineNumber || 1, side: 'RIGHT', }); } catch (error) { throw new Error(`Failed to post review comment: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async postReviewComments( owner: string, repo: string, pullNumber: number, comments: ReviewComment[] ): Promise { for (const comment of comments) { await this.postReviewComment(owner, repo, pullNumber, comment); } } async postReviewReport( owner: string, repo: string, pullNumber: number, report: ReviewReport ): Promise { try { const body = this.formatReviewReport(report); await this.octokit.rest.issues.createComment({ owner, repo, issue_number: pullNumber, body, }); } catch (error) { throw new Error(`Failed to post review report: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getPullRequest( owner: string, repo: string, pullNumber: number ): Promise { try { const response = await this.octokit.rest.pulls.get({ owner, repo, pull_number: pullNumber, }); const pr = response.data; // Get files changed in the PR const filesResponse = await this.octokit.rest.pulls.listFiles({ owner, repo, pull_number: pullNumber, }); // Get commits in the PR const commitsResponse = await this.octokit.rest.pulls.listCommits({ owner, repo, pull_number: pullNumber, }); return { number: pr.number, title: pr.title, body: pr.body || '', author: pr.user?.login || '', baseBranch: pr.base.ref, headBranch: pr.head.ref, state: pr.state as 'open' | 'closed' | 'merged', files: filesResponse.data.map(file => file.filename), commits: commitsResponse.data.map(commit => commit.sha), }; } catch (error) { if (error instanceof Error && error.message.includes('404')) { return null; } throw new Error(`Failed to get pull request: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getIssue( owner: string, repo: string, issueNumber: number ): Promise { try { const response = await this.octokit.rest.issues.get({ owner, repo, issue_number: issueNumber, }); const issue = response.data; return { number: issue.number, title: issue.title, body: issue.body || '', author: issue.user?.login || '', state: issue.state as 'open' | 'closed', labels: issue.labels.map(label => typeof label === 'string' ? label : label.name || '').filter(Boolean), }; } catch (error) { if (error instanceof Error && error.message.includes('404')) { return null; } throw new Error(`Failed to get issue: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getRepository(owner: string, repo: string): Promise<{ readonly name: string; readonly fullName: string; readonly description: string; readonly language: string; readonly defaultBranch: string; readonly isPrivate: boolean; } | null> { try { const response = await this.octokit.rest.repos.get({ owner, repo, }); const repository = response.data; return { name: repository.name, fullName: repository.full_name, description: repository.description || '', language: repository.language || '', defaultBranch: repository.default_branch, isPrivate: repository.private, }; } catch (error) { if (error instanceof Error && error.message.includes('404')) { return null; } throw new Error(`Failed to get repository: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async isAccessible(): Promise { try { await this.octokit.rest.users.getAuthenticated(); return true; } catch (error) { return false; } } async getCurrentUser(): Promise<{ readonly login: string; readonly name: string; readonly email: string; } | null> { try { const response = await this.octokit.rest.users.getAuthenticated(); const user = response.data; return { login: user.login, name: user.name || '', email: user.email || '', }; } catch (error) { return null; } } private formatReviewComment(comment: ReviewComment): string { let body = `**${comment.severity.toUpperCase()}** [${comment.category}]\n\n`; body += `${comment.message}\n\n`; if (comment.suggestion) { body += `**💡 Suggestion:**\n${comment.suggestion}\n\n`; } if (comment.codeSnippet) { body += `**Code:**\n\`\`\`\n${comment.codeSnippet}\n\`\`\`\n\n`; } body += `---\n*Generated by AI Developer Assistant*`; return body; } private formatReviewReport(report: ReviewReport): string { let body = `## 📋 Review Report\n\n`; body += `**Summary:** ${report.summary}\n\n`; body += `**Overall Score:** ${report.overallScore}/100\n\n`; body += `**Timestamp:** ${report.timestamp.toISOString()}\n\n`; body += `### 📊 Metrics\n\n`; body += `- Total Comments: ${report.metrics.totalComments}\n`; body += `- Errors: ${report.metrics.errorCount}\n`; body += `- Warnings: ${report.metrics.warningCount}\n`; body += `- Info: ${report.metrics.infoCount}\n`; body += `- Suggestions: ${report.metrics.suggestionCount}\n`; body += `- Files Reviewed: ${report.metrics.filesReviewed}\n`; body += `- Lines Added: ${report.metrics.linesAdded}\n`; body += `- Lines Removed: ${report.metrics.linesRemoved}\n\n`; if (report.comments.length > 0) { body += `### 💬 Comments\n\n`; // Group comments by file const commentsByFile = report.comments.reduce((groups, comment) => { const file = comment.filePath; if (!groups[file]) { groups[file] = []; } groups[file].push(comment); return groups; }, {} as Record); for (const [file, comments] of Object.entries(commentsByFile)) { body += `#### ${file}\n\n`; for (const comment of comments) { body += `- **${comment.severity.toUpperCase()}** [${comment.category}] ${comment.message}`; if (comment.lineNumber) { body += ` (Line ${comment.lineNumber})`; } body += `\n`; if (comment.suggestion) { body += ` - 💡 **Suggestion:** ${comment.suggestion}\n`; } } body += `\n`; } } if (report.recommendations.length > 0) { body += `### 🎯 Recommendations\n\n`; for (const rec of report.recommendations) { body += `- ${rec}\n`; } body += `\n`; } body += `---\n*Generated by AI Developer Assistant*`; return body; } }