Spaces:
Sleeping
Sleeping
| import { Octokit } from '@octokit/rest'; | |
| import { throttling } from '@octokit/plugin-throttling'; | |
| import { retry } from '@octokit/plugin-retry'; | |
| import logger from '../utils/logger.js'; | |
| const OctokitWithPlugins = Octokit.plugin(throttling, retry); | |
| class GitHubService { | |
| constructor(config) { | |
| this.octokit = new OctokitWithPlugins({ | |
| auth: config.github.token, | |
| request: { | |
| timeout: 15000, | |
| }, | |
| throttle: { | |
| onRateLimit: (retryAfter, options, octokit, retryCount) => { | |
| logger.warn(`Rate limit hit for ${options.method} ${options.url}`); | |
| if (retryCount < 2) return true; | |
| return false; | |
| }, | |
| onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => { | |
| logger.warn(`Secondary rate limit hit for ${options.method} ${options.url}`); | |
| if (retryCount < 2) return true; | |
| return false; | |
| }, | |
| }, | |
| retry: { | |
| doNotRetry: [400, 401, 403, 404, 422], | |
| retries: 2, | |
| }, | |
| }); | |
| this.owner = config.github.owner; | |
| this.repo = config.github.repo; | |
| this._requestCount = 0; | |
| } | |
| async createIssue(title, body, labels = []) { | |
| try { | |
| const response = await this.octokit.issues.create({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| title, | |
| body, | |
| labels, | |
| }); | |
| logger.info(`Created issue #${response.data.number}: ${title}`); | |
| return response.data; | |
| } catch (error) { | |
| logger.error(`Failed to create issue: ${error.message}`); | |
| throw error; | |
| } | |
| } | |
| async listIssues(state = 'open', labels = []) { | |
| try { | |
| const response = await this.octokit.issues.listForRepo({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| state, | |
| labels: labels.length > 0 ? labels.join(',') : undefined, | |
| per_page: 30, | |
| }); | |
| return response.data; | |
| } catch (error) { | |
| logger.error(`Failed to list issues: ${error.message}`); | |
| return []; | |
| } | |
| } | |
| async createBranch(branchName, baseBranch = 'main') { | |
| try { | |
| const { data: ref } = await this.octokit.git.getRef({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| ref: `heads/${baseBranch}`, | |
| }); | |
| await this.octokit.git.createRef({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| ref: `refs/heads/${branchName}`, | |
| sha: ref.object.sha, | |
| }); | |
| logger.info(`Created branch: ${branchName} from ${baseBranch}`); | |
| return ref; | |
| } catch (error) { | |
| if (error.status === 422) { | |
| logger.warn(`Branch ${branchName} already exists`); | |
| } else { | |
| logger.error(`Failed to create branch: ${error.message}`); | |
| throw error; | |
| } | |
| } | |
| } | |
| async deleteBranch(branchName) { | |
| try { | |
| await this.octokit.git.deleteRef({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| ref: `heads/${branchName}`, | |
| }); | |
| logger.info(`Deleted branch: ${branchName}`); | |
| } catch (error) { | |
| logger.error(`Failed to delete branch: ${error.message}`); | |
| } | |
| } | |
| async createPullRequest(title, body, head, base = 'main') { | |
| try { | |
| const response = await this.octokit.pulls.create({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| title, | |
| body, | |
| head, | |
| base, | |
| }); | |
| logger.info(`Created PR #${response.data.number}: ${title}`); | |
| return response.data; | |
| } catch (error) { | |
| logger.error(`Failed to create PR: ${error.message}`); | |
| throw error; | |
| } | |
| } | |
| async listPullRequests(state = 'open') { | |
| try { | |
| const response = await this.octokit.pulls.list({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| state, | |
| per_page: 20, | |
| }); | |
| return response.data; | |
| } catch (error) { | |
| logger.error(`Failed to list PRs: ${error.message}`); | |
| return []; | |
| } | |
| } | |
| async mergePullRequest(pullNumber, mergeMethod = 'merge') { | |
| try { | |
| const response = await this.octokit.pulls.merge({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| pull_number: pullNumber, | |
| merge_method: mergeMethod, | |
| }); | |
| logger.info(`Merged PR #${pullNumber}`); | |
| return response.data; | |
| } catch (error) { | |
| logger.error(`Failed to merge PR: ${error.message}`); | |
| throw error; | |
| } | |
| } | |
| async addPullRequestComment(pullNumber, body) { | |
| try { | |
| await this.octokit.issues.createComment({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| issue_number: pullNumber, | |
| body, | |
| }); | |
| logger.info(`Added comment to PR #${pullNumber}`); | |
| } catch (error) { | |
| logger.error(`Failed to add PR comment: ${error.message}`); | |
| } | |
| } | |
| async addPullRequestReview(pullNumber, event, body, commitId) { | |
| try { | |
| await this.octokit.pulls.createReview({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| pull_number: pullNumber, | |
| body, | |
| event, | |
| commit_id: commitId, | |
| }); | |
| logger.info(`Added ${event} review to PR #${pullNumber}`); | |
| } catch (error) { | |
| logger.error(`Failed to add PR review: ${error.message}`); | |
| } | |
| } | |
| async getPullRequestFiles(pullNumber) { | |
| try { | |
| const response = await this.octokit.pulls.listFiles({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| pull_number: pullNumber, | |
| }); | |
| return response.data; | |
| } catch (error) { | |
| logger.error(`Failed to get PR files: ${error.message}`); | |
| return []; | |
| } | |
| } | |
| async getPullRequest(pullNumber) { | |
| try { | |
| const response = await this.octokit.pulls.get({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| pull_number: pullNumber, | |
| }); | |
| return response.data; | |
| } catch (error) { | |
| logger.error(`Failed to get PR: ${error.message}`); | |
| return null; | |
| } | |
| } | |
| async closeIssue(issueNumber) { | |
| try { | |
| await this.octokit.issues.update({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| issue_number: issueNumber, | |
| state: 'closed', | |
| }); | |
| logger.info(`Closed issue #${issueNumber}`); | |
| } catch (error) { | |
| logger.error(`Failed to close issue: ${error.message}`); | |
| } | |
| } | |
| async addLabels(issueNumber, labels) { | |
| try { | |
| await this.octokit.issues.addLabels({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| issue_number: issueNumber, | |
| labels, | |
| }); | |
| } catch (error) { | |
| logger.error(`Failed to add labels: ${error.message}`); | |
| } | |
| } | |
| async getIssueComments(issueNumber) { | |
| try { | |
| const response = await this.octokit.issues.listComments({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| issue_number: issueNumber, | |
| }); | |
| return response.data; | |
| } catch (error) { | |
| logger.error(`Failed to get issue comments: ${error.message}`); | |
| return []; | |
| } | |
| } | |
| async addIssueComment(issueNumber, body) { | |
| try { | |
| const response = await this.octokit.issues.createComment({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| issue_number: issueNumber, | |
| body, | |
| }); | |
| logger.info(`Added comment to issue #${issueNumber}`); | |
| return response.data; | |
| } catch (error) { | |
| logger.error(`Failed to add issue comment: ${error.message}`); | |
| throw error; | |
| } | |
| } | |
| async getRepositoryContent(path, ref = 'main') { | |
| try { | |
| const response = await this.octokit.repos.getContent({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| path, | |
| ref, | |
| }); | |
| return response.data; | |
| } catch (error) { | |
| if (error.status === 404) { | |
| return null; | |
| } | |
| logger.error(`Failed to get repo content: ${error.message}`); | |
| return null; | |
| } | |
| } | |
| async createOrUpdateFile(path, content, message, branch, sha = null) { | |
| try { | |
| const contentBase64 = Buffer.from(content).toString('base64'); | |
| if (!sha) { | |
| const existing = await this.getRepositoryContent(path, branch); | |
| sha = existing?.sha || null; | |
| } | |
| const response = await this.octokit.repos.createOrUpdateFileContents({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| path, | |
| message, | |
| content: contentBase64, | |
| branch, | |
| sha, | |
| }); | |
| logger.info(`Updated file: ${path} on ${branch}`); | |
| return response.data; | |
| } catch (error) { | |
| logger.error(`Failed to update file: ${error.message}`); | |
| throw error; | |
| } | |
| } | |
| async getCommitHistory(branch = 'main', perPage = 10) { | |
| try { | |
| const response = await this.octokit.repos.listCommits({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| sha: branch, | |
| per_page: perPage, | |
| }); | |
| return response.data; | |
| } catch (error) { | |
| logger.error(`Failed to get commit history: ${error.message}`); | |
| return []; | |
| } | |
| } | |
| async getRepositoryInfo() { | |
| try { | |
| const response = await this.octokit.repos.get({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| }); | |
| return response.data; | |
| } catch (error) { | |
| logger.error(`Failed to get repository info: ${error.message}`); | |
| return null; | |
| } | |
| } | |
| getStats() { | |
| return { | |
| requestCount: this._requestCount, | |
| owner: this.owner, | |
| repo: this.repo, | |
| }; | |
| } | |
| } | |
| export default GitHubService; | |