UNPKG

repoweaver

Version:

A GitHub App that skillfully weaves multiple templates together to create and update repositories with intelligent merge strategies

218 lines (188 loc) 5.74 kB
import { createAppAuth } from '@octokit/auth-app'; import { Octokit } from '@octokit/rest'; import { TemplateRepository } from './types'; export interface GitHubFile { name: string; path: string; content: string; type: 'file' | 'dir'; } export interface GitHubRepository { id?: number; owner: string; name: string; fullName: string; defaultBranch: string; isTemplate: boolean; } export class GitHubClient { private octokit: Octokit; private installationId: number; constructor(appId: string, privateKey: string, installationId: number) { this.installationId = installationId; this.octokit = new Octokit({ authStrategy: createAppAuth, auth: { appId, privateKey, installationId, }, }); } async createRepository(name: string, description?: string, isPrivate = false): Promise<GitHubRepository> { const response = await this.octokit.rest.repos.createForAuthenticatedUser({ name, description, private: isPrivate, auto_init: true, }); return { id: response.data.id, owner: response.data.owner.login, name: response.data.name, fullName: response.data.full_name, defaultBranch: response.data.default_branch, isTemplate: response.data.is_template || false, }; } async getRepository(owner: string, name: string): Promise<GitHubRepository> { const response = await this.octokit.rest.repos.get({ owner, repo: name, }); return { id: response.data.id, owner: response.data.owner.login, name: response.data.name, fullName: response.data.full_name, defaultBranch: response.data.default_branch, isTemplate: response.data.is_template || false, }; } async getRepositoryContents(owner: string, repo: string, path = '', ref?: string): Promise<GitHubFile[]> { const response = await this.octokit.rest.repos.getContent({ owner, repo, path, ref, }); const items = Array.isArray(response.data) ? response.data : [response.data]; const files: GitHubFile[] = []; for (const item of items) { if (item.type === 'file' && item.content) { files.push({ name: item.name, path: item.path, content: Buffer.from(item.content, 'base64').toString('utf-8'), type: 'file', }); } else if (item.type === 'dir') { files.push({ name: item.name, path: item.path, content: '', type: 'dir', }); // Recursively get directory contents const dirContents = await this.getRepositoryContents(owner, repo, item.path, ref); files.push(...dirContents); } } return files; } async createOrUpdateFile(owner: string, repo: string, path: string, content: string, message: string, branch?: string): Promise<void> { // Check if file exists let sha: string | undefined; try { const existing = await this.octokit.rest.repos.getContent({ owner, repo, path, ref: branch, }); if (!Array.isArray(existing.data) && existing.data.type === 'file') { sha = existing.data.sha; } } catch (error) { // File doesn't exist, which is fine } await this.octokit.rest.repos.createOrUpdateFileContents({ owner, repo, path, message, content: Buffer.from(content, 'utf-8').toString('base64'), sha, branch, }); } async createBranch(owner: string, repo: string, branchName: string, fromBranch?: string): Promise<void> { // Get the SHA of the base branch const baseRef = fromBranch || 'main'; const baseBranchResponse = await this.octokit.rest.git.getRef({ owner, repo, ref: `heads/${baseRef}`, }); // Create new branch await this.octokit.rest.git.createRef({ owner, repo, ref: `refs/heads/${branchName}`, sha: baseBranchResponse.data.object.sha, }); } async createPullRequest(owner: string, repo: string, title: string, body: string, head: string, base: string): Promise<number> { const response = await this.octokit.rest.pulls.create({ owner, repo, title, body, head, base, }); return response.data.number; } async parseRepositoryUrl(url: string): Promise<{ owner: string; repo: string; branch?: string }> { // Parse GitHub repository URL const match = url.match(/github\.com\/([^\/]+)\/([^\/]+?)(?:\.git)?(?:\/tree\/([^\/]+))?/); if (!match) { throw new Error(`Invalid GitHub repository URL: ${url}`); } return { owner: match[1], repo: match[2], branch: match[3], }; } async getTemplateFiles(template: TemplateRepository): Promise<GitHubFile[]> { const { owner, repo, branch } = await this.parseRepositoryUrl(template.url); const ref = template.branch || branch || 'main'; let files = await this.getRepositoryContents(owner, repo, '', ref); // Filter out .git directory files = files.filter((file) => !file.path.startsWith('.git')); // If subDirectory is specified, filter to only include files from that directory if (template.subDirectory) { files = files.filter((file) => file.path.startsWith(template.subDirectory!)); // Remove the subdirectory prefix from paths files = files.map((file) => ({ ...file, path: file.path.replace(new RegExp(`^${template.subDirectory}/`), ''), })); } return files; } /*async addRepositoryToInstallation(repositoryName: string): Promise<void> { await this.octokit.rest.apps.addRepoToInstallationForAuthenticatedUser({ installation_id: this.installationId, repository_id: r.data.id, }); } async removeRepositoryFromInstallation(repositoryName: string): Promise<void> { const { owner, repo } = await this.getRepository(this.parseRepositoryUrl(url).owner, repositoryName); await this.octokit.rest.apps.removeRepoFromInstallation({ installation_id: this.installationId, repository_names: [repositoryName], }); }*/ }