'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