bookwiz.io / lib / services / github-version-control.ts
github-version-control.ts
Raw
'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<boolean> {
    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<void> {
    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<GitHubRepo[]> {
    return this.githubService.getUserRepos(this.config.accessToken)
  }

  /**
   * Create a new repository for the book
   */
  async createBookRepo(
    bookName: string,
    description?: string,
    isPrivate: boolean = true
  ): Promise<GitHubRepo> {
    return this.githubService.createRepo(
      this.config.accessToken,
      bookName,
      description,
      isPrivate
    )
  }

  /**
   * Get uncommitted changes by comparing local files with GitHub
   */
  async getUncommittedChanges(localFiles: BookFile[]): Promise<FileChange[]> {
    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<GitHubCommit> {
    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<GitHubCommit[]> {
    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<BookFile[]> {
    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<GitHubVersionControlConfig>) {
    this.config = { ...this.config, ...config }
  }
}

export default GitHubVersionControl