'use client' import { GitHubService, type GitHubRepo, type GitHubCommit } from './github-service' export interface GitHubVersionControlConfig { accessToken: string owner: string repo: string branch?: string } export interface BookFile { id: string name: string path: string content: string type: 'file' | 'folder' } export interface FileChange { id: string name: string path: string changeType: 'added' | 'modified' | 'deleted' currentContent: string lastCommittedContent: string linesAdded: number linesRemoved: number } export class GitHubVersionControl { private githubService: GitHubService private config: GitHubVersionControlConfig constructor(config: GitHubVersionControlConfig) { this.githubService = new GitHubService() this.config = config } /** * Check if user has GitHub integration set up */ static async hasGitHubIntegration(bookId: string): Promise { try { const response = await fetch(`/api/books/${bookId}/github-integration`) return response.ok } catch { return false } } /** * Set up GitHub integration for a book */ static async setupGitHubIntegration( bookId: string, accessToken: string, repoOwner: string, repoName: string ): Promise { const response = await fetch(`/api/books/${bookId}/github-integration`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ accessToken, repoOwner, repoName }) }) if (!response.ok) { const error = await response.json() throw new Error(error.message || 'Failed to setup GitHub integration') } } /** * Get user's GitHub repositories */ async getUserRepos(): Promise { return this.githubService.getUserRepos(this.config.accessToken) } /** * Create a new repository for the book */ async createBookRepo( bookName: string, description?: string, isPrivate: boolean = true ): Promise { return this.githubService.createRepo( this.config.accessToken, bookName, description, isPrivate ) } /** * Get uncommitted changes by comparing local files with GitHub */ async getUncommittedChanges(localFiles: BookFile[]): Promise { const changes: FileChange[] = [] try { // Get latest commit to compare against const commits = await this.githubService.pullCommits( this.config.accessToken, this.config.owner, this.config.repo, this.config.branch || 'main' ) if (commits.length === 0) { // No commits yet - all files are "added" return localFiles.map(file => ({ id: file.id, name: file.name, path: file.path, changeType: 'added' as const, currentContent: file.content, lastCommittedContent: '', linesAdded: file.content.split('\n').length, linesRemoved: 0 })) } // Compare each local file with GitHub version for (const localFile of localFiles) { try { const githubContent = await this.githubService.getFileContent( this.config.accessToken, this.config.owner, this.config.repo, localFile.path ) if (githubContent !== localFile.content) { const localLines = localFile.content.split('\n') const githubLines = githubContent.split('\n') changes.push({ id: localFile.id, name: localFile.name, path: localFile.path, changeType: 'modified', currentContent: localFile.content, lastCommittedContent: githubContent, linesAdded: Math.max(0, localLines.length - githubLines.length), linesRemoved: Math.max(0, githubLines.length - localLines.length) }) } } catch (error) { // File doesn't exist in GitHub - it's a new file changes.push({ id: localFile.id, name: localFile.name, path: localFile.path, changeType: 'added', currentContent: localFile.content, lastCommittedContent: '', linesAdded: localFile.content.split('\n').length, linesRemoved: 0 }) } } return changes } catch (error) { console.error('Error getting uncommitted changes:', error) throw new Error('Failed to get uncommitted changes') } } /** * Commit changes to GitHub */ async commitChanges( message: string, files: BookFile[], authorName: string, authorEmail: string ): Promise { try { // For each file, update it in GitHub const updates = await Promise.all( files.map(async (file) => { try { // Check if file exists to get its SHA const existingFile = await this.githubService.getFileContent( this.config.accessToken, this.config.owner, this.config.repo, file.path ) // File exists, update it return this.githubService.updateFile( this.config.accessToken, this.config.owner, this.config.repo, file.path, file.content, message, this.config.branch ) } catch { // File doesn't exist, create it return this.githubService.updateFile( this.config.accessToken, this.config.owner, this.config.repo, file.path, file.content, message, this.config.branch ) } }) ) // Get the latest commit after our updates const commits = await this.githubService.pullCommits( this.config.accessToken, this.config.owner, this.config.repo, this.config.branch || 'main' ) return commits[0] } catch (error) { console.error('Error committing changes:', error) throw new Error('Failed to commit changes to GitHub') } } /** * Get commit history from GitHub */ async getCommitHistory(limit: number = 50): Promise { return this.githubService.pullCommits( this.config.accessToken, this.config.owner, this.config.repo, this.config.branch || 'main' ).then(commits => commits.slice(0, limit)) } /** * Checkout a specific commit (pull all files from that commit) */ async checkoutCommit(commitSha: string): Promise { try { // Get the tree for this commit const response = await fetch(`https://api.github.com/repos/${this.config.owner}/${this.config.repo}/git/trees/${commitSha}?recursive=1`, { headers: { 'Authorization': `Bearer ${this.config.accessToken}`, 'Accept': 'application/vnd.github.v3+json' } }) if (!response.ok) { throw new Error('Failed to get commit tree') } const tree = await response.json() const files: BookFile[] = [] // Get content for each file in the tree for (const item of tree.tree) { if (item.type === 'blob') { try { const content = await this.githubService.getFileContent( this.config.accessToken, this.config.owner, this.config.repo, item.path, commitSha ) files.push({ id: item.sha, // Use GitHub SHA as ID name: item.path.split('/').pop() || item.path, path: item.path, content, type: 'file' }) } catch (error) { console.warn(`Failed to get content for ${item.path}:`, error) } } } return files } catch (error) { console.error('Error checking out commit:', error) throw new Error('Failed to checkout commit') } } /** * Update configuration */ updateConfig(config: Partial) { this.config = { ...this.config, ...config } } } export default GitHubVersionControl